QFtpCompat 1.0.0
Qt6-compatible async FTP client library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
22
23#include <QCoreApplication>
24#include <QFile>
25#include <QTimer>
26#include <QUrl>
27#include <QDebug>
28
29#include <QFtpCompat>
30#include <QFtpCompatDirEntry>
31
32// ── Config ────────────────────────────────────────────────────────────────────
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");
38
39// ── URL factory ───────────────────────────────────────────────────────────────
40struct Urls {
41 static QUrl conn(int port = PlainPort) {
42 QUrl u;
43 u.setScheme(QStringLiteral("ftp"));
44 u.setHost(Host);
45 u.setPort(port);
46 u.setUserName(User);
47 u.setPassword(Password);
48 return u;
49 }
50 static QUrl file(const QString &path, int port = PlainPort) {
51 QUrl u;
52 u.setScheme(QStringLiteral("ftp"));
53 u.setHost(Host);
54 u.setPort(port);
55 u.setPath(QLatin1Char('/') + path);
56 return u;
57 }
58 static QUrl dir(const QString &path, int port = PlainPort) {
59 QString p = path;
60 if (!p.startsWith(QLatin1Char('/'))) p.prepend(QLatin1Char('/'));
61 if (!p.endsWith(QLatin1Char('/'))) p.append(QLatin1Char('/'));
62 QUrl u;
63 u.setScheme(QStringLiteral("ftp"));
64 u.setHost(Host);
65 u.setPort(port);
66 u.setPath(p);
67 return u;
68 }
69};
70
71// =============================================================================
72// Plain FTP test (port 2121) – full API coverage
73// =============================================================================
74class FtpTest : public QObject
75{
76 Q_OBJECT
77public:
78 explicit FtpTest(QObject *parent = nullptr) : QObject(parent)
79 {
80 connect(&m_ftp, &QFtpCompat::commandFinished,
81 this, &FtpTest::onCommandFinished);
82 connect(&m_ftp, &QFtpCompat::stateChanged, this,
83 [](QFtpCompat::State s){ qInfo() << "[state →]" << static_cast<int>(s); });
84 connect(&m_ftp, &QFtpCompat::dataTransferProgress, this,
85 [](qint64 done, qint64 total){
86 if (total > 0) qInfo().nospace() << "[progress] " << done << "/" << total;
87 else qInfo().nospace() << "[progress] ↓ " << done << " bytes";
88 });
89 connect(&m_ftp, &QFtpCompat::listInfo, this,
90 [this](const QFtpCompatDirEntry &e){
91 qInfo() << "[entry ]" << (e.isDirectory() ? "DIR" : "FILE")
92 << e.name() << e.size();
93 m_lastListing.append(e);
94 });
95 connect(&m_ftp, &QFtpCompat::rawCommandReply, this,
96 [](int code, const QString &msg){ qInfo() << "[raw]" << code << msg; });
97 }
98
99public slots:
100 void run()
101 {
102 step("CONNECT (plain)");
103 m_openId = m_ftp.open(Urls::conn(PlainPort));
104
105 step("CLEANUP");
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"));
110
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. ");
114
115 step("UPLOAD small (QByteArray)");
116 m_uploadSmallId = m_ftp.put(Urls::file("small.txt"), small);
117
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);
123
124 step("MKDIR subdir/");
125 m_mkdirId = m_ftp.mkdir(Urls::dir("subdir"));
126
127 step("UPLOAD subdir/nested.txt");
128 m_uploadNestedId = m_ftp.put(Urls::file("subdir/nested.txt"), small);
129
130 step("LIST /");
131 m_listRootId = m_ftp.list(Urls::dir(""));
132
133 step("LIST /subdir/");
134 m_listSubId = m_ftp.list(Urls::dir("subdir"));
135
136 step("DOWNLOAD small → QByteArray");
137 m_downloadBaId = m_ftp.get(Urls::file("small.txt"), &m_downloadedBytes);
138
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);
143
144 step("RENAME small.txt → renamed.txt");
145 m_renameId = m_ftp.rename(Urls::file("small.txt"), Urls::file("renamed.txt"));
146
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"));
151
152 step("RMDIR subdir/");
153 m_rmdirId = m_ftp.rmdir(Urls::dir("subdir"));
154
155 step("PWD");
156 m_pwdId = m_ftp.pwd();
157
158 step("RAW NOOP");
159 m_noopId = m_ftp.rawCommand("NOOP");
160
161 step("DOWNLOAD ghost.txt (expect 550)");
162 m_errorDlId = m_ftp.get(Urls::file("ghost.txt"), &m_ghostBytes);
163
164 step("LIST / final");
165 m_listFinalId = m_ftp.list(Urls::dir(""));
166 }
167
168signals:
169 void plainDone();
170
171private slots:
172 void onCommandFinished(int id, bool hasError)
173 {
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);
185
186 const bool errExpected = (id == m_errorDlId) || m_cleanupIds.contains(id);
187
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;
192
193 if (id == m_listRootId) {
194 qInfo() << " → entries:" << m_lastListing.size();
195 m_lastListing.clear();
196 }
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();
202 }
203 if (id == m_downloadBaId) {
204 qInfo() << " → content match:"
205 << (m_downloadedBytes == "Hello from QFtpCompat!\nLine 2\nLine 3\n" ? "✓" : "✗");
206 }
207 if (id == m_downloadLargeId) {
208 m_downloadFile->flush(); m_downloadFile->close();
209 qInfo() << " → size:" << QFile("/tmp/qftp_large_dl.txt").size();
210 }
211 if (id == m_listFinalId) {
212 // Filter out permanent ftproot files (.DS_Store, *.pem)
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");
218 });
219 qInfo() << " → remaining (excl. permanent files):" << unexpected
220 << (unexpected == 0 ? "(clean ✓)" : "(unexpected files ✗)");
221 m_lastListing.clear();
222 qInfo() << "\n══ Plain FTP complete ══\n";
223 emit plainDone();
224 }
225 }
226
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);
230 }
231
232private:
233 QFtpCompat m_ftp;
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};
245};
246
247// =============================================================================
248// Explicit FTPS test (port 2122)
249// Tests: connect with AUTH TLS, self-signed cert ignore, upload, download,
250// content verify, delete – confirms TLS data channel works end-to-end.
251// =============================================================================
252class FtpTlsTest : public QObject
253{
254 Q_OBJECT
255public:
256 explicit FtpTlsTest(QObject *parent = nullptr) : QObject(parent)
257 {
258 // Accept self-signed certificate from the test server
259 connect(&m_ftp, &QFtpCompat::sslErrors,
261
262 connect(&m_ftp, &QFtpCompat::commandFinished,
263 this, &FtpTlsTest::onCommandFinished);
264 connect(&m_ftp, &QFtpCompat::stateChanged, this,
265 [](QFtpCompat::State s){ qInfo() << "[TLS state →]" << static_cast<int>(s); });
266 connect(&m_ftp, &QFtpCompat::dataTransferProgress, this,
267 [](qint64 done, qint64 total){
268 if (total > 0) qInfo().nospace() << "[TLS progress] " << done << "/" << total << " bytes";
269 else qInfo().nospace() << "[TLS progress] ↓ " << done << " bytes";
270 });
271 connect(&m_ftp, &QFtpCompat::listInfo, this,
272 [this](const QFtpCompatDirEntry &e) {
273 m_lastListing.append(e);
274 });
275 }
276
277public slots:
278 void run()
279 {
280 qInfo() << "\n══════════════════════════════════════════";
281 qInfo() << " Explicit FTPS test (port" << TlsPort << ")";
282 qInfo() << "══════════════════════════════════════════";
283
284 step("CONNECT (Explicit FTPS – AUTH TLS)");
285 // EncryptionMode::Explicit: plain TCP → AUTH TLS → PBSZ 0 → PROT P → login
286 m_openId = m_ftp.open(Urls::conn(TlsPort),
287 QFtpCompat::EncryptionMode::Explicit);
288
289 step("CLEANUP");
290 m_cleanupIds << m_ftp.remove(Urls::file("tls_test.txt", TlsPort));
291
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"));
295
296 step("LIST / (verify file visible)");
297 m_listId = m_ftp.list(Urls::dir("", TlsPort));
298
299 step("DOWNLOAD tls_test.txt → QByteArray");
300 m_downloadId = m_ftp.get(Urls::file("tls_test.txt", TlsPort),
301 &m_downloadedBytes);
302
303 step("DELETE tls_test.txt");
304 m_removeId = m_ftp.remove(Urls::file("tls_test.txt", TlsPort));
305
306 step("LIST / final (expect empty)");
307 m_listFinalId = m_ftp.list(Urls::dir("", TlsPort));
308 }
309
310private slots:
311 void onCommandFinished(int id, bool hasError)
312 {
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);
319
320 const bool errExpected = m_cleanupIds.contains(id);
321
322 if (hasError && !errExpected) qCritical() << " [FAIL]" << label << m_ftp.errorString();
323 else if (m_cleanupIds.contains(id)) {}
324 else qInfo() << " [OK ]" << label;
325
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();
331 }
332 if (id == m_downloadId) {
333 qInfo() << " → content match:"
334 << (m_downloadedBytes == "TLS upload works!\n" ? "✓" : "✗");
335 }
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");
342 });
343 qInfo() << " → remaining (excl. permanent files):" << unexpected
344 << (unexpected == 0 ? "(clean ✓)" : "(unexpected files ✗)");
345 qInfo() << "\n══ Explicit FTPS complete ══\n";
346 QCoreApplication::quit();
347 }
348 }
349
350 static void step(const QString &l) { qInfo() << "\n──" << l; }
351
352private:
353 QFtpCompat m_ftp;
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};
359};
360
361// =============================================================================
362int main(int argc, char *argv[])
363{
364 QCoreApplication app(argc, argv);
365
366 // Run plain test first, then TLS test when plain is done
367 auto *plain = new FtpTest(&app);
368 auto *tls = new FtpTlsTest(&app);
369
370 QObject::connect(plain, &FtpTest::plainDone,
371 tls, &FtpTlsTest::run);
372
373 QTimer::singleShot(0, plain, &FtpTest::run);
374
375 return app.exec();
376}
377
378#include "main.moc"
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 ignoreSslErrors()
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.
Definition qftpcompat.h:81
void dataTransferProgress(qint64 bytesTransferred, qint64 bytesTotal)
Emitted periodically during data transfers.