// Ryzom - MMORPG Framework
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#include "stdpch.h"
#include "downloader.h"
#include "nel/misc/system_info.h"
#include "nel/misc/path.h"
#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif
CDownloader::CDownloader(QObject *parent):QObject(parent), m_manager(NULL), m_reply(NULL), m_timer(NULL),
m_offset(0), m_size(0), m_supportsAcceptRanges(false), m_supportsContentRange(false),
m_downloadAfterHead(false), m_aborted(false), m_file(NULL)
{
m_manager = new QNetworkAccessManager(this);
m_timer = new QTimer(this);
connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
}
CDownloader::~CDownloader()
{
stopTimer();
closeFile();
}
bool CDownloader::getHtmlPageContent(const QString &url)
{
if (url.isEmpty()) return false;
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::UserAgentHeader, "Ryzom Installer/1.0");
QNetworkReply *reply = m_manager->get(request);
connect(reply, SIGNAL(finished()), SLOT(onHtmlPageFinished()));
return true;
}
bool CDownloader::prepareFile(const QString &url, const QString &fullPath)
{
if (url.isEmpty()) return false;
m_downloadAfterHead = false;
emit downloadPrepare();
m_fullPath = fullPath;
m_url = url;
getFileHead();
return true;
}
bool CDownloader::getFile()
{
if (m_fullPath.isEmpty() || m_url.isEmpty())
{
qDebug() << "You forget to call prepareFile before";
return false;
}
m_downloadAfterHead = true;
getFileHead();
return true;
}
bool CDownloader::stop()
{
if (!m_reply) return false;
m_reply->abort();
return true;
}
void CDownloader::startTimer()
{
stopTimer();
m_timer->setInterval(5000);
m_timer->setSingleShot(true);
m_timer->start();
}
void CDownloader::stopTimer()
{
if (m_timer->isActive()) m_timer->stop();
}
bool CDownloader::openFile()
{
closeFile();
m_file = new QFile(m_fullPath);
if (m_file->open(QFile::Append)) return true;
closeFile();
return false;
}
void CDownloader::closeFile()
{
if (m_file)
{
m_file->close();
delete m_file;
m_file = NULL;
}
}
void CDownloader::getFileHead()
{
if (m_supportsAcceptRanges)
{
QFileInfo fileInfo(m_fullPath);
if (fileInfo.exists())
{
m_offset = fileInfo.size();
}
else
{
m_offset = 0;
}
// continue if offset less than size
if (m_offset >= m_size)
{
if (checkDownloadedFile())
{
// file is already downloaded
emit downloadSuccess(m_size);
}
else
{
// or has wrong size
emit downloadFail(tr("File (%1B) is larger than expected (%2B)").arg(m_offset).arg(m_size));
}
return;
}
}
QNetworkRequest request(m_url);
request.setHeader(QNetworkRequest::UserAgentHeader, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0");
if (m_supportsAcceptRanges)
{
request.setRawHeader("Range", QString("bytes=%1-").arg(m_offset).toLatin1());
}
m_reply = m_manager->head(request);
connect(m_reply, SIGNAL(finished()), SLOT(onHeadFinished()));
connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onError(QNetworkReply::NetworkError)));
startTimer();
}
void CDownloader::downloadFile()
{
qint64 freeSpace = NLMISC::CSystemInfo::availableHDSpace(m_fullPath.toUtf8().constData());
if (freeSpace < m_size - m_offset)
{
// we have not enough free disk space to continue download
emit downloadFail(tr("You only have %1 bytes left on device, but %2 bytes are required.").arg(freeSpace).arg(m_size - m_offset));
return;
}
if (!openFile())
{
emit downloadFail(tr("Unable to write file"));
return;
}
QNetworkRequest request(m_url);
request.setHeader(QNetworkRequest::UserAgentHeader, "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17");
if (supportsResume())
{
request.setRawHeader("Range", QString("bytes=%1-%2").arg(m_offset).arg(m_size-1).toLatin1());
}
m_reply = m_manager->get(request);
connect(m_reply, SIGNAL(finished()), SLOT(onDownloadFinished()));
connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onError(QNetworkReply::NetworkError)));
connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), SLOT(onDownloadProgress(qint64, qint64)));
connect(m_reply, SIGNAL(readyRead()), SLOT(onDownloadRead()));
emit downloadStart();
startTimer();
}
bool CDownloader::checkDownloadedFile()
{
QFileInfo file(m_fullPath);
return file.size() == m_size && file.lastModified().toUTC() == m_lastModified;
}
void CDownloader::onTimeout()
{
qDebug() << "Timeout";
emit downloadFail(tr("Timeout"));
}
void CDownloader::onHtmlPageFinished()
{
QNetworkReply *reply = qobject_cast(sender());
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString html = QString::fromUtf8(reply->readAll());
reply->deleteLater();
emit htmlPageContent(html);
}
void CDownloader::onHeadFinished()
{
stopTimer();
int status = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString redirection = m_reply->header(QNetworkRequest::LocationHeader).toString();
m_size = m_reply->header(QNetworkRequest::ContentLengthHeader).toInt();
m_lastModified = m_reply->header(QNetworkRequest::LastModifiedHeader).toDateTime().toUTC();
QString acceptRanges = QString::fromLatin1(m_reply->rawHeader("Accept-Ranges"));
QString contentRange = QString::fromLatin1(m_reply->rawHeader("Content-Range"));
m_reply->deleteLater();
m_reply = NULL;
// redirection
if (status == 302)
{
if (redirection.isEmpty())
{
emit downloadFail(tr("Redirection URL is not defined"));
return;
}
// redirection on another server, recheck resume
m_supportsAcceptRanges = false;
m_supportsContentRange = false;
m_referer = m_url;
// update real URL
m_url = redirection;
getFileHead();
return;
}
// we requested without range
else if (status == 200)
{
// update size
emit downloadInit(0, m_size);
if (!m_supportsAcceptRanges && acceptRanges == "bytes")
{
// server supports resume, part 1
m_supportsAcceptRanges = true;
// request range
getFileHead();
return;
}
// server doesn't support resume or
// we requested range, but server always returns 200
// download from the beginning
}
// we requested with a range
else if (status == 206)
{
// server supports resume
QRegExp regexp("^bytes ([0-9]+)-([0-9]+)/([0-9]+)$");
if (m_supportsAcceptRanges && regexp.exactMatch(contentRange))
{
m_supportsContentRange = true;
m_offset = regexp.cap(1).toLongLong();
// when resuming, Content-Length is the size of missing parts to download
m_size = regexp.cap(3).toLongLong();
// update offset and size
emit downloadInit(m_offset, m_size);
}
else
{
qDebug() << "Unable to parse";
}
}
// other status
else
{
emit downloadFail(tr("Wrong status code: %1").arg(status));
return;
}
if (m_downloadAfterHead)
{
if (checkDownloadedFile())
{
qDebug() << "same date and size";
}
else
{
downloadFile();
}
}
}
void CDownloader::onDownloadFinished()
{
m_reply->deleteLater();
m_reply = NULL;
closeFile();
if (m_aborted)
{
m_aborted = false;
emit downloadStop();
}
else
{
bool ok = NLMISC::CFile::setFileModificationDate(m_fullPath.toUtf8().constData(), m_lastModified.toTime_t());
emit downloadSuccess(m_size);
}
}
void CDownloader::onError(QNetworkReply::NetworkError error)
{
if (error == QNetworkReply::OperationCanceledError)
{
m_aborted = true;
}
else
{
emit downloadFail(tr("Network error: %1").arg(error));
}
}
void CDownloader::onDownloadProgress(qint64 current, qint64 total)
{
stopTimer();
emit downloadProgress(m_offset + current);
}
void CDownloader::onDownloadRead()
{
if (m_file) m_file->write(m_reply->readAll());
}