Qt 4.7: FTP Example

Qt Reference Documentation

# FTP Example

Files:

- network/qftp/ftpwindow.cpp
- network/qftp/ftpwindow.h
- network/qftp/main.cpp
- network/qftp/ftp.qrc
- network/qftp/qftp.pro

Images:

- network/qftp/images/cdtoparent.png
- network/qftp/images/dir.png
- network/qftp/images/file.png

The FTP example demonstrates a simple FTP client that can be used to list the available files on an FTP server and download them.

The user of the example can enter the address or hostname of an FTP server in the Ftp Server line edit, and then push the Connect button to connect to it. A list of the server's top-level directory is then presented in the File List tree view. If the selected item in the view is a file, the user can download it by pushing the Download button. An item representing a directory can be double clicked with the mouse to show the contents of that directory in the view.

The functionality required for the example is implemented in the QFtp class, which provides an easy, high-level interface to the file transfer protocol. FTP operations are requested through QFtp::Commands. The operations are asynchronous. QFtp will notify us through signals when commands are started and finished.

We have one class, `FtpWindow`, which sets up the GUI and handles the FTP functionality. We will now go through its definition and implementation - focusing on the code concerning FTP. The code for managing the GUI is explained in other examples.

## FtpWindow Class Definition

The `FtpWindow` class displays a window, in which the user can connect to and browse the contents of an FTP server. The slots of `FtpWindow` are connected to its widgets, and contain the functionality for managing the FTP connection. We also connect to signals in QFtp, which tells us when the commands we request are finished, the progress of current commands, and information about files on the server.

     private slots:
         void connectOrDisconnect();
         void downloadFile();
         void cancelDownload();

         void ftpCommandFinished(int commandId, bool error);
         void addToList(const QUrlInfo &urlInfo);
         void processItem(QTreeWidgetItem *item, int column);
         void cdToParent();
         void updateDataTransferProgress(qint64 readBytes,
                                         qint64 totalBytes);
         void enableDownloadButton();
         void enableConnectButton();

We will look at each slot when we examine the `FtpWindow` implementation in the next section. We also make use of a few private variables:

         QHash<QString, bool> isDirectory;
         QString currentPath;
         QFtp *ftp;
         QFile *file;

         QNetworkSession *networkSession;

The `isDirectory` hash keeps a history of all entries explored on the FTP server, and registers whether an entry represents a directory or a file. We use the QFile object to download files from the FTP server.

## FtpWindow Class Implementation

We skip the `FtpWindow` constructor as it only contains code for setting up the GUI, which is explained in other examples.

We move on to the slots, starting with `connectOrDisconnect()`.

     void FtpWindow::connectOrDisconnect()
     {
         if (ftp) {
             ftp->abort();
             ftp->deleteLater();
             ftp = 0;

If `ftp` is already pointing to a QFtp object, we QFtp::Close its FTP connection and delete the object it points to. Note that we do not delete the object using standard C++ `delete` as we need it to finish its abort operation.

         ...
         ftp = new QFtp(this);
         connect(ftp, SIGNAL(commandFinished(int,bool)),
                 this, SLOT(ftpCommandFinished(int,bool)));
         connect(ftp, SIGNAL(listInfo(QUrlInfo)),
                 this, SLOT(addToList(QUrlInfo)));
         connect(ftp, SIGNAL(dataTransferProgress(qint64,qint64)),
                 this, SLOT(updateDataTransferProgress(qint64,qint64)));

         fileList->clear();
         currentPath.clear();
         isDirectory.clear();

If we get here, `connectOrDisconnect()` was called to establish a new FTP connection. We create a new QFtp for our new connection, and connect its signals to slots in `FtpWindow`. The listInfo() signal is emitted whenever information about a single file on the sever has been resolved. This signal is sent when we ask QFtp to list() the contents of a directory. Finally, the dataTransferProgress() signal is emitted repeatedly during an FTP file transfer, giving us progress reports.

         QUrl url(ftpServerLineEdit->text());
         if (!url.isValid() || url.scheme().toLower() != QLatin1String("ftp")) {
             ftp->connectToHost(ftpServerLineEdit->text(), 21);
             ftp->login();
         } else {
             ftp->connectToHost(url.host(), url.port(21));

             if (!url.userName().isEmpty())
                 ftp->login(QUrl::fromPercentEncoding(url.userName().toLatin1()), url.password());
             else
                 ftp->login();
             if (!url.path().isEmpty())
                 ftp->cd(url.path());
         }

The Ftp Server line edit contains the IP address or hostname of the server to which we want to connect. We first check that the URL is a valid FTP sever address. If it isn't, we still try to connect using the plain text in `ftpServerLineEdit`. In either case, we assume that port `21` is used.

If the URL does not contain a user name and password, we use QFtp::login(), which will attempt to log into the FTP sever as an anonymous user. The QFtp object will now notify us when it has connected to the FTP server; it will also send a signal if it fails to connect or the username and password were rejected.

We move on to the `downloadFile()` slot:

     void FtpWindow::downloadFile()
     {
         QString fileName = fileList->currentItem()->text(0);
         ...
         file = new QFile(fileName);
         if (!file->open(QIODevice::WriteOnly)) {
             QMessageBox::information(this, tr("FTP"),
                                      tr("Unable to save the file %1: %2.")
                                      .arg(fileName).arg(file->errorString()));
             delete file;
             return;
         }

         ftp->get(fileList->currentItem()->text(0), file);

         progressDialog->setLabelText(tr("Downloading %1...").arg(fileName));
         downloadButton->setEnabled(false);
         progressDialog->exec();
     }

We first fetch the name of the file, which we find in the selected item of `fileList`. We then start the download by using QFtp::get(). QFtp will send progress signals during the download and a signal when the download is completed.

     void FtpWindow::cancelDownload()
     {
         ftp->abort();

         if (file->exists()) {
             file->close();
             file->remove();
         }
         delete file;
     }

QFtp supports canceling the download of files. We make sure that any file that is currently being written to is closed and removed, and tidy up by deleting the file object.

     void FtpWindow::ftpCommandFinished(int, bool error)
     {
     #ifndef QT_NO_CURSOR
         setCursor(Qt::ArrowCursor);
     #endif

         if (ftp->currentCommand() == QFtp::ConnectToHost) {
             if (error) {
                 QMessageBox::information(this, tr("FTP"),
                                          tr("Unable to connect to the FTP server "
                                             "at %1. Please check that the host "
                                             "name is correct.")
                                          .arg(ftpServerLineEdit->text()));
                 connectOrDisconnect();
                 return;
             }
             statusLabel->setText(tr("Logged onto %1.")
                                  .arg(ftpServerLineEdit->text()));
             fileList->setFocus();
             downloadButton->setDefault(true);
             connectButton->setEnabled(true);
             return;
         }

The `ftpCommandFinished()` slot is called when QFtp has finished a QFtp::Command. If an error occurred during the command, QFtp will set `error` to one of the values in the QFtp::Error enum; otherwise, `error` is zero.

         if (ftp->currentCommand() == QFtp::Login)
             ftp->list();

After login, the QFtp::list() function will list the top-level directory on the server. addToList() is connected to QFtp::listInfo(), and will be invoked for each entry in that directory.

         if (ftp->currentCommand() == QFtp::Get) {
             if (error) {
                 statusLabel->setText(tr("Canceled download of %1.")
                                      .arg(file->fileName()));
                 file->close();
                 file->remove();
             } else {
                 statusLabel->setText(tr("Downloaded %1 to current directory.")
                                      .arg(file->fileName()));
                 file->close();
             }
             delete file;
             enableDownloadButton();
             progressDialog->hide();

When a Get command is finished, a file has finished downloading (or an error occurred during the download).

         } else if (ftp->currentCommand() == QFtp::List) {
             if (isDirectory.isEmpty()) {
                 fileList->addTopLevelItem(new QTreeWidgetItem(QStringList() << tr("<empty>")));
                 fileList->setEnabled(false);
             }
         }

After a List command is performed, we have to check if no entries were found (in which case our `addToList()` function would not have been called).

Let's continue with the `addToList()` slot:

     void FtpWindow::addToList(const QUrlInfo &urlInfo)
     {
         QTreeWidgetItem *item = new QTreeWidgetItem;
         item->setText(0, urlInfo.name());
         item->setText(1, QString::number(urlInfo.size()));
         item->setText(2, urlInfo.owner());
         item->setText(3, urlInfo.group());
         item->setText(4, urlInfo.lastModified().toString("MMM dd yyyy"));

         QPixmap pixmap(urlInfo.isDir() ? ":/images/dir.png" : ":/images/file.png");
         item->setIcon(0, pixmap);

         isDirectory[urlInfo.name()] = urlInfo.isDir();
         fileList->addTopLevelItem(item);
         if (!fileList->currentItem()) {
             fileList->setCurrentItem(fileList->topLevelItem(0));
             fileList->setEnabled(true);
         }
     }

When a new file has been resolved during a QFtp::List command, this slot is invoked with a QUrlInfo describing the file. We create a separate row for the file in `fileList`. If `fileList` does not have a current item, we set the new item to be the current item.

     void FtpWindow::processItem(QTreeWidgetItem *item, int /*column*/)
     {
         QString name = item->text(0);
         if (isDirectory.value(name)) {
             fileList->clear();
             isDirectory.clear();
             currentPath += '/';
             currentPath += name;
             ftp->cd(name);
             ftp->list();
             cdToParentButton->setEnabled(true);
     #ifndef QT_NO_CURSOR
             setCursor(Qt::WaitCursor);
     #endif
             return;
         }
     }

The `processItem()` slot is called when an item is double clicked in the File List. If the item represents a directory, we want to load the contents of that directory with QFtp::list().

     void FtpWindow::cdToParent()
     {
     #ifndef QT_NO_CURSOR
         setCursor(Qt::WaitCursor);
     #endif
         fileList->clear();
         isDirectory.clear();
         currentPath = currentPath.left(currentPath.lastIndexOf('/'));
         if (currentPath.isEmpty()) {
             cdToParentButton->setEnabled(false);
             ftp->cd("/");
         } else {
             ftp->cd(currentPath);
         }
         ftp->list();
     }

`cdToParent()` is invoked when the user requests to go to the parent directory of the one displayed in the file list. After changing the directory, we QFtp::List its contents.

     void FtpWindow::updateDataTransferProgress(qint64 readBytes,
                                                qint64 totalBytes)
     {
         progressDialog->setMaximum(totalBytes);
         progressDialog->setValue(readBytes);
     }

The `updateDataTransferProgress()` slot is called regularly by QFtp::dataTransferProgress() when a file download is in progress. We use a QProgressDialog to show the download progression to the user.

     void FtpWindow::enableDownloadButton()
     {
         QTreeWidgetItem *current = fileList->currentItem();
         if (current) {
             QString currentFile = current->text(0);
             downloadButton->setEnabled(!isDirectory.value(currentFile));
         } else {
             downloadButton->setEnabled(false);
         }
     }

The `enableDownloadButton()` is called whenever the current item in `fileList` changes. If the item represents a file, the Enable Download Button should be enabled; otherwise, it is disabled.