23#include <QCoreApplication>
30#include <QFtpCompatDirEntry>
33static const QString Host = QStringLiteral(
"127.0.0.1");
34static const int PlainPort = 2121;
35static const int TlsPort = 2122;
36static const QString User = QStringLiteral(
"root");
37static const QString Password = QStringLiteral(
"root");
41 static QUrl conn(
int port = PlainPort) {
43 u.setScheme(QStringLiteral(
"ftp"));
47 u.setPassword(Password);
50 static QUrl file(
const QString &path,
int port = PlainPort) {
52 u.setScheme(QStringLiteral(
"ftp"));
55 u.setPath(QLatin1Char(
'/') + path);
58 static QUrl dir(
const QString &path,
int port = PlainPort) {
60 if (!p.startsWith(QLatin1Char(
'/'))) p.prepend(QLatin1Char(
'/'));
61 if (!p.endsWith(QLatin1Char(
'/'))) p.append(QLatin1Char(
'/'));
63 u.setScheme(QStringLiteral(
"ftp"));
74class FtpTest :
public QObject
78 explicit FtpTest(QObject *parent =
nullptr) : QObject(parent)
81 this, &FtpTest::onCommandFinished);
85 [](qint64 done, qint64 total){
86 if (total > 0) qInfo().nospace() <<
"[progress] " << done <<
"/" << total;
87 else qInfo().nospace() <<
"[progress] ↓ " << done <<
" bytes";
90 [
this](
const QFtpCompatDirEntry &e){
91 qInfo() <<
"[entry ]" << (e.
isDirectory() ?
"DIR" :
"FILE")
93 m_lastListing.append(e);
96 [](
int code,
const QString &msg){ qInfo() <<
"[raw]" << code << msg; });
102 step(
"CONNECT (plain)");
103 m_openId = m_ftp.open(Urls::conn(PlainPort));
106 m_cleanupIds << m_ftp.remove(Urls::file(
"test.txt"))
107 << m_ftp.remove(Urls::file(
"small.txt"))
108 << m_ftp.remove(Urls::file(
"large.txt"))
109 << m_ftp.remove(Urls::file(
"renamed.txt"));
111 const QByteArray small =
"Hello from QFtpCompat!\nLine 2\nLine 3\n";
112 QByteArray large; large.reserve(128*1024);
113 while (large.size() < 128*1024) large.append(
"The quick brown fox jumps over the lazy dog. ");
115 step(
"UPLOAD small (QByteArray)");
116 m_uploadSmallId = m_ftp.put(Urls::file(
"small.txt"), small);
118 step(
"UPLOAD large (QIODevice, 128 KB)");
119 writeFile(
"/tmp/qftp_large.txt", large);
120 auto *src =
new QFile(
"/tmp/qftp_large.txt",
this);
121 src->open(QIODevice::ReadOnly);
122 m_uploadLargeId = m_ftp.put(Urls::file(
"large.txt"), src);
124 step(
"MKDIR subdir/");
125 m_mkdirId = m_ftp.mkdir(Urls::dir(
"subdir"));
127 step(
"UPLOAD subdir/nested.txt");
128 m_uploadNestedId = m_ftp.put(Urls::file(
"subdir/nested.txt"), small);
131 m_listRootId = m_ftp.list(Urls::dir(
""));
133 step(
"LIST /subdir/");
134 m_listSubId = m_ftp.list(Urls::dir(
"subdir"));
136 step(
"DOWNLOAD small → QByteArray");
137 m_downloadBaId = m_ftp.get(Urls::file(
"small.txt"), &m_downloadedBytes);
139 step(
"DOWNLOAD large → QFile");
140 m_downloadFile =
new QFile(
"/tmp/qftp_large_dl.txt",
this);
141 m_downloadFile->open(QIODevice::WriteOnly);
142 m_downloadLargeId = m_ftp.get(Urls::file(
"large.txt"), m_downloadFile);
144 step(
"RENAME small.txt → renamed.txt");
145 m_renameId = m_ftp.rename(Urls::file(
"small.txt"), Urls::file(
"renamed.txt"));
147 step(
"DELETE renamed / large / nested");
148 m_removeId = m_ftp.remove(Urls::file(
"renamed.txt"));
149 m_removeLargeId = m_ftp.remove(Urls::file(
"large.txt"));
150 m_removeNestedId = m_ftp.remove(Urls::file(
"subdir/nested.txt"));
152 step(
"RMDIR subdir/");
153 m_rmdirId = m_ftp.rmdir(Urls::dir(
"subdir"));
156 m_pwdId = m_ftp.pwd();
159 m_noopId = m_ftp.rawCommand(
"NOOP");
161 step(
"DOWNLOAD ghost.txt (expect 550)");
162 m_errorDlId = m_ftp.get(Urls::file(
"ghost.txt"), &m_ghostBytes);
164 step(
"LIST / final");
165 m_listFinalId = m_ftp.list(Urls::dir(
""));
172 void onCommandFinished(
int id,
bool hasError)
174 const QString label = m_cleanupIds.contains(
id) ? QStringLiteral(
"CLEANUP") :
175 id==m_openId ?
"OPEN" : id==m_uploadSmallId ?
"UPLOAD small" :
176 id==m_uploadLargeId ?
"UPLOAD large" : id==m_mkdirId ?
"MKDIR" :
177 id==m_uploadNestedId ?
"UPLOAD nested" : id==m_listRootId ?
"LIST /" :
178 id==m_listSubId ?
"LIST subdir" : id==m_downloadBaId ?
"DL→QByteArray" :
179 id==m_downloadLargeId?
"DL→QFile" : id==m_renameId ?
"RENAME" :
180 id==m_removeId ?
"DELETE renamed" : id==m_removeLargeId ?
"DELETE large" :
181 id==m_removeNestedId ?
"DELETE nested" : id==m_rmdirId ?
"RMDIR" :
182 id==m_pwdId ?
"PWD" : id==m_noopId ?
"NOOP" :
183 id==m_errorDlId ?
"DL ghost" : id==m_listFinalId ?
"LIST final" :
184 QString(
"id=%1").arg(id);
186 const bool errExpected = (
id == m_errorDlId) || m_cleanupIds.contains(
id);
188 if (hasError && !errExpected) qCritical() <<
" [FAIL]" << label << m_ftp.errorString();
189 else if (m_cleanupIds.contains(
id)) {}
190 else if (hasError) qInfo() <<
" [OK ]" << label <<
"(error expected)";
191 else qInfo() <<
" [OK ]" << label;
193 if (
id == m_listRootId) {
194 qInfo() <<
" → entries:" << m_lastListing.size();
195 m_lastListing.clear();
197 if (
id == m_listSubId) {
198 const bool found = std::any_of(m_lastListing.cbegin(), m_lastListing.cend(),
199 [](
const QFtpCompatDirEntry &e){ return e.name() ==
"nested.txt"; });
200 qInfo() <<
" → nested.txt:" << (found ?
"yes ✓" :
"no ✗");
201 m_lastListing.clear();
203 if (
id == m_downloadBaId) {
204 qInfo() <<
" → content match:"
205 << (m_downloadedBytes ==
"Hello from QFtpCompat!\nLine 2\nLine 3\n" ?
"✓" :
"✗");
207 if (
id == m_downloadLargeId) {
208 m_downloadFile->flush(); m_downloadFile->close();
209 qInfo() <<
" → size:" << QFile(
"/tmp/qftp_large_dl.txt").size();
211 if (
id == m_listFinalId) {
213 const auto unexpected = std::count_if(
214 m_lastListing.cbegin(), m_lastListing.cend(),
215 [](
const QFtpCompatDirEntry &e) {
216 return e.name() !=
".DS_Store"
217 && !e.name().endsWith(
".pem");
219 qInfo() <<
" → remaining (excl. permanent files):" << unexpected
220 << (unexpected == 0 ?
"(clean ✓)" :
"(unexpected files ✗)");
221 m_lastListing.clear();
222 qInfo() <<
"\n══ Plain FTP complete ══\n";
227 static void step(
const QString &l) { qInfo() <<
"\n──" << l; }
228 static void writeFile(
const QString &p,
const QByteArray &d) {
229 QFile f(p); f.open(QIODevice::WriteOnly); f.write(d);
234 QFile *m_downloadFile{
nullptr};
235 QByteArray m_downloadedBytes, m_ghostBytes;
236 QList<QFtpCompatDirEntry> m_lastListing;
237 QList<int> m_cleanupIds;
238 int m_openId{-1}, m_uploadSmallId{-1}, m_uploadLargeId{-1},
239 m_mkdirId{-1}, m_uploadNestedId{-1},
240 m_listRootId{-1}, m_listSubId{-1}, m_listFinalId{-1},
241 m_downloadBaId{-1}, m_downloadLargeId{-1},
242 m_renameId{-1}, m_removeId{-1}, m_removeLargeId{-1},
243 m_removeNestedId{-1}, m_rmdirId{-1},
244 m_pwdId{-1}, m_noopId{-1}, m_errorDlId{-1};
252class FtpTlsTest :
public QObject
256 explicit FtpTlsTest(QObject *parent =
nullptr) : QObject(parent)
263 this, &FtpTlsTest::onCommandFinished);
267 [](qint64 done, qint64 total){
268 if (total > 0) qInfo().nospace() <<
"[TLS progress] " << done <<
"/" << total <<
" bytes";
269 else qInfo().nospace() <<
"[TLS progress] ↓ " << done <<
" bytes";
272 [
this](
const QFtpCompatDirEntry &e) {
273 m_lastListing.append(e);
280 qInfo() <<
"\n══════════════════════════════════════════";
281 qInfo() <<
" Explicit FTPS test (port" << TlsPort <<
")";
282 qInfo() <<
"══════════════════════════════════════════";
284 step(
"CONNECT (Explicit FTPS – AUTH TLS)");
286 m_openId = m_ftp.open(Urls::conn(TlsPort),
287 QFtpCompat::EncryptionMode::Explicit);
290 m_cleanupIds << m_ftp.remove(Urls::file(
"tls_test.txt", TlsPort));
292 step(
"UPLOAD tls_test.txt (over encrypted channel)");
293 m_uploadId = m_ftp.put(Urls::file(
"tls_test.txt", TlsPort),
294 QByteArray(
"TLS upload works!\n"));
296 step(
"LIST / (verify file visible)");
297 m_listId = m_ftp.list(Urls::dir(
"", TlsPort));
299 step(
"DOWNLOAD tls_test.txt → QByteArray");
300 m_downloadId = m_ftp.get(Urls::file(
"tls_test.txt", TlsPort),
303 step(
"DELETE tls_test.txt");
304 m_removeId = m_ftp.remove(Urls::file(
"tls_test.txt", TlsPort));
306 step(
"LIST / final (expect empty)");
307 m_listFinalId = m_ftp.list(Urls::dir(
"", TlsPort));
311 void onCommandFinished(
int id,
bool hasError)
313 const QString label =
314 m_cleanupIds.contains(
id) ? QStringLiteral(
"CLEANUP") :
315 id==m_openId ?
"OPEN (FTPS)" : id==m_uploadId ?
"UPLOAD" :
316 id==m_listId ?
"LIST" : id==m_downloadId ?
"DOWNLOAD" :
317 id==m_removeId ?
"DELETE" : id==m_listFinalId?
"LIST final" :
318 QString(
"id=%1").arg(id);
320 const bool errExpected = m_cleanupIds.contains(
id);
322 if (hasError && !errExpected) qCritical() <<
" [FAIL]" << label << m_ftp.errorString();
323 else if (m_cleanupIds.contains(
id)) {}
324 else qInfo() <<
" [OK ]" << label;
326 if (
id == m_listId) {
327 const bool found = std::any_of(m_lastListing.cbegin(), m_lastListing.cend(),
328 [](
const QFtpCompatDirEntry &e){ return e.name() ==
"tls_test.txt"; });
329 qInfo() <<
" → tls_test.txt visible:" << (found ?
"yes ✓" :
"no ✗");
330 m_lastListing.clear();
332 if (
id == m_downloadId) {
333 qInfo() <<
" → content match:"
334 << (m_downloadedBytes ==
"TLS upload works!\n" ?
"✓" :
"✗");
336 if (
id == m_listFinalId) {
337 const auto unexpected = std::count_if(
338 m_lastListing.cbegin(), m_lastListing.cend(),
339 [](
const QFtpCompatDirEntry &e) {
340 return e.name() !=
".DS_Store"
341 && !e.name().endsWith(
".pem");
343 qInfo() <<
" → remaining (excl. permanent files):" << unexpected
344 << (unexpected == 0 ?
"(clean ✓)" :
"(unexpected files ✗)");
345 qInfo() <<
"\n══ Explicit FTPS complete ══\n";
346 QCoreApplication::quit();
350 static void step(
const QString &l) { qInfo() <<
"\n──" << l; }
354 QByteArray m_downloadedBytes;
355 QList<QFtpCompatDirEntry> m_lastListing;
356 QList<int> m_cleanupIds;
357 int m_openId{-1}, m_uploadId{-1}, m_listId{-1},
358 m_downloadId{-1}, m_removeId{-1}, m_listFinalId{-1};
362int main(
int argc,
char *argv[])
364 QCoreApplication app(argc, argv);
367 auto *plain =
new FtpTest(&app);
368 auto *tls =
new FtpTlsTest(&app);
370 QObject::connect(plain, &FtpTest::plainDone,
371 tls, &FtpTlsTest::run);
373 QTimer::singleShot(0, plain, &FtpTest::run);
QString name() const noexcept
File or directory name (never includes the path).
bool isDirectory() const noexcept
true for directory entries.
qint64 size() const noexcept
File size in bytes. 0 for directories.
void stateChanged(QFtpCompat::State newState)
Emitted when the connection state changes.
void rawCommandReply(int code, QString detail)
Emitted for rawCommand() responses and pwd() results.
void listInfo(QFtpCompatDirEntry entry)
Emitted once per entry during a list() command.
void commandFinished(int id, bool error)
Emitted when a command completes (successfully or with error).
void sslErrors(const QList< QSslError > &errors)
Emitted when SSL/TLS errors occur.
State
High-level connection state.
void dataTransferProgress(qint64 bytesTransferred, qint64 bytesTotal)
Emitted periodically during data transfers.