Using QFtp
The QFtp class implements the client side of the FTP protocol in Qt. It provides various functions to perform the most common FTP operations, including get(), put(), remove(), and mkdir(), and provides a means of executing arbitrary FTP commands.
The QFtp class works asynchronously. When we call a function like get() or put(), it returns immediately and the data transfer occurs when control passes back to Qt's event loop. This ensures that the user interface remains responsive while FTP commands are executed.
We will start with an example that shows how to retrieve a single file using get(). The example assumes that the application's MainWindow class needs to retrieve a price list from an FTP site.
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0, const char *name = 0); void getPriceList(); ... private slots: void ftpDone(bool error); private: QFtp ftp; QFile file; ... };
The class has a public function, getPriceList(), that retrieves the price list file, and a private slot, ftpDone(bool), that is called when the file transfer is completed. The class also has two private variables: The ftp variable, of type QFtp, encapsulates the connection to an FTP server; the file variable is used for writing the downloaded file to disk.
MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { ... connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); }
In the constructor, we connect the QFtp object's done(bool) signal to our ftpDone(bool) private slot. QFtp emits the done(bool) signal when it has finished processing all requests. The bool parameter indicates whether an error occurred or not.
void MainWindow::getPriceList() { file.setName("price-list.csv"); if (!file.open(IO_WriteOnly)) { QMessageBox::warning(this, tr("Sales Pro"), tr("Cannot write file %1 %2.") .arg(file.name()) .arg(file.errorString())); return; } ftp.connectToHost("ftp.trolltech.com"); ftp.login(); ftp.cd("/topsecret/csv"); ftp.get("price-list.csv", &file); ftp.close(); }
The getPriceList() function downloads the ftp://ftp.trolltech.com/topsecret/csv/price-list.csv file and saves it as price-list.csv in the current directory.
We start by opening the QFile for writing. Then we execute a sequence of five FTP commands using our QFtp object. The second argument to get() specifies the output I/O device.
The FTP commands are queued and executed in Qt's event loop. The completion of the commands is indicated by QFtp's done(bool) signal, which we connected to ftpDone(bool) in the constructor.
void MainWindow::ftpDone(bool error) { if (error) QMessageBox::warning(this, tr("Sales Pro"), tr("Error while retrieving file with " "FTP: %1.") .arg(ftp.errorString())); file.close(); }
Once the FTP commands are executed, we close the file. If an error occurred, we display it in a QMessageBox.
QFtp provides these operations: connectToHost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir(), and rename(). All of these functions schedule an FTP command and return an ID number that identifies the command. Arbitrary FTP commands can be executed using rawCommand(). For example, here's how to execute a SITE CHMOD command:
ftp.rawCommand("SITE CHMOD 755 fortune");
QFtp emits the commandStarted(int) signal when it starts executing a command, and it emits the commandFinished(int, bool) signal when the command is finished. The int parameter is the ID number that identifies a command. If we are interested in the fate of individual commands, we can store the ID numbers when we schedule the commands. Keeping track of the ID numbers allows us to provide detailed feedback to the user. For example:
void MainWindow::getPriceList() { ... connectId = ftp.connectToHost("ftp.trolltech.com"); loginId = ftp.login(); cdId = ftp.cd("/topsecret/csv"); getId = ftp.get("price-list.csv", &file); closeId = ftp.close(); } void MainWindow::commandStarted(int id) { if (id == connectId) { statusBar()->message(tr("Connecting...")); } else if (id == loginId) { statusBar()->message(tr("Logging in...")); ... }
Another way of providing feedback is to connect to QFtp's stateChanged() signal.
In most applications, we are only interested in the fate of the whole sequence of commands. We can then simply connect to the done(bool) signal, which is emitted whenever the command queue becomes empty.
When an error occurs, QFtp automatically clears the command queue. This means that if the connection or the login fails, the commands that follow in the queue are never executed. But if we schedule new commands after the error has occurred using the same QFtp object, these commands will be queued and executed as if nothing had happened.
We will now review a more advanced example:
class Downloader : public QObject { Q_OBJECT public: Downloader(const QUrl &url); signals: void finished(); private slots: void ftpDone(bool error); void listInfo(const QUrlInfo &urlInfo); private: QFtp ftp; std::vector openedFiles; };
The Downloader class downloads all the files located in an FTP directory. The directory is specified as a QUrl passed to the class's constructor. The QUrl class is a Qt class that provides a high-level interface for extracting the different parts of a URL, such as the file name, path, protocol, and port.
Downloader::Downloader(const QUrl &url) { if (url.protocol() != "ftp") { QMessageBox::warning(0, tr("Downloader"), tr("Protocol must be 'ftp'.")); emit finished(); return; } int port = 21; if (url.hasPort()) port = url.port(); connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(listInfo(const QUrlInfo &))); ftp.connectToHost(url.host(), port); ftp.login(url.user(), url.password()); ftp.cd(url.path()); ftp.list(); }
In the constructor, we first check that the URL starts with "ftp:". Then we extract a port number. If no port is specified, we use port 21, the default port for FTP.
Next, we establish two signalslot connections, and we schedule four FTP commands. The last FTP command, list(), retrieves the name of every file in the directory and emits a listInfo(const QUrlInfo &) signal for each name that it retrieves. This signal is connected to a slot also called listInfo(), which downloads the file associated with the URL it is given.
void Downloader::listInfo(const QUrlInfo &urlInfo) { if (urlInfo.isFile() && urlInfo.isReadable()) { QFile *file = new QFile(urlInfo.name()); if (!file->open(IO_WriteOnly)) { QMessageBox::warning(0, tr("Downloader"), tr("Error: Cannot open file " "%1: %2.") .arg(file->name()) .arg(file->errorString())); emit finished(); return; } ftp.get(urlInfo.name(), file); openedFiles.push_back(file); } }
The listInfo() slot's QUrlInfo parameter provides detailed information about a remote file. If the file is a normal file (not a directory) and is readable, we call get() to download it. The QFile object used for downloading is allocated using new and a pointer to it is stored in the openedFiles vector.
void Downloader::ftpDone(bool error) { if (error) QMessageBox::warning(0, tr("Downloader"), tr("Error: %1.") .arg(ftp.errorString())); for (int i = 0; i < (int)openedFiles.size(); ++i) delete openedFiles[i]; emit finished(); }
The ftpDone() slot is called when all the FTP commands have finished, or if an error occurred. We delete the QFile objects to prevent memory leaks, and also to close each file. (The QFile destructor automatically closes the file if it's open.)
If there are no errors, the sequence of FTP commands and signals is as follows:
connectToHost(host) login() cd(path) list() emit listInfo(file_1) get(file_1) emit listInfo(file_2) get(file_2) ... emit listInfo(file_N) get(file_N) emit done()
If a network error occurs while downloading the fifth of, say, twenty files to download, the remaining files will not be downloaded. If we wanted to download as many files as possible, one solution would be to schedule the GET operations one at a time and to wait for the done(bool) signal before scheduling a new GET operation. In listInfo(), we would simply append the file name to a QStringList, instead of calling get() right away, and in done(bool) we would call get() on the next file to download in the QStringList. The sequence of execution would then look like this:
connectToHost(host) login() cd(path) list() emit listInfo(file_1) emit listInfo(file_2) ... emit listInfo(file_N) emit done() get(file_1) emit done() get(file_2) emit done() ... get(file_N) emit done()
Another solution would be to use one QFtp object per file. This would enable us to download the files in parallel, through separate FTP connections.
int main(int argc, char *argv[]) { QApplication app(argc, argv); QUrl url("ftp://ftp.example.com/"); if (argc >= 2) url = argv[1]; Downloader downloader(url); QObject::connect(&downloader, SIGNAL(finished()), &app, SLOT(quit())); return app.exec(); }
The main() function completes the program. If the user specifies a URL on the command line, we use it; otherwise, we fall back on ftp://ftp.example.com/.
In both examples, the data retrieved using get() was written to a QFile. This doesn't have to be the case. If we wanted the data in memory, we could use a QBuffer, the QIODevice subclass that wraps a QByteArray. For example:
QBuffer *buffer = new QBuffer(byteArray); buffer->open(IO_WriteOnly); ftp.get(urlInfo.name(), buffer);
We could also omit the I/O device argument to get(), or pass a null pointer. The QFtp class then emits a readyRead() signal every time new data is available, and the data can be read using readBlock() or readAll().
If we want to provide the user with feedback while the data is being downloaded, we can connect QFtp's dataTransferProgress(int, int) signal to the setProgress(int, int) slot in a QProgressBar or in a QProgressDialog. We would then also connect the QProgressBar or QProgressDialog's canceled() signal to QFtp's abort() slot.