// 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 "operation.h"
#include "downloader.h"
#include "utils.h"
#include "nel/misc/system_info.h"
#include "nel/misc/path.h"
#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif
CDownloader::CDownloader(QObject *parent, IOperationProgressListener *listener):QObject(parent), m_listener(listener), m_manager(NULL), m_timer(NULL),
m_offset(0), m_size(0), m_supportsAcceptRanges(false), m_supportsContentRange(false),
m_downloadAfterHead(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, QString("Ryzom Installer/%1").arg(QApplication::applicationVersion()));
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;
if (m_listener) m_listener->operationPrepare();
m_fullPath = fullPath;
m_url = url;
getFileHead();
return true;
}
bool CDownloader::getFile()
{
if (m_fullPath.isEmpty() || m_url.isEmpty())
{
nlwarning("You forget to call prepareFile before");
return false;
}
m_downloadAfterHead = true;
getFileHead();
return true;
}
void CDownloader::startTimer()
{
stopTimer();
m_timer->setInterval(30000);
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
if (m_listener) m_listener->operationSuccess(m_size);
emit downloadDone();
}
else
{
// or has wrong size
if (m_listener) m_listener->operationFail(tr("File is larger (%1B) 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());
}
QNetworkReply *reply = m_manager->head(request);
connect(reply, SIGNAL(finished()), SLOT(onHeadFinished()));
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onError(QNetworkReply::NetworkError)));
startTimer();
}
void CDownloader::downloadFile()
{
qint64 freeSpace = NLMISC::CSystemInfo::availableHDSpace(m_fullPath.toUtf8().constData());
if (freeSpace == 0)
{
if (m_listener)
{
QString error = qFromUtf8(NLMISC::formatErrorMessage(NLMISC::getLastError()));
m_listener->operationFail(tr("Error '%1' occurred when trying to check free disk space on %2.").arg(error).arg(m_fullPath));
}
return;
}
if (freeSpace < m_size - m_offset)
{
// we have not enough free disk space to continue download
if (m_listener) m_listener->operationFail(tr("You only have %1 bytes left on the device, but %2 bytes are needed.").arg(freeSpace).arg(m_size - m_offset));
return;
}
if (!openFile())
{
if (m_listener) m_listener->operationFail(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());
}
QNetworkReply *reply = m_manager->get(request);
connect(reply, SIGNAL(finished()), SLOT(onDownloadFinished()));
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(onError(QNetworkReply::NetworkError)));
connect(reply, SIGNAL(downloadProgress(qint64, qint64)), SLOT(onDownloadProgress(qint64, qint64)));
connect(reply, SIGNAL(readyRead()), SLOT(onDownloadRead()));
if (m_listener) m_listener->operationStart();
startTimer();
}
bool CDownloader::checkDownloadedFile()
{
QFileInfo file(m_fullPath);
return file.size() == m_size && file.lastModified().toUTC() == m_lastModified;
}
void CDownloader::onTimeout()
{
nlwarning("Timeout");
if (m_listener) m_listener->operationFail(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();
QNetworkReply *reply = qobject_cast(sender());
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString url = reply->url().toString();
QString redirection = reply->header(QNetworkRequest::LocationHeader).toString();
m_size = reply->header(QNetworkRequest::ContentLengthHeader).toInt();
m_lastModified = reply->header(QNetworkRequest::LastModifiedHeader).toDateTime().toUTC();
QString acceptRanges = QString::fromLatin1(reply->rawHeader("Accept-Ranges"));
QString contentRange = QString::fromLatin1(reply->rawHeader("Content-Range"));
reply->deleteLater();
nlinfo("HTTP status code %d on HEAD for %s", status, Q2C(url));
if (!redirection.isEmpty())
{
nlinfo("Redirected to %s", Q2C(redirection));
}
// redirection
if (status >= 300 && status < 400)
{
if (redirection.isEmpty())
{
nlwarning("No redirection defined");
if (m_listener) m_listener->operationFail(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
if (m_listener) m_listener->operationInit(0, m_size);
if (!m_supportsAcceptRanges && acceptRanges == "bytes")
{
nlinfo("Server supports resume for %s", Q2C(url));
// 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
nlwarning("Server doesn't support resume, download %s from the beginning", Q2C(url));
}
// 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
if (m_listener) m_listener->operationInit(m_offset, m_size);
nlinfo("Server supports resume for %s: offset %" NL_I64 "d, size %" NL_I64 "d", Q2C(url), m_offset, m_size);
}
else
{
nlwarning("Unable to parse %s", Q2C(contentRange));
}
}
// other status
else
{
if (m_listener) m_listener->operationFail(tr("Incorrect status code: %1").arg(status));
return;
}
if (m_downloadAfterHead)
{
if (checkDownloadedFile())
{
nlwarning("Same date and size");
}
else
{
downloadFile();
}
}
else
{
emit downloadPrepared();
}
}
void CDownloader::onDownloadFinished()
{
QNetworkReply *reply = qobject_cast(sender());
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString url = reply->url().toString();
reply->deleteLater();
nlwarning("Download finished with HTTP status code %d when downloading %s", status, Q2C(url));
closeFile();
if (m_listener && m_listener->operationShouldStop())
{
m_listener->operationStop();
}
else
{
if (QFileInfo(m_fullPath).size() == m_size)
{
bool ok = NLMISC::CFile::setFileModificationDate(m_fullPath.toUtf8().constData(), m_lastModified.toTime_t());
if (m_listener) m_listener->operationSuccess(m_size);
emit downloadDone();
}
else if (status >= 200 && status < 300)
{
if (m_listener) m_listener->operationContinue();
}
else
{
if (m_listener) m_listener->operationFail(tr("HTTP error: %1").arg(status));
}
}
}
void CDownloader::onError(QNetworkReply::NetworkError error)
{
QNetworkReply *reply = qobject_cast(sender());
nlwarning("Network error %s (%d) when downloading %s", Q2C(reply->errorString()), error, Q2C(m_url));
if (!m_listener) return;
if (error == QNetworkReply::OperationCanceledError)
{
m_listener->operationStop();
}
}
void CDownloader::onDownloadProgress(qint64 current, qint64 total)
{
stopTimer();
if (!m_listener) return;
QNetworkReply *reply = qobject_cast(sender());
m_listener->operationProgress(m_offset + current, m_url);
// abort download
if (m_listener->operationShouldStop() && reply) reply->abort();
}
void CDownloader::onDownloadRead()
{
QNetworkReply *reply = qobject_cast(sender());
if (m_file && reply) m_file->write(reply->readAll());
}