QtFileDownloader.cpp   [plain text]


/*
 * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this program; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */
#include "config.h"
#include "QtFileDownloader.h"

#include "DataReference.h"
#include "Download.h"
#include "HTTPParsers.h"
#include "MIMETypeRegistry.h"
#include <QCoreApplication>
#include <QFile>
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <WebCore/QNetworkReplyHandler.h>
#include <WebCore/ResourceError.h>
#include <WebCore/ResourceResponse.h>

using namespace WebCore;
using namespace WTF;

namespace WebKit {

QtFileDownloader::QtFileDownloader(Download* download, PassOwnPtr<QNetworkReply> reply)
    : m_download(download)
    , m_reply(reply)
    , m_error(QNetworkReply::NoError)
    , m_headersRead(false)
{
}

QtFileDownloader::~QtFileDownloader()
{
    if (!m_destinationFile)
        return;

    abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorAborted);
}

void QtFileDownloader::init()
{
    connect(m_reply.get(), SIGNAL(readyRead()), SLOT(onReadyRead()));
    connect(m_reply.get(), SIGNAL(finished()), SLOT(onFinished()));
    connect(m_reply.get(), SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onError(QNetworkReply::NetworkError)));
}

QString QtFileDownloader::determineFilename()
{
    ASSERT(!m_destinationFile);

    QString filenameCandidate = filenameFromHTTPContentDisposition(QString::fromLatin1(m_reply->rawHeader("Content-Disposition")));
    if (filenameCandidate.isEmpty()) {
        KURL kurl = m_reply->url();
        filenameCandidate = decodeURLEscapeSequences(kurl.lastPathComponent());
    }

    if (filenameCandidate.isEmpty()) {
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCannotDetermineFilename);
        return QString();
    }

    // Make sure that we remove possible "../.." parts in the given file name.
    QFileInfo filenameFilter(filenameCandidate);
    QString filename = filenameFilter.fileName();

    if (filename.isEmpty()) {
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCannotDetermineFilename);
        return QString();
    }

    return filename;
}

void QtFileDownloader::startTransfer(const QString& decidedFilePath)
{
    ASSERT(!m_destinationFile);

    // Error might have occured during destination query.
    if (m_error != QNetworkReply::NoError) {
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorNetworkFailure);
        return;
    }

    if (decidedFilePath.isEmpty()) {
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCancelled);
        return;
    }

    OwnPtr<QFile> downloadFile = adoptPtr(new QFile(decidedFilePath));

    if (!downloadFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCannotOpenFile);
        return;
    }

    // Assigning to m_destinationFile flags that either error or
    // finished shall be called in the end.
    m_destinationFile = downloadFile.release();

    m_download->didCreateDestination(m_destinationFile->fileName());

    // We might have gotten readyRead already even before this function
    // was called.
    if (m_reply->bytesAvailable())
        onReadyRead();

    // We might have gotten finished already even before this
    // function was called.
    if (m_reply->isFinished())
        onFinished();
}

void QtFileDownloader::abortDownloadWritingAndEmitError(QtFileDownloader::DownloadError errorCode)
{
    m_reply->abort();

    // On network failures it's QNetworkReplyHandler::errorForReply who will handle errors.
    if (errorCode == QtFileDownloader::DownloadErrorNetworkFailure) {
        m_download->didFail(QNetworkReplyHandler::errorForReply(m_reply.get()), CoreIPC::DataReference(0, 0));
        return;
    }

    QString translatedErrorMessage;
    switch (errorCode) {
    case QtFileDownloader::DownloadErrorAborted:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Download aborted");
        break;
    case QtFileDownloader::DownloadErrorCannotWriteToFile:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Cannot write to file");
        break;
    case QtFileDownloader::DownloadErrorCannotOpenFile:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Cannot open file for writing");
        break;
    case QtFileDownloader::DownloadErrorDestinationAlreadyExists:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Destination already exists");
        break;
    case QtFileDownloader::DownloadErrorCancelled:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Download cancelled by caller");
        break;
    case QtFileDownloader::DownloadErrorCannotDetermineFilename:
        translatedErrorMessage = QCoreApplication::translate("QtFileDownloader", "Cannot determine filename");
        break;
    default:
        ASSERT_NOT_REACHED();
    }

    ResourceError downloadError("Download", errorCode, m_reply->url().toString(), translatedErrorMessage);

    m_download->didFail(downloadError, CoreIPC::DataReference(0, 0));
}

void QtFileDownloader::handleDownloadResponse()
{
    // By API contract, QNetworkReply::metaDataChanged cannot really be trusted.
    // Thus we need to call this upon receiving first data.
    String contentType = m_reply->header(QNetworkRequest::ContentTypeHeader).toString();
    String encoding = extractCharsetFromMediaType(contentType);
    String mimeType = extractMIMETypeFromMediaType(contentType);
    String filename = determineFilename();

    // If filename is empty it means determineFilename aborted and emitted an error.
    if (filename.isEmpty())
        return;

    // Let's try to guess from the extension.
    if (mimeType.isEmpty())
        mimeType = MIMETypeRegistry::getMIMETypeForPath(m_reply->url().path());

    ResourceResponse response(m_reply->url(), mimeType, m_reply->header(QNetworkRequest::ContentLengthHeader).toLongLong(), encoding, filename);
    m_download->didReceiveResponse(response);
}

void QtFileDownloader::onReadyRead()
{
    if (m_destinationFile) {
        QByteArray content = m_reply->readAll();
        if (content.size() <= 0)
            return;

        qint64 bytesWritten = m_destinationFile->write(content);

        if (bytesWritten == -1) {
            abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCannotWriteToFile);
            return;
        }

        // There might a corner case to be fixed here if bytesWritten != content.size()
        // does not actually represent an error.
        ASSERT(bytesWritten == content.size());

        m_download->didReceiveData(bytesWritten);
    } else if (!m_headersRead) {
        handleDownloadResponse();
        m_headersRead = true;
    }
}

void QtFileDownloader::onFinished()
{
    // If it's finished and we haven't even read the headers, it means we never got to onReadyRead and that we are
    // probably dealing with the download of a local file or of a small file that was started with a handle.
    if (!m_headersRead) {
        handleDownloadResponse();
        m_headersRead = true;
        return;
    }

    if (!m_destinationFile)
        return;

    m_destinationFile.clear();

    if (m_error == QNetworkReply::NoError)
        m_download->didFinish();
    else if  (m_error == QNetworkReply::OperationCanceledError)
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorCancelled);
    else
        abortDownloadWritingAndEmitError(QtFileDownloader::DownloadErrorNetworkFailure);
}

void QtFileDownloader::onError(QNetworkReply::NetworkError code)
{
    m_error = code;
}

void QtFileDownloader::cancel()
{
    m_reply->abort();
    // QtFileDownloader::onFinished() will be called and will raise a DownloadErrorCancelled.
}

} // namespace WebKit
#include "moc_QtFileDownloader.cpp"