mirror of
https://github.com/nextcloud/desktop.git
synced 2024-10-29 16:44:45 +03:00
* Downloadinfo entries for files that no longer need to be downloaded are useless and can be removed. In particular, the temporary files holding partially retrieved files are now deleted when no longer necessary. * The same is true for blacklist entries for paths that are no longer being discovered. * Same for uploadinfos for files that no longer need to be uploaded.
764 lines
27 KiB
C++
764 lines
27 KiB
C++
/*
|
|
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU 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 General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "propagator_qnam.h"
|
|
#include "networkjobs.h"
|
|
#include "account.h"
|
|
#include "syncjournaldb.h"
|
|
#include "syncjournalfilerecord.h"
|
|
#include "utility.h"
|
|
#include "filesystem.h"
|
|
#include "propagatorjobs.h"
|
|
#include <QNetworkAccessManager>
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <cmath>
|
|
|
|
namespace Mirall {
|
|
|
|
static qint64 chunkSize() {
|
|
static uint chunkSize;
|
|
if (!chunkSize) {
|
|
chunkSize = qgetenv("OWNCLOUD_CHUNK_SIZE").toUInt();
|
|
if (chunkSize == 0) {
|
|
chunkSize = 20*1024*1024; // default to 20 MiB
|
|
}
|
|
}
|
|
return chunkSize;
|
|
}
|
|
|
|
static QByteArray get_etag_from_reply(QNetworkReply *reply)
|
|
{
|
|
QByteArray ret = parseEtag(reply->rawHeader("OC-ETag"));
|
|
if (ret.isEmpty()) {
|
|
ret = parseEtag(reply->rawHeader("ETag"));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Fiven an error from the network, map to a SyncFileItem::Status error
|
|
*/
|
|
static SyncFileItem::Status classifyError(QNetworkReply::NetworkError nerror, int httpCode) {
|
|
Q_ASSERT (nerror != QNetworkReply::NoError); // we should only be called when there is an error
|
|
|
|
if (nerror > QNetworkReply::NoError && nerror <= QNetworkReply::UnknownProxyError) {
|
|
// network error or proxy error -> fatal
|
|
return SyncFileItem::FatalError;
|
|
}
|
|
|
|
if (httpCode == 412) {
|
|
// "Precondition Failed"
|
|
// Happens when the e-tag has changed
|
|
return SyncFileItem::SoftError;
|
|
}
|
|
|
|
return SyncFileItem::NormalError;
|
|
}
|
|
|
|
void PUTFileJob::start() {
|
|
QNetworkRequest req;
|
|
for(QMap<QByteArray, QByteArray>::const_iterator it = _headers.begin(); it != _headers.end(); ++it) {
|
|
req.setRawHeader(it.key(), it.value());
|
|
}
|
|
|
|
setReply(davRequest("PUT", path(), req, _device));
|
|
_device->setParent(reply());
|
|
setupConnections(reply());
|
|
|
|
if( reply()->error() != QNetworkReply::NoError ) {
|
|
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();
|
|
}
|
|
|
|
connect(reply(), SIGNAL(uploadProgress(qint64,qint64)), this, SIGNAL(uploadProgress(qint64,qint64)));
|
|
connect(reply(), SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(resetTimeout()));
|
|
|
|
AbstractNetworkJob::start();
|
|
}
|
|
|
|
void PUTFileJob::slotTimeout() {
|
|
_errorString = tr("Connection Timeout");
|
|
reply()->abort();
|
|
}
|
|
|
|
void PropagateUploadFileQNAM::start()
|
|
{
|
|
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
|
return;
|
|
|
|
_file = new QFile(_propagator->getFilePath(_item._file), this);
|
|
if (!_file->open(QIODevice::ReadOnly)) {
|
|
done(SyncFileItem::NormalError, _file->errorString());
|
|
delete _file;
|
|
return;
|
|
}
|
|
|
|
quint64 fileSize = _file->size();
|
|
_chunkCount = std::ceil(fileSize/double(chunkSize()));
|
|
_startChunk = 0;
|
|
_transferId = qrand() ^ _item._modtime ^ (_item._size << 16);
|
|
|
|
const SyncJournalDb::UploadInfo progressInfo = _propagator->_journal->getUploadInfo(_item._file);
|
|
|
|
if (progressInfo._valid && Utility::qDateTimeToTime_t(progressInfo._modtime) == _item._modtime ) {
|
|
_startChunk = progressInfo._chunk;
|
|
_transferId = progressInfo._transferid;
|
|
qDebug() << Q_FUNC_INFO << _item._file << ": Resuming from chunk " << _startChunk;
|
|
}
|
|
|
|
_currentChunk = 0;
|
|
_duration.start();
|
|
|
|
_propagator->_activeJobs++;
|
|
emit progress(_item, 0);
|
|
emitReady();
|
|
this->startNextChunk();
|
|
}
|
|
|
|
struct ChunkDevice : QIODevice {
|
|
public:
|
|
QPointer<QIODevice> _file;
|
|
qint64 _read;
|
|
qint64 _size;
|
|
qint64 _start;
|
|
|
|
ChunkDevice(QIODevice *file, qint64 start, qint64 size)
|
|
: QIODevice(file), _file(file), _read(0), _size(size), _start(start) {
|
|
_file = QPointer<QIODevice>(file);
|
|
_file.data()->seek(start);
|
|
}
|
|
|
|
virtual qint64 writeData(const char* , qint64 ) Q_DECL_OVERRIDE {
|
|
Q_ASSERT(!"write to read only device");
|
|
return 0;
|
|
}
|
|
|
|
virtual qint64 readData(char* data, qint64 maxlen) Q_DECL_OVERRIDE {
|
|
if (_file.isNull()) {
|
|
qDebug() << Q_FUNC_INFO << "Upload file object deleted during upload";
|
|
close();
|
|
return -1;
|
|
}
|
|
maxlen = qMin(maxlen, chunkSize() - _read);
|
|
if (maxlen == 0)
|
|
return 0;
|
|
qint64 ret = _file.data()->read(data, maxlen);
|
|
if (ret < 0)
|
|
return -1;
|
|
_read += ret;
|
|
return ret;
|
|
}
|
|
|
|
virtual bool atEnd() const Q_DECL_OVERRIDE {
|
|
if (_file.isNull()) {
|
|
qDebug() << Q_FUNC_INFO << "Upload file object deleted during upload";
|
|
return true;
|
|
}
|
|
return _read >= chunkSize() || _file.data()->atEnd();
|
|
}
|
|
|
|
virtual qint64 size() const Q_DECL_OVERRIDE{
|
|
return _size;
|
|
}
|
|
|
|
qint64 bytesAvailable() const Q_DECL_OVERRIDE
|
|
{
|
|
return _size - _read + QIODevice::bytesAvailable();
|
|
}
|
|
|
|
// random access, we can seek
|
|
virtual bool isSequential() const Q_DECL_OVERRIDE{
|
|
return false;
|
|
}
|
|
|
|
virtual bool seek ( qint64 pos ) Q_DECL_OVERRIDE {
|
|
if (_file.isNull()) {
|
|
qDebug() << Q_FUNC_INFO << "Upload file object deleted during upload";
|
|
close();
|
|
return false;
|
|
}
|
|
_read = pos;
|
|
return _file.data()->seek(pos + _start);
|
|
}
|
|
};
|
|
|
|
void PropagateUploadFileQNAM::startNextChunk()
|
|
{
|
|
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
|
return;
|
|
|
|
|
|
/*
|
|
* // If the source file has changed during upload, it is detected and the
|
|
* // variable _previousFileSize is set accordingly. The propagator waits a
|
|
* // couple of seconds and retries.
|
|
* if(_previousFileSize > 0) {
|
|
* qDebug() << "File size changed underway: " << trans->stat_size - _previousFileSize;
|
|
* // Report the change of the overall transmission size to the propagator
|
|
* _propagator->overallTransmissionSizeChanged(qint64(trans->stat_size - _previousFileSize));
|
|
* // update the item's values to the current from trans. hbf_splitlist does a stat
|
|
* _item._size = trans->stat_size;
|
|
* _item._modtime = trans->modtime;
|
|
*
|
|
*/
|
|
quint64 fileSize = _item._size;
|
|
QMap<QByteArray, QByteArray> headers;
|
|
headers["OC-Total-Length"] = QByteArray::number(fileSize);
|
|
headers["Content-Type"] = "application/octet-stream";
|
|
headers["X-OC-Mtime"] = QByteArray::number(qint64(_item._modtime));
|
|
if (!_item._etag.isEmpty() && _item._etag != "empty_etag" &&
|
|
_item._instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
|
|
) {
|
|
// We add quotes because the owncloud server always add quotes around the etag, and
|
|
// csync_owncloud.c's owncloud_file_id always strip the quotes.
|
|
headers["If-Match"] = '"' + _item._etag + '"';
|
|
}
|
|
|
|
QString path = _item._file;
|
|
QIODevice *device = 0;
|
|
if (_chunkCount > 1) {
|
|
int sendingChunk = (_currentChunk + _startChunk) % _chunkCount;
|
|
// XOR with chunk size to make sure everything goes well if chunk size change between runs
|
|
uint transid = _transferId ^ chunkSize();
|
|
path += QString("-chunking-%1-%2-%3").arg(transid).arg(_chunkCount).arg(sendingChunk);
|
|
headers["OC-Chunked"] = "1";
|
|
int currentChunkSize = chunkSize();
|
|
if (sendingChunk == _chunkCount - 1) { // last chunk
|
|
currentChunkSize = (fileSize % chunkSize());
|
|
if( currentChunkSize == 0 ) { // if the last chunk pretents to be 0, its actually the full chunk size.
|
|
currentChunkSize = chunkSize();
|
|
}
|
|
}
|
|
device = new ChunkDevice(_file, chunkSize() * quint64(sendingChunk), currentChunkSize);
|
|
} else {
|
|
device = _file;
|
|
}
|
|
|
|
bool isOpen = true;
|
|
if (!device->isOpen()) {
|
|
isOpen = device->open(QIODevice::ReadOnly);
|
|
}
|
|
|
|
if( isOpen ) {
|
|
_job = new PUTFileJob(AccountManager::instance()->account(), _propagator->_remoteFolder + path, device, headers);
|
|
_job->setTimeout(_propagator->httpTimeout() * 1000);
|
|
connect(_job, SIGNAL(finishedSignal()), this, SLOT(slotPutFinished()));
|
|
connect(_job, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(slotUploadProgress(qint64,qint64)));
|
|
_job->start();
|
|
} else {
|
|
qDebug() << "ERR: Could not open upload file: " << device->errorString();
|
|
done( SyncFileItem::NormalError, device->errorString() );
|
|
delete device;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void PropagateUploadFileQNAM::slotPutFinished()
|
|
{
|
|
PUTFileJob *job = qobject_cast<PUTFileJob *>(sender());
|
|
Q_ASSERT(job);
|
|
|
|
qDebug() << Q_FUNC_INFO << job->reply()->request().url() << "FINISHED WITH STATUS"
|
|
<< job->reply()->error()
|
|
<< (job->reply()->error() == QNetworkReply::NoError ? QLatin1String("") : job->reply()->errorString())
|
|
<< job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute)
|
|
<< job->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
|
|
|
|
QNetworkReply::NetworkError err = job->reply()->error();
|
|
if (err != QNetworkReply::NoError) {
|
|
_item._httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
_propagator->_activeJobs--;
|
|
if(checkForProblemsWithShared(_item._httpErrorCode,
|
|
tr("The file was edited locally but is part of a read only share. "
|
|
"It is restored and your edit is in the conflict file."))) {
|
|
return;
|
|
}
|
|
QString errorString = job->errorString();
|
|
|
|
QByteArray replyContent = job->reply()->readAll();
|
|
qDebug() << replyContent; // display the XML error in the debug
|
|
QRegExp rx("<s:message>(.*)</s:message>"); // Issue #1366: display server exception
|
|
if (rx.indexIn(QString::fromUtf8(replyContent)) != -1) {
|
|
errorString += QLatin1String(" (") + rx.cap(1) + QLatin1Char(')');
|
|
}
|
|
|
|
if (_item._httpErrorCode == 412) {
|
|
// Precondition Failed: Maybe the bad etag is in the database, we need to clear the
|
|
// parent folder etag so we won't read from DB next sync.
|
|
_propagator->_journal->avoidReadFromDbOnNextSync(_item._file);
|
|
}
|
|
|
|
done(classifyError(err, _item._httpErrorCode), errorString);
|
|
return;
|
|
}
|
|
|
|
bool finished = job->reply()->hasRawHeader("ETag")
|
|
|| job->reply()->hasRawHeader("OC-ETag");
|
|
|
|
if (!finished) {
|
|
QFileInfo fi(_propagator->getFilePath(_item._file));
|
|
if( !fi.exists() ) {
|
|
_propagator->_activeJobs--;
|
|
done(SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
|
return;
|
|
}
|
|
|
|
if (Utility::qDateTimeToTime_t(fi.lastModified()) != _item._modtime) {
|
|
qDebug() << "The local file has changed during upload:" << _item._modtime << "!=" << Utility::qDateTimeToTime_t(fi.lastModified()) << fi.lastModified();
|
|
_propagator->_activeJobs--;
|
|
done(SyncFileItem::SoftError, tr("Local file changed during sync."));
|
|
// FIXME: the legacy code was retrying for a few seconds.
|
|
// and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
|
|
return;
|
|
}
|
|
|
|
// Proceed to next chunk.
|
|
_currentChunk++;
|
|
if (_currentChunk >= _chunkCount) {
|
|
_propagator->_activeJobs--;
|
|
done(SyncFileItem::NormalError, tr("The server did not acknowledge the last chunk. (No e-tag were present)"));
|
|
return;
|
|
}
|
|
|
|
SyncJournalDb::UploadInfo pi;
|
|
pi._valid = true;
|
|
pi._chunk = (_currentChunk + _startChunk) % _chunkCount; // next chunk to start with
|
|
pi._transferid = _transferId;
|
|
pi._modtime = Utility::qDateTimeFromTime_t(_item._modtime);
|
|
_propagator->_journal->setUploadInfo(_item._file, pi);
|
|
_propagator->_journal->commit("Upload info");
|
|
startNextChunk();
|
|
return;
|
|
}
|
|
|
|
// the following code only happens after all chunks were uploaded.
|
|
//
|
|
// the file id should only be empty for new files up- or downloaded
|
|
QByteArray fid = job->reply()->rawHeader("OC-FileID");
|
|
if( !fid.isEmpty() ) {
|
|
if( !_item._fileId.isEmpty() && _item._fileId != fid ) {
|
|
qDebug() << "WARN: File ID changed!" << _item._fileId << fid;
|
|
}
|
|
_item._fileId = fid;
|
|
}
|
|
|
|
QByteArray etag = get_etag_from_reply(job->reply());
|
|
_item._etag = etag;
|
|
|
|
_item._responseTimeStamp = job->responseTimestamp();
|
|
|
|
if (job->reply()->rawHeader("X-OC-MTime") != "accepted") {
|
|
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
|
|
// Normaly Owncloud 6 always put X-OC-MTime
|
|
qDebug() << "Server do not support X-OC-MTime";
|
|
PropagatorJob *newJob = new UpdateMTimeAndETagJob(_propagator, _item);
|
|
QObject::connect(newJob, SIGNAL(completed(SyncFileItem)), this, SLOT(finalize(SyncFileItem)));
|
|
QMetaObject::invokeMethod(newJob, "start");
|
|
return;
|
|
}
|
|
finalize(_item);
|
|
}
|
|
|
|
void PropagateUploadFileQNAM::finalize(const SyncFileItem ©)
|
|
{
|
|
// Normally, copy == _item, but when it comes from the UpdateMTimeAndETagJob, we need to do
|
|
// some updates
|
|
_item._etag = copy._etag;
|
|
_item._fileId = copy._fileId;
|
|
|
|
_propagator->_activeJobs--;
|
|
|
|
_item._requestDuration = _duration.elapsed();
|
|
|
|
_propagator->_journal->setFileRecord(SyncJournalFileRecord(_item, _propagator->getFilePath(_item._file)));
|
|
// Remove from the progress database:
|
|
_propagator->_journal->setUploadInfo(_item._file, SyncJournalDb::UploadInfo());
|
|
_propagator->_journal->commit("upload file start");
|
|
|
|
done(SyncFileItem::Success);
|
|
}
|
|
|
|
void PropagateUploadFileQNAM::slotUploadProgress(qint64 sent, qint64)
|
|
{
|
|
int progressChunk = _currentChunk + _startChunk;
|
|
if (progressChunk >= _chunkCount)
|
|
progressChunk = _currentChunk;
|
|
emit progress(_item, sent + progressChunk * chunkSize());
|
|
}
|
|
|
|
|
|
void PropagateUploadFileQNAM::abort()
|
|
{
|
|
if (_job && _job->reply()) {
|
|
qDebug() << Q_FUNC_INFO << this->_item._file;
|
|
_job->reply()->abort();
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// DOES NOT take owncership of the device.
|
|
GETFileJob::GETFileJob(Account* account, const QString& path, QFile *device,
|
|
const QMap<QByteArray, QByteArray> &headers, QByteArray expectedEtagForResume,
|
|
quint64 _resumeStart, QObject* parent)
|
|
: AbstractNetworkJob(account, path, parent),
|
|
_device(device), _headers(headers), _expectedEtagForResume(expectedEtagForResume),
|
|
_resumeStart(_resumeStart) , _errorStatus(SyncFileItem::NoStatus)
|
|
{
|
|
}
|
|
|
|
GETFileJob::GETFileJob(Account* account, const QUrl& url, QFile *device,
|
|
const QMap<QByteArray, QByteArray> &headers,
|
|
QObject* parent)
|
|
: AbstractNetworkJob(account, url.toEncoded(), parent),
|
|
_device(device), _headers(headers), _resumeStart(0),
|
|
_errorStatus(SyncFileItem::NoStatus), _directDownloadUrl(url)
|
|
{
|
|
}
|
|
|
|
|
|
void GETFileJob::start() {
|
|
QNetworkRequest req;
|
|
for(QMap<QByteArray, QByteArray>::const_iterator it = _headers.begin(); it != _headers.end(); ++it) {
|
|
req.setRawHeader(it.key(), it.value());
|
|
}
|
|
|
|
if (_directDownloadUrl.isEmpty()) {
|
|
setReply(davRequest("GET", path(), req));
|
|
} else {
|
|
// Use direct URL
|
|
setReply(davRequest("GET", _directDownloadUrl, req));
|
|
}
|
|
setupConnections(reply());
|
|
reply()->setReadBufferSize(128 * 1024);
|
|
|
|
if( reply()->error() != QNetworkReply::NoError ) {
|
|
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();
|
|
}
|
|
|
|
connect(reply(), SIGNAL(metaDataChanged()), this, SLOT(slotMetaDataChanged()));
|
|
connect(reply(), SIGNAL(readyRead()), this, SLOT(slotReadyRead()));
|
|
connect(reply(), SIGNAL(downloadProgress(qint64,qint64)), this, SIGNAL(downloadProgress(qint64,qint64)));
|
|
|
|
AbstractNetworkJob::start();
|
|
}
|
|
|
|
void GETFileJob::slotMetaDataChanged()
|
|
{
|
|
if (reply()->error() != QNetworkReply::NoError
|
|
|| reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() / 100 != 2) {
|
|
// We will handle the error when the job is finished.
|
|
return;
|
|
}
|
|
_etag = get_etag_from_reply(reply());
|
|
|
|
if (!_directDownloadUrl.isEmpty() && !_etag.isEmpty()) {
|
|
qDebug() << Q_FUNC_INFO << "Direct download used, ignoring server ETag" << _etag;
|
|
_etag = QByteArray(); // reset received ETag
|
|
} else if (!_directDownloadUrl.isEmpty()) {
|
|
// All fine, ETag empty and directDownloadUrl used
|
|
} else if (_etag.isEmpty()) {
|
|
qDebug() << Q_FUNC_INFO << "No E-Tag reply by server, considering it invalid";
|
|
_errorString = tr("No E-Tag received from server, check Proxy/Gateway");
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
reply()->abort();
|
|
return;
|
|
} else if (!_expectedEtagForResume.isEmpty() && _expectedEtagForResume != _etag) {
|
|
qDebug() << Q_FUNC_INFO << "We received a different E-Tag for resuming!"
|
|
<< _expectedEtagForResume << "vs" << _etag;
|
|
_errorString = tr("We received a different E-Tag for resuming. Retrying next time.");
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
reply()->abort();
|
|
return;
|
|
}
|
|
|
|
quint64 start = 0;
|
|
QByteArray ranges = reply()->rawHeader("Content-Range");
|
|
if (!ranges.isEmpty()) {
|
|
QRegExp rx("bytes (\\d+)-");
|
|
if (rx.indexIn(ranges) >= 0) {
|
|
start = rx.cap(1).toULongLong();
|
|
}
|
|
}
|
|
if (start != _resumeStart) {
|
|
qDebug() << Q_FUNC_INFO << "Wrong content-range: "<< ranges << " while expecting start was" << _resumeStart;
|
|
if (start == 0) {
|
|
// device don't support range, just stry again from scratch
|
|
_device->close();
|
|
if (!_device->open(QIODevice::WriteOnly)) {
|
|
_errorString = _device->errorString();
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
reply()->abort();
|
|
return;
|
|
}
|
|
_resumeStart = 0;
|
|
} else {
|
|
_errorString = tr("Server returned wrong content-range");
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
reply()->abort();
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void GETFileJob::slotReadyRead()
|
|
{
|
|
int bufferSize = qMin(1024*8ll , reply()->bytesAvailable());
|
|
QByteArray buffer(bufferSize, Qt::Uninitialized);
|
|
|
|
while(reply()->bytesAvailable() > 0) {
|
|
qint64 r = reply()->read(buffer.data(), bufferSize);
|
|
if (r < 0) {
|
|
_errorString = reply()->errorString();
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
qDebug() << "Error while reading from device: " << _errorString;
|
|
reply()->abort();
|
|
return;
|
|
}
|
|
|
|
qint64 w = _device->write(buffer.constData(), r);
|
|
if (w != r) {
|
|
_errorString = _device->errorString();
|
|
_errorStatus = SyncFileItem::NormalError;
|
|
qDebug() << "Error while writing to file" << w << r << _errorString;
|
|
reply()->abort();
|
|
return;
|
|
}
|
|
}
|
|
resetTimeout();
|
|
}
|
|
|
|
void GETFileJob::slotTimeout()
|
|
{
|
|
_errorString = tr("Connection Timeout");
|
|
_errorStatus = SyncFileItem::FatalError;
|
|
reply()->abort();
|
|
}
|
|
|
|
void PropagateDownloadFileQNAM::start()
|
|
{
|
|
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
|
return;
|
|
|
|
qDebug() << Q_FUNC_INFO << _item._file << _propagator->_activeJobs;
|
|
|
|
// do a klaas' case clash check.
|
|
if( _propagator->localFileNameClash(_item._file) ) {
|
|
done( SyncFileItem::NormalError, tr("File %1 can not be downloaded because of a local file name clash!")
|
|
.arg(QDir::toNativeSeparators(_item._file)) );
|
|
return;
|
|
}
|
|
|
|
emit progress(_item, 0);
|
|
|
|
QString tmpFileName;
|
|
QByteArray expectedEtagForResume;
|
|
const SyncJournalDb::DownloadInfo progressInfo = _propagator->_journal->getDownloadInfo(_item._file);
|
|
if (progressInfo._valid) {
|
|
// if the etag has changed meanwhile, remove the already downloaded part.
|
|
if (progressInfo._etag != _item._etag) {
|
|
QFile::remove(_propagator->getFilePath(progressInfo._tmpfile));
|
|
_propagator->_journal->setDownloadInfo(_item._file, SyncJournalDb::DownloadInfo());
|
|
} else {
|
|
tmpFileName = progressInfo._tmpfile;
|
|
expectedEtagForResume = progressInfo._etag;
|
|
}
|
|
|
|
}
|
|
|
|
if (tmpFileName.isEmpty()) {
|
|
tmpFileName = _item._file;
|
|
//add a dot at the begining of the filename to hide the file.
|
|
int slashPos = tmpFileName.lastIndexOf('/');
|
|
tmpFileName.insert(slashPos+1, '.');
|
|
//add the suffix
|
|
tmpFileName += ".~" + QString::number(uint(qrand()), 16);
|
|
}
|
|
|
|
_tmpFile.setFileName(_propagator->getFilePath(tmpFileName));
|
|
if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) {
|
|
done(SyncFileItem::NormalError, _tmpFile.errorString());
|
|
return;
|
|
}
|
|
|
|
FileSystem::setFileHidden(_tmpFile.fileName(), true);
|
|
|
|
{
|
|
SyncJournalDb::DownloadInfo pi;
|
|
pi._etag = _item._etag;
|
|
pi._tmpfile = tmpFileName;
|
|
pi._valid = true;
|
|
_propagator->_journal->setDownloadInfo(_item._file, pi);
|
|
_propagator->_journal->commit("download file start");
|
|
}
|
|
|
|
|
|
QMap<QByteArray, QByteArray> headers;
|
|
|
|
quint64 startSize = 0;
|
|
if (_tmpFile.size() > 0) {
|
|
quint64 done = _tmpFile.size();
|
|
if (done == _item._size) {
|
|
qDebug() << "File is already complete, no need to download";
|
|
downloadFinished();
|
|
return;
|
|
}
|
|
headers["Range"] = "bytes=" + QByteArray::number(done) +'-';
|
|
headers["Accept-Ranges"] = "bytes";
|
|
qDebug() << "Retry with range " << headers["Range"];
|
|
startSize = done;
|
|
}
|
|
|
|
if (_item._directDownloadUrl.isEmpty()) {
|
|
// Normal job, download from oC instance
|
|
_job = new GETFileJob(AccountManager::instance()->account(),
|
|
_propagator->_remoteFolder + _item._file,
|
|
&_tmpFile, headers, expectedEtagForResume, startSize);
|
|
} else {
|
|
// We were provided a direct URL, use that one
|
|
if (!_item._directDownloadCookies.isEmpty()) {
|
|
headers["Cookie"] = _item._directDownloadCookies.toUtf8();
|
|
}
|
|
QUrl url = QUrl::fromUserInput(_item._directDownloadUrl);
|
|
_job = new GETFileJob(AccountManager::instance()->account(),
|
|
url,
|
|
&_tmpFile, headers);
|
|
qDebug() << Q_FUNC_INFO << "directDownloadUrl given for " << _item._file << _item._directDownloadUrl;
|
|
}
|
|
_job->setTimeout(_propagator->httpTimeout() * 1000);
|
|
connect(_job, SIGNAL(finishedSignal()), this, SLOT(slotGetFinished()));
|
|
connect(_job, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(slotDownloadProgress(qint64,qint64)));
|
|
_propagator->_activeJobs ++;
|
|
_job->start();
|
|
emitReady();
|
|
}
|
|
|
|
void PropagateDownloadFileQNAM::slotGetFinished()
|
|
{
|
|
_propagator->_activeJobs--;
|
|
|
|
GETFileJob *job = qobject_cast<GETFileJob *>(sender());
|
|
Q_ASSERT(job);
|
|
|
|
qDebug() << Q_FUNC_INFO << job->reply()->request().url() << "FINISHED WITH STATUS"
|
|
<< job->reply()->error()
|
|
<< (job->reply()->error() == QNetworkReply::NoError ? QLatin1String("") : job->reply()->errorString());
|
|
|
|
QNetworkReply::NetworkError err = job->reply()->error();
|
|
if (err != QNetworkReply::NoError) {
|
|
if (_tmpFile.size() == 0) {
|
|
// don't keep the temporary file if it is empty.
|
|
_tmpFile.close();
|
|
_tmpFile.remove();
|
|
_propagator->_journal->setDownloadInfo(_item._file, SyncJournalDb::DownloadInfo());
|
|
}
|
|
_item._httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
_propagator->_activeJobs--;
|
|
SyncFileItem::Status status = job->errorStatus();
|
|
if (status == SyncFileItem::NoStatus) {
|
|
status = classifyError(err, _item._httpErrorCode);
|
|
}
|
|
done(status, job->errorString());
|
|
return;
|
|
}
|
|
|
|
if (!job->etag().isEmpty()) {
|
|
// The etag will be empty if we used a direct download URL.
|
|
// (If it was really empty by the server, the GETFileJob will have errored
|
|
_item._etag = parseEtag(job->etag());
|
|
}
|
|
_item._requestDuration = job->duration();
|
|
_item._responseTimeStamp = job->responseTimestamp();
|
|
|
|
_tmpFile.close();
|
|
_tmpFile.flush();
|
|
downloadFinished();
|
|
}
|
|
|
|
QString makeConflictFileName(const QString &fn, const QDateTime &dt)
|
|
{
|
|
QString conflictFileName(fn);
|
|
// Add _conflict-XXXX before the extention.
|
|
int dotLocation = conflictFileName.lastIndexOf('.');
|
|
// If no extention, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
|
|
if (dotLocation <= conflictFileName.lastIndexOf('/') + 1) {
|
|
dotLocation = conflictFileName.size();
|
|
}
|
|
QString timeString = dt.toString("yyyyMMdd-hhmmss");
|
|
|
|
// Additional marker
|
|
QByteArray conflictFileUserName = qgetenv("CSYNC_CONFLICT_FILE_USERNAME");
|
|
if (conflictFileUserName.isEmpty())
|
|
conflictFileName.insert(dotLocation, "_conflict-" + timeString);
|
|
else
|
|
conflictFileName.insert(dotLocation, "_conflict_" + QString::fromUtf8(conflictFileUserName) + "-" + timeString);
|
|
|
|
return conflictFileName;
|
|
}
|
|
|
|
void PropagateDownloadFileQNAM::downloadFinished()
|
|
{
|
|
|
|
QString fn = _propagator->getFilePath(_item._file);
|
|
|
|
|
|
bool isConflict = _item._instruction == CSYNC_INSTRUCTION_CONFLICT
|
|
&& !FileSystem::fileEquals(fn, _tmpFile.fileName()); // compare the files to see if there was an actual conflict.
|
|
//In case of conflict, make a backup of the old file
|
|
if (isConflict) {
|
|
QFile f(fn);
|
|
QString conflictFileName = makeConflictFileName(fn, Utility::qDateTimeFromTime_t(_item._modtime));
|
|
if (!f.rename(conflictFileName)) {
|
|
//If the rename fails, don't replace it.
|
|
done(SyncFileItem::NormalError, f.errorString());
|
|
return;
|
|
}
|
|
}
|
|
|
|
QFileInfo existingFile(fn);
|
|
if(existingFile.exists() && existingFile.permissions() != _tmpFile.permissions()) {
|
|
_tmpFile.setPermissions(existingFile.permissions());
|
|
}
|
|
|
|
FileSystem::setFileHidden(_tmpFile.fileName(), false);
|
|
|
|
QString error;
|
|
if (!FileSystem::renameReplace(_tmpFile.fileName(), fn, &error)) {
|
|
done(SyncFileItem::NormalError, error);
|
|
return;
|
|
}
|
|
|
|
FileSystem::setModTime(fn, _item._modtime);
|
|
|
|
_propagator->_journal->setFileRecord(SyncJournalFileRecord(_item, fn));
|
|
_propagator->_journal->setDownloadInfo(_item._file, SyncJournalDb::DownloadInfo());
|
|
_propagator->_journal->commit("download file start2");
|
|
done(isConflict ? SyncFileItem::Conflict : SyncFileItem::Success);
|
|
}
|
|
|
|
void PropagateDownloadFileQNAM::slotDownloadProgress(qint64 received, qint64)
|
|
{
|
|
if (!_job) return;
|
|
emit progress(_item, received + _job->resumeStart());
|
|
}
|
|
|
|
|
|
void PropagateDownloadFileQNAM::abort()
|
|
{
|
|
if (_job && _job->reply())
|
|
_job->reply()->abort();
|
|
}
|
|
|
|
}
|