4#include "private/qftpcompatcommand_p.h"
12using Cmd = QFtpCompatInternal::Command;
14static constexpr quint16 DefaultFtpPort = 21;
15static constexpr int InactivityMs = 30000;
16static const QString AnonymousUser = QStringLiteral(
"anonymous");
17static const QString AnonymousPass = QStringLiteral(
"anonymous@");
23QFtpCompat::QFtpCompat(QObject *parent)
25 , m_control(new QSslSocket(this))
26 , m_data(new QSslSocket(this))
28 qDebug() << Q_FUNC_INFO <<
"created";
30 connect(m_control, &QAbstractSocket::connected,
31 this, &QFtpCompat::onControlConnected);
32 connect(m_control, &QAbstractSocket::readyRead,
33 this, &QFtpCompat::onControlReadyRead);
34 connect(m_control, &QAbstractSocket::errorOccurred,
35 this, &QFtpCompat::onControlError);
38 connect(m_control, &QSslSocket::encrypted,
this, [
this]() {
39 qDebug() << Q_FUNC_INFO <<
"control channel encrypted";
40 if (m_ctrlState == ControlState::WaitingForControlHandshake) {
41 if (m_encMode == EncryptionMode::Implicit) {
43 m_ctrlState = ControlState::WaitingForWelcome;
46 sendCommand(
"PBSZ 0");
47 m_ctrlState = ControlState::WaitingForPbszAck;
51 connect(m_control, &QSslSocket::sslErrors,
this,
52 [
this](
const QList<QSslError> &errs) {
53 qDebug() << Q_FUNC_INFO <<
"control SSL errors:" << errs;
58 connect(m_data, &QAbstractSocket::connected,
this, &QFtpCompat::onDataConnected);
59 connect(m_data, &QAbstractSocket::readyRead,
this, &QFtpCompat::onDataReadyRead);
60 connect(m_data, &QAbstractSocket::disconnected,
this, &QFtpCompat::onDataDisconnected);
61 connect(m_data, &QAbstractSocket::errorOccurred,
this, &QFtpCompat::onDataError);
64 connect(m_data, &QSslSocket::encrypted,
this, [
this]() {
65 qDebug() << Q_FUNC_INFO <<
"data channel encrypted";
66 if (m_ctrlState == ControlState::WaitingForDataHandshake) {
67 m_ctrlState = ControlState::WaitingForTransferStartAck;
68 sendDataTransferCommand();
71 connect(m_data, &QSslSocket::sslErrors,
this,
72 [
this](
const QList<QSslError> &errs) {
73 qDebug() << Q_FUNC_INFO <<
"data SSL errors:" << errs;
78QFtpCompat::~QFtpCompat()
80 qDebug() << Q_FUNC_INFO <<
"destroyed";
91 qDebug() << Q_FUNC_INFO << url.toString() <<
"encryption:" <<
static_cast<int>(mode);
93 cmd->type = Cmd::Type::Open;
101 m_sslConfig = config;
102 m_control->setSslConfiguration(config);
103 m_data->setSslConfiguration(config);
113 m_control->ignoreSslErrors();
114 m_data->ignoreSslErrors();
119 qDebug() << Q_FUNC_INFO;
120 clearPendingCommands();
123 cmd->type = Cmd::Type::Close;
133 qDebug() << Q_FUNC_INFO <<
"aborting command" << m_current->id;
135 m_abortPending =
true;
137 if (isDataTransferCommand()) {
143 m_ctrlState = ControlState::WaitingForAbortAck;
148 QStringLiteral(
"Operation aborted by the user (command id %1)")
149 .arg(m_current->id));
150 finalizeCommand(
true);
160 qDebug() << Q_FUNC_INFO << url.path();
162 cmd->type = Cmd::Type::Get;
165 cmd->transferType = type;
171 qDebug() << Q_FUNC_INFO << url.path() <<
"(→ QByteArray)";
173 auto *buf =
new QBuffer;
174 buf->open(QIODevice::WriteOnly);
177 cmd->type = Cmd::Type::Get;
180 cmd->byteArrayDst = dst;
181 cmd->ownsDevice =
true;
182 cmd->transferType = type;
188 qDebug() << Q_FUNC_INFO << url.path() <<
"size:" << data.size();
189 auto *buf =
new QBuffer;
191 buf->open(QIODevice::ReadOnly);
194 cmd->type = Cmd::Type::Put;
197 cmd->ownsDevice =
true;
198 cmd->transferType = type;
204 qDebug() << Q_FUNC_INFO << url.path()
205 <<
"size:" << (src ? src->size() : -1);
207 cmd->type = Cmd::Type::Put;
210 cmd->transferType = type;
220 qDebug() << Q_FUNC_INFO << url.path();
222 cmd->type = Cmd::Type::List;
229 qDebug() << Q_FUNC_INFO << url.path();
231 cmd->type = Cmd::Type::Mkdir;
238 qDebug() << Q_FUNC_INFO << url.path();
240 cmd->type = Cmd::Type::Rmdir;
247 qDebug() << Q_FUNC_INFO << url.path();
249 cmd->type = Cmd::Type::Remove;
256 qDebug() << Q_FUNC_INFO << from.path() <<
"->" << to.path();
258 cmd->type = Cmd::Type::Rename;
260 cmd->renameDst = to.path();
266 qDebug() << Q_FUNC_INFO << url.path();
268 cmd->type = Cmd::Type::Cd;
276 cmd->type = Cmd::Type::Pwd;
277 cmd->rawCmd = QStringLiteral(
"PWD");
283 qDebug() << Q_FUNC_INFO << command;
285 cmd->type = Cmd::Type::Raw;
286 cmd->rawCmd = command;
302 qDebug() << Q_FUNC_INFO;
303 clearPendingCommands();
312 m_ctrlState = ControlState::Idle;
313 m_abortPending =
false;
318 m_encMode = EncryptionMode::None;
321void QFtpCompat::clearPendingCommands()
323 qDebug() << Q_FUNC_INFO <<
"clearing" << m_queue.size() <<
"pending commands";
332int QFtpCompat::enqueue(Cmd *cmd)
334 cmd->id = m_nextId++;
335 qDebug() << Q_FUNC_INFO <<
"id:" << cmd->id
336 <<
"type:" <<
static_cast<int>(cmd->type)
337 <<
"queue depth:" << m_queue.size() + 1;
338 m_queue.enqueue(cmd);
342 QMetaObject::invokeMethod(
this, &QFtpCompat::startNextCommand,
343 Qt::QueuedConnection);
348void QFtpCompat::startNextCommand()
350 qDebug() << Q_FUNC_INFO <<
"queue:" << m_queue.size();
351 if (m_current || m_queue.isEmpty()) {
355 m_current = m_queue.dequeue();
358 m_dataChannelClosed =
false;
359 m_ctrlTransferComplete =
false;
360 m_abortPending =
false;
361 m_dataReadBuf.clear();
364 startCurrentCommand();
367void QFtpCompat::startCurrentCommand()
373 if (m_current->type != Cmd::Type::Open
374 && m_current->type != Cmd::Type::Close
376 qWarning() << Q_FUNC_INFO
377 <<
"not logged in – failing command"
378 <<
static_cast<int>(m_current->type);
380 QStringLiteral(
"command %1 requires login")
381 .arg(
static_cast<int>(m_current->type)));
382 finalizeCommand(
true);
386 switch (m_current->type) {
388 case Cmd::Type::Open: {
390 finalizeCommand(
false);
394 const QUrl &url = m_current->url;
395 m_encMode = m_current->encMode;
399 constexpr quint16 ImplicitFtpsPort = 990;
400 m_port = (url.port() > 0)
401 ?
static_cast<quint16
>(url.port())
402 : (m_encMode == EncryptionMode::Implicit ? ImplicitFtpsPort
405 if (m_host.trimmed().isEmpty()) {
407 QStringLiteral(
"open(): URL contains no host name ('%1')")
408 .arg(url.toString()));
409 finalizeCommand(
true);
414 m_control->setSslConfiguration(m_sslConfig);
415 m_data->setSslConfiguration(m_sslConfig);
417 qDebug() << Q_FUNC_INFO
419 << m_host <<
":" << m_port
420 <<
"encryption:" <<
static_cast<int>(m_encMode);
424 if (m_encMode == EncryptionMode::Implicit) {
427 m_ctrlState = ControlState::WaitingForControlHandshake;
428 m_control->connectToHostEncrypted(m_host, m_port);
431 m_ctrlState = ControlState::WaitingForWelcome;
432 m_control->connectToHost(m_host, m_port);
437 QStringLiteral(
"open(): called in unexpected state %1")
438 .arg(
static_cast<int>(m_state)));
439 finalizeCommand(
true);
443 case Cmd::Type::List:
448 QStringLiteral(
"FTP %1: cannot execute – not logged in")
449 .arg(m_current->type == Cmd::Type::List ? QLatin1String(
"LIST")
450 : m_current->type == Cmd::Type::Get ? QLatin1String(
"RETR")
451 : QLatin1String(
"STOR")));
452 finalizeCommand(
true);
458 case Cmd::Type::Remove:
459 sendCommand(
"DELE " + m_current->url.path().toUtf8());
460 m_ctrlState = ControlState::WaitingForSimpleAck;
463 case Cmd::Type::Mkdir:
464 sendCommand(
"MKD " + m_current->url.path().toUtf8());
465 m_ctrlState = ControlState::WaitingForSimpleAck;
468 case Cmd::Type::Rmdir:
469 sendCommand(
"RMD " + m_current->url.path().toUtf8());
470 m_ctrlState = ControlState::WaitingForSimpleAck;
473 case Cmd::Type::Rename:
474 sendCommand(
"RNFR " + m_current->url.path().toUtf8());
475 m_ctrlState = ControlState::WaitingForRnfrAck;
479 sendCommand(
"CWD " + m_current->url.path().toUtf8());
480 m_ctrlState = ControlState::WaitingForSimpleAck;
485 sendCommand(m_current->rawCmd.toUtf8());
486 m_ctrlState = ControlState::WaitingForSimpleAck;
489 case Cmd::Type::Close:
492 m_ctrlState = ControlState::WaitingForQuitAck;
497void QFtpCompat::finalizeCommand(
bool hadError)
503 const int id = m_current->id;
504 qDebug() << Q_FUNC_INFO <<
"id:" <<
id <<
"error:" << hadError;
507 if (m_current->ownsDevice && m_current->device) {
509 if (m_current->byteArrayDst) {
510 auto *buf = qobject_cast<QBuffer *>(m_current->device);
512 *m_current->byteArrayDst = buf->data();
515 delete m_current->device;
516 m_current->device =
nullptr;
524 if (m_queue.isEmpty()) {
527 QMetaObject::invokeMethod(
this, &QFtpCompat::startNextCommand,
528 Qt::QueuedConnection);
536void QFtpCompat::sendCommand(
const QByteArray &cmd)
538 if (cmd.startsWith(
"PASS")) {
539 qDebug() << Q_FUNC_INFO <<
"PASS ****";
541 qDebug() << Q_FUNC_INFO << cmd;
543 m_control->write(cmd +
"\r\n");
546void QFtpCompat::onControlConnected()
548 qDebug() << Q_FUNC_INFO;
553void QFtpCompat::onControlReadyRead()
558 while (m_control->canReadLine()) {
559 const QByteArray line = m_control->readLine().trimmed();
561 if (line.size() < 3) {
566 if (line.size() >= 4 && line.at(3) ==
'-') {
567 qDebug() << Q_FUNC_INFO <<
"[multi-line]" << line;
572 const int code = line.left(3).toInt(&ok);
574 qDebug() << Q_FUNC_INFO <<
"Unparseable control line:" << line;
578 processResponse(code, line.size() > 4 ? line.mid(4) : QByteArray{});
582void QFtpCompat::processResponse(
int code,
const QByteArray &message)
584 qDebug() << Q_FUNC_INFO << code << message;
592 m_ctrlState = ControlState::Idle;
593 if (m_data->state() != QAbstractSocket::UnconnectedState) {
596 const QString detail = QStringLiteral(
"server returned error %1: '%2'")
598 .arg(QString::fromUtf8(message));
604 finalizeCommand(
true);
609 QStringLiteral(
"server closed connection (421): '%1'")
610 .arg(QString::fromUtf8(message)));
611 finalizeCommand(
true);
617 switch (m_ctrlState) {
622 case ControlState::WaitingForWelcome:
625 if (m_encMode == EncryptionMode::Explicit) {
627 sendCommand(
"AUTH TLS");
628 m_ctrlState = ControlState::WaitingForAuthTlsAck;
630 const QUrl &url = m_current->url;
631 const QString user = url.userName().isEmpty() ? AnonymousUser : url.userName();
632 sendCommand(
"USER " + user.toUtf8());
633 m_ctrlState = ControlState::WaitingForUserAck;
637 QStringLiteral(
"open(): expected 220 welcome banner, got %1: '%2'")
638 .arg(code).arg(QString::fromUtf8(message)));
639 finalizeCommand(
true);
643 case ControlState::WaitingForUserAck:
646 const QUrl &url = m_current->url;
647 const QString pass = url.password().isEmpty() ? AnonymousPass : url.password();
648 sendCommand(
"PASS " + pass.toUtf8());
649 m_ctrlState = ControlState::WaitingForPassAck;
650 }
else if (code == 230) {
653 finalizeCommand(
false);
656 QStringLiteral(
"open(): USER rejected (code %1): '%2' – check username")
657 .arg(code).arg(QString::fromUtf8(message)));
658 finalizeCommand(
true);
662 case ControlState::WaitingForPassAck:
665 finalizeCommand(
false);
666 }
else if (code == 332) {
669 QStringLiteral(
"open(): server requires ACCOUNT (332), which is not supported"));
670 finalizeCommand(
true);
673 QStringLiteral(
"open(): authentication failed (code %1): '%2' – check password")
674 .arg(code).arg(QString::fromUtf8(message)));
675 finalizeCommand(
true);
682 case ControlState::WaitingForTypeAck:
684 if (code == 200 || code == 250) {
688 QStringLiteral(
"TYPE command rejected (code %1): '%2'")
689 .arg(code).arg(QString::fromUtf8(message)));
690 finalizeCommand(
true);
697 case ControlState::WaitingForPasvAck: {
700 QStringLiteral(
"PASV rejected (code %1): '%2'")
701 .arg(code).arg(QString::fromUtf8(message)));
702 finalizeCommand(
true);
708 if (!parsePasvAddress(message, dataHost, dataPort)) {
710 QStringLiteral(
"PASV: could not parse server address from '%1'")
711 .arg(QString::fromUtf8(message)));
712 finalizeCommand(
true);
717 if (dataHost == QLatin1String(
"0.0.0.0") || dataHost.isEmpty()) {
718 dataHost = m_control->peerAddress().toString();
719 qDebug() << Q_FUNC_INFO
720 <<
"PASV returned invalid host, using control peer address:" << dataHost;
725 QStringLiteral(
"PASV: server returned port 0 in '%1'")
726 .arg(QString::fromUtf8(message)));
727 finalizeCommand(
true);
731 qDebug() << Q_FUNC_INFO <<
"Data channel:" << dataHost <<
":" << dataPort;
732 m_ctrlState = ControlState::ConnectingData;
733 m_data->connectToHost(dataHost, dataPort);
739 case ControlState::ConnectingData:
745 case ControlState::WaitingForTransferStartAck:
746 if (code == 125 || code == 150) {
747 m_ctrlState = ControlState::Transferring;
748 if (m_current && m_current->type == Cmd::Type::Put) {
752 if (m_current->device) {
753 if (!m_current->device->isReadable()) {
754 m_current->device->open(QIODevice::ReadOnly);
756 payload = m_current->device->readAll();
758 qDebug() << Q_FUNC_INFO <<
"UP uploading:" << payload.size() <<
"bytes";
760 m_data->write(payload);
761 m_data->disconnectFromHost();
766 QStringLiteral(
"transfer start rejected (code %1): '%2' – "
767 "check remote path '%3'")
769 .arg(QString::fromUtf8(message))
770 .arg(m_current ? m_current->url.path() : QString{}));
771 finalizeCommand(
true);
779 case ControlState::Transferring:
780 case ControlState::WaitingForTransferCompleteAck:
781 if (code == 226 || code == 250) {
782 m_ctrlTransferComplete =
true;
783 if (m_dataChannelClosed) {
784 onTransferCompleted();
786 }
else if (code >= 400) {
788 QStringLiteral(
"transfer failed mid-stream (code %1): '%2'")
789 .arg(code).arg(QString::fromUtf8(message)));
791 finalizeCommand(
true);
800 case ControlState::WaitingForAbortAck:
805 if (code == 226 || code == 225) {
807 QStringLiteral(
"operation aborted by user (ABOR acknowledged)"));
808 finalizeCommand(
true);
815 case ControlState::WaitingForSimpleAck:
816 if (code >= 200 && code < 300) {
817 if (m_current && (m_current->type == Cmd::Type::Pwd
818 || m_current->type == Cmd::Type::Raw)) {
821 finalizeCommand(
false);
824 QStringLiteral(
"command rejected (code %1): '%2' – path: '%3'")
826 .arg(QString::fromUtf8(message))
827 .arg(m_current ? m_current->url.path() : QString{}));
828 finalizeCommand(
true);
835 case ControlState::WaitingForRnfrAck:
839 sendCommand(
"RNTO " + m_current->renameDst.toUtf8());
840 m_ctrlState = ControlState::WaitingForRntoAck;
844 QStringLiteral(
"RNFR rejected (code %1): '%2' – source path: '%3'")
846 .arg(QString::fromUtf8(message))
847 .arg(m_current ? m_current->url.path() : QString{}));
848 finalizeCommand(
true);
852 case ControlState::WaitingForRntoAck:
854 finalizeCommand(
false);
857 QStringLiteral(
"RNTO rejected (code %1): '%2' – destination path: '%3'")
859 .arg(QString::fromUtf8(message))
860 .arg(m_current ? m_current->renameDst : QString{}));
861 finalizeCommand(
true);
868 case ControlState::WaitingForQuitAck:
870 if (code >= 200 && code < 300) {
871 m_control->disconnectFromHost();
873 finalizeCommand(
false);
880 case ControlState::WaitingForAuthTlsAck:
883 m_ctrlState = ControlState::WaitingForControlHandshake;
884 m_control->startClientEncryption();
887 QStringLiteral(
"AUTH TLS rejected (code %1): '%2'")
888 .arg(code).arg(QString::fromUtf8(message)));
889 finalizeCommand(
true);
893 case ControlState::WaitingForPbszAck:
895 sendCommand(
"PROT P");
896 m_ctrlState = ControlState::WaitingForProtAck;
899 QStringLiteral(
"PBSZ rejected (code %1): '%2'")
900 .arg(code).arg(QString::fromUtf8(message)));
901 finalizeCommand(
true);
905 case ControlState::WaitingForProtAck:
908 const QUrl &url = m_current->url;
909 const QString user = url.userName().isEmpty() ? AnonymousUser : url.userName();
910 sendCommand(
"USER " + user.toUtf8());
911 m_ctrlState = ControlState::WaitingForUserAck;
914 QStringLiteral(
"PROT P rejected (code %1): '%2'")
915 .arg(code).arg(QString::fromUtf8(message)));
916 finalizeCommand(
true);
920 case ControlState::WaitingForControlHandshake:
921 case ControlState::WaitingForDataHandshake:
924 qDebug() << Q_FUNC_INFO <<
"Response during TLS handshake (ignored):" << code;
927 case ControlState::Idle:
928 qDebug() << Q_FUNC_INFO <<
"Unexpected response in Idle state:" << code << message;
934void QFtpCompat::onControlError(QAbstractSocket::SocketError socketError)
936 qDebug() << Q_FUNC_INFO << socketError << m_control->errorString();
938 if (m_state ==
State::Unconnected || m_ctrlState == ControlState::WaitingForQuitAck) {
945 switch (socketError) {
946 case QAbstractSocket::HostNotFoundError:
948 detail = QStringLiteral(
"host '%1' not found – check the URL and network connectivity")
951 case QAbstractSocket::ConnectionRefusedError:
953 detail = QStringLiteral(
"connection refused by '%1:%2' – "
954 "verify the FTP server is running and the port is correct")
955 .arg(m_host).arg(m_port);
957 case QAbstractSocket::RemoteHostClosedError:
959 detail = QStringLiteral(
"server '%1' closed the control connection unexpectedly "
960 "(possible server-side timeout or crash)")
963 case QAbstractSocket::SocketTimeoutError:
965 detail = QStringLiteral(
"control connection to '%1' timed out").arg(m_host);
969 detail = QStringLiteral(
"control socket error [Qt code %1]: %2")
970 .arg(
static_cast<int>(socketError))
971 .arg(m_control->errorString());
975 setError(err, detail);
979 finalizeCommand(
true);
987void QFtpCompat::onDataConnected()
989 qDebug() << Q_FUNC_INFO;
991 if (!m_current) {
return; }
993 if (m_encMode != EncryptionMode::None) {
995 qDebug() << Q_FUNC_INFO <<
"starting data channel TLS handshake";
996 m_ctrlState = ControlState::WaitingForDataHandshake;
997 startDataChannelEncryption();
999 m_ctrlState = ControlState::WaitingForTransferStartAck;
1000 sendDataTransferCommand();
1004void QFtpCompat::startDataChannelEncryption()
1006 m_data->setSslConfiguration(m_sslConfig);
1007 m_data->startClientEncryption();
1010void QFtpCompat::sendDataTransferCommand()
1016 switch (m_current->type) {
1017 case Cmd::Type::List:
1018 sendCommand(
"LIST" + (m_current->url.path().isEmpty()
1020 :
" " + m_current->url.path().toUtf8()));
1022 case Cmd::Type::Get:
1023 sendCommand(
"RETR " + m_current->url.path().toUtf8());
1025 case Cmd::Type::Put:
1026 sendCommand(
"STOR " + m_current->url.path().toUtf8());
1033void QFtpCompat::onDataReadyRead()
1035 const QByteArray chunk = m_data->readAll();
1041 if (m_current->type == Cmd::Type::Get) {
1042 if (m_current->device) {
1043 m_current->device->write(chunk);
1045 const qint64 received = m_current->device ? m_current->device->pos() : 0;
1046 qDebug() << Q_FUNC_INFO <<
"DOWN received:" << received <<
"bytes";
1048 }
else if (m_current->type == Cmd::Type::List) {
1049 m_dataReadBuf += chunk;
1053void QFtpCompat::onDataDisconnected()
1055 qDebug() << Q_FUNC_INFO;
1059 if (m_ctrlState != ControlState::Transferring
1060 && m_ctrlState != ControlState::WaitingForTransferStartAck
1061 && m_ctrlState != ControlState::WaitingForTransferCompleteAck
1062 && m_ctrlState != ControlState::WaitingForAbortAck) {
1063 qDebug() << Q_FUNC_INFO <<
"stale – ignored in state"
1064 <<
static_cast<int>(m_ctrlState);
1069 const QByteArray tail = m_data->readAll();
1072 if (m_current->type == Cmd::Type::Get && m_current->device) {
1073 if (!tail.isEmpty()) {
1074 m_current->device->write(tail);
1076 }
else if (m_current->type == Cmd::Type::List) {
1077 m_dataReadBuf += tail;
1081 m_dataChannelClosed =
true;
1083 if (m_ctrlTransferComplete) {
1084 onTransferCompleted();
1086 m_ctrlState = ControlState::WaitingForTransferCompleteAck;
1090void QFtpCompat::onDataError(QAbstractSocket::SocketError socketError)
1092 qDebug() << Q_FUNC_INFO << socketError;
1095 if (socketError == QAbstractSocket::RemoteHostClosedError) {
1100 if (m_ctrlState != ControlState::Transferring
1101 && m_ctrlState != ControlState::WaitingForTransferStartAck
1102 && m_ctrlState != ControlState::WaitingForTransferCompleteAck
1103 && m_ctrlState != ControlState::ConnectingData
1104 && m_ctrlState != ControlState::WaitingForAbortAck) {
1105 qDebug() << Q_FUNC_INFO <<
"stale – ignored in state"
1106 <<
static_cast<int>(m_ctrlState);
1111 QStringLiteral(
"data channel error [Qt code %1]: %2 – "
1112 "transfer of '%3' interrupted")
1113 .arg(
static_cast<int>(socketError))
1114 .arg(m_data->errorString())
1115 .arg(m_current ? m_current->url.path() : QString{}));
1116 finalizeCommand(
true);
1123void QFtpCompat::onTransferCompleted()
1125 qDebug() << Q_FUNC_INFO;
1130 if (m_current->type == Cmd::Type::Put && m_uploadTotal > 0) {
1131 qDebug() << Q_FUNC_INFO <<
"↑ upload complete:" << m_uploadTotal <<
"bytes";
1135 if (m_current->type == Cmd::Type::List) {
1138 qDebug() << Q_FUNC_INFO <<
"LIST: parsed" << entries.size() <<
"entries";
1139 for (
const QFtpCompatDirEntry &e : entries) {
1144 finalizeCommand(
false);
1151void QFtpCompat::beginDataTransfer()
1153 qDebug() << Q_FUNC_INFO;
1157void QFtpCompat::sendTypeCommand()
1159 qDebug() << Q_FUNC_INFO;
1160 const bool isBinary = !m_current
1164 sendCommand(
"TYPE I");
1169 sendCommand(
"TYPE A");
1171 m_ctrlState = ControlState::WaitingForTypeAck;
1174void QFtpCompat::sendPasv()
1176 qDebug() << Q_FUNC_INFO;
1177 sendCommand(
"PASV");
1178 m_ctrlState = ControlState::WaitingForPasvAck;
1186bool QFtpCompat::parsePasvAddress(
const QByteArray &msg,
1187 QString &host, quint16 &port)
const
1189 const int open = msg.indexOf(
'(');
1190 const int close = msg.indexOf(
')');
1196 const QByteArray inner = msg.mid(
open + 1,
close -
open - 1);
1197 const QList<QByteArray> parts = inner.split(
',');
1199 if (parts.size() != 6) {
1204 const int h1 = parts[0].trimmed().toInt(&ok);
if (!ok) {
return false; }
1205 const int h2 = parts[1].trimmed().toInt(&ok);
if (!ok) {
return false; }
1206 const int h3 = parts[2].trimmed().toInt(&ok);
if (!ok) {
return false; }
1207 const int h4 = parts[3].trimmed().toInt(&ok);
if (!ok) {
return false; }
1208 const int p1 = parts[4].trimmed().toInt(&ok);
if (!ok) {
return false; }
1209 const int p2 = parts[5].trimmed().toInt(&ok);
if (!ok) {
return false; }
1211 host = QStringLiteral(
"%1.%2.%3.%4").arg(h1).arg(h2).arg(h3).arg(h4);
1212 port =
static_cast<quint16
>(p1 * 256 + p2);
1213 qDebug() << Q_FUNC_INFO <<
"data channel:" << host <<
":" << port;
1221void QFtpCompat::setState(State s)
1226 qDebug() << Q_FUNC_INFO
1227 <<
"state:" <<
static_cast<int>(m_state)
1228 <<
"->" <<
static_cast<int>(s);
1233void QFtpCompat::setError(Error err,
const QString &detail)
1236 m_errorString = detail;
1237 qDebug() << Q_FUNC_INFO << detail;
1240bool QFtpCompat::isDataTransferCommand() const noexcept
1245 switch (m_current->type) {
1246 case Cmd::Type::List:
1247 case Cmd::Type::Get:
1248 case Cmd::Type::Put:
static QVector< QFtpCompatDirEntry > parseList(const QByteArray &rawListing)
Parses a full LIST / MLSD response body into directory entries.
int currentId() const noexcept
ID of the command currently being executed, or -1 if idle.
int remove(const QUrl &url)
Deletes a file on the server (DELE).
void abort()
Aborts the current data transfer and clears the command queue.
State state() const noexcept
Current connection state.
void done(bool error)
Emitted when all queued commands have completed.
int pwd()
Requests the current working directory path (PWD).
int open(const QUrl &url, EncryptionMode mode=EncryptionMode::None)
Connects to the FTP server and logs in using the URL credentials.
int rmdir(const QUrl &url)
Removes an empty directory on the server (RMD).
int put(const QUrl &url, const QByteArray &data, TransferType type=TransferType::Binary)
Uploads data to the remote path in url.
int rawCommand(const QString &command)
Sends an arbitrary FTP command on the control channel.
void stateChanged(QFtpCompat::State newState)
Emitted when the connection state changes.
int cd(const QUrl &url)
Changes the current working directory (CWD).
void rawCommandReply(int code, QString detail)
Emitted for rawCommand() responses and pwd() results.
EncryptionMode
FTP encryption mode. \value None Plain FTP – default, fully backward-compatible. \value Explicit Expl...
int mkdir(const QUrl &url)
Creates a directory on the server (MKD).
void commandStarted(int id)
Emitted when a queued command begins executing.
int list(const QUrl &url)
Requests a directory listing (LIST).
Error
Error codes set on the most recent failed command.
@ TlsHandshakeFailed
TLS handshake failed on control or data channel.
@ FileNotFound
Server returned 550 for a file operation.
@ NetworkError
Generic socket error.
@ PermissionDenied
Server returned 553 / 550 permission error.
@ LoginFailed
USER/PASS rejected by the server.
@ NotLoggedIn
Command requires authentication.
@ Aborted
Operation cancelled by abort().
@ ServerError
Unexpected server response code.
@ ConnectionRefused
Server actively refused the TCP connection.
@ TransferFailed
Data channel closed before transfer completed.
@ ParseError
Failed to parse a server response (PASV, LIST).
@ AuthTlsRejected
Server rejected the AUTH TLS command.
@ NetworkTimeout
No activity within the inactivity timeout.
@ HostNotFound
DNS resolution failed.
void listInfo(QFtpCompatDirEntry entry)
Emitted once per entry during a list() command.
void close()
Sends QUIT and closes the connection gracefully.
int rename(const QUrl &from, const QUrl &to)
Renames or moves a file on the server (RNFR + RNTO).
void setSslConfiguration(const QSslConfiguration &config)
Removes all pending (not yet started) commands from the queue.
Error error() const noexcept
Error code set by the most recent failed command.
void commandFinished(int id, bool error)
Emitted when a command completes (successfully or with error).
int get(const QUrl &url, QIODevice *dst, TransferType type=TransferType::Binary)
Downloads a remote file into dst.
State
High-level connection state.
@ Unconnected
No TCP connection established.
@ Connected
TCP connected, waiting for server greeting (220).
@ Closing
QUIT sent, waiting for server acknowledgement.
@ Connecting
TCP connect in progress.
@ LoggedIn
Authenticated; ready to accept commands.
QSslConfiguration sslConfiguration() const
Returns the current SSL configuration.
bool hasPendingCommands() const noexcept
true if commands are waiting in the queue.
QString errorString() const noexcept
Verbose, human-readable description of the last error.
TransferType
FTP data representation type (RFC 959 §3.1.1).
@ Binary
TYPE I – raw binary (8-bit) transfer. Fully implemented.
void dataTransferProgress(qint64 bytesTransferred, qint64 bytesTotal)
Emitted periodically during data transfers.
Qt6-compatible async FTP client.