mirror of
https://github.com/nextcloud/desktop.git
synced 2025-10-26 11:17:43 +00:00
Use qCInfo for anything that has general value for support and development. Use qCWarning for any recoverable error and qCCritical for anything that could result in data loss or would identify a serious issue with the code. Issue #5647
383 lines
15 KiB
C++
383 lines
15 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 "config.h"
|
|
#include "propagateupload.h"
|
|
#include "owncloudpropagator_p.h"
|
|
#include "networkjobs.h"
|
|
#include "account.h"
|
|
#include "syncjournaldb.h"
|
|
#include "syncjournalfilerecord.h"
|
|
#include "utility.h"
|
|
#include "filesystem.h"
|
|
#include "propagatorjobs.h"
|
|
#include "checksums.h"
|
|
#include "syncengine.h"
|
|
#include "propagateremotedelete.h"
|
|
#include "asserts.h"
|
|
|
|
#include <QNetworkAccessManager>
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
namespace OCC {
|
|
|
|
void PropagateUploadFileV1::doStartUpload()
|
|
{
|
|
_chunkCount = std::ceil(_item->_size / 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;
|
|
qCDebug(lcPropagateUpload) << _item->_file << ": Resuming from chunk " << _startChunk;
|
|
}
|
|
|
|
_currentChunk = 0;
|
|
|
|
propagator()->reportProgress(*_item, 0);
|
|
startNextChunk();
|
|
}
|
|
|
|
void PropagateUploadFileV1::startNextChunk()
|
|
{
|
|
if (propagator()->_abortRequested.fetchAndAddRelaxed(0))
|
|
return;
|
|
|
|
if (! _jobs.isEmpty() && _currentChunk + _startChunk >= _chunkCount - 1) {
|
|
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
|
// https://github.com/owncloud/core/issues/11106
|
|
// We return now and when the _jobs are finished we will proceed with the last chunk
|
|
// NOTE: Some other parts of the code such as slotUploadProgress also assume that the last chunk
|
|
// is sent last.
|
|
return;
|
|
}
|
|
quint64 fileSize = _item->_size;
|
|
auto headers = PropagateUploadFileCommon::headers();
|
|
headers["OC-Total-Length"] = QByteArray::number(fileSize);
|
|
headers["OC-Chunk-Size"]= QByteArray::number(quint64(chunkSize()));
|
|
|
|
QString path = _item->_file;
|
|
|
|
UploadDevice *device = new UploadDevice(&propagator()->_bandwidthManager);
|
|
qint64 chunkStart = 0;
|
|
qint64 currentChunkSize = fileSize;
|
|
bool isFinalChunk = false;
|
|
if (_chunkCount > 1) {
|
|
int sendingChunk = (_currentChunk + _startChunk) % _chunkCount;
|
|
// XOR with chunk size to make sure everything goes well if chunk size changes between runs
|
|
uint transid = _transferId ^ chunkSize();
|
|
qCInfo(lcPropagateUpload) << "Upload chunk" << sendingChunk << "of" << _chunkCount << "transferid(remote)=" << transid;
|
|
path += QString("-chunking-%1-%2-%3").arg(transid).arg(_chunkCount).arg(sendingChunk);
|
|
|
|
headers["OC-Chunked"] = "1";
|
|
|
|
chunkStart = chunkSize() * quint64(sendingChunk);
|
|
currentChunkSize = chunkSize();
|
|
if (sendingChunk == _chunkCount - 1) { // last chunk
|
|
currentChunkSize = (fileSize % chunkSize());
|
|
if( currentChunkSize == 0 ) { // if the last chunk pretends to be 0, its actually the full chunk size.
|
|
currentChunkSize = chunkSize();
|
|
}
|
|
isFinalChunk = true;
|
|
}
|
|
} else {
|
|
// if there's only one chunk, it's the final one
|
|
isFinalChunk = true;
|
|
}
|
|
qCDebug(lcPropagateUpload) << _chunkCount << isFinalChunk << chunkStart << currentChunkSize;
|
|
|
|
if (isFinalChunk && !_transmissionChecksumType.isEmpty()) {
|
|
headers[checkSumHeaderC] = makeChecksumHeader(
|
|
_transmissionChecksumType, _transmissionChecksum);
|
|
}
|
|
|
|
const QString fileName = propagator()->getFilePath(_item->_file);
|
|
if (! device->prepareAndOpen(fileName, chunkStart, currentChunkSize)) {
|
|
qCWarning(lcPropagateUpload) << "Could not prepare upload device: " << device->errorString();
|
|
|
|
// If the file is currently locked, we want to retry the sync
|
|
// when it becomes available again.
|
|
if (FileSystem::isFileLocked(fileName)) {
|
|
emit propagator()->seenLockedFile(fileName);
|
|
}
|
|
// Soft error because this is likely caused by the user modifying his files while syncing
|
|
abortWithError( SyncFileItem::SoftError, device->errorString() );
|
|
delete device;
|
|
return;
|
|
}
|
|
|
|
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
|
PUTFileJob* job = new PUTFileJob(propagator()->account(), propagator()->_remoteFolder + path, device, headers, _currentChunk, this);
|
|
_jobs.append(job);
|
|
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotPutFinished()));
|
|
connect(job, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(slotUploadProgress(qint64,qint64)));
|
|
connect(job, SIGNAL(uploadProgress(qint64,qint64)), device, SLOT(slotJobUploadProgress(qint64,qint64)));
|
|
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
|
job->start();
|
|
propagator()->_activeJobList.append(this);
|
|
_currentChunk++;
|
|
|
|
bool parallelChunkUpload = true;
|
|
|
|
if (propagator()->account()->capabilities().chunkingParallelUploadDisabled()) {
|
|
// Server may also disable parallel chunked upload for any higher version
|
|
parallelChunkUpload = false;
|
|
} else {
|
|
QByteArray env = qgetenv("OWNCLOUD_PARALLEL_CHUNK");
|
|
if (!env.isEmpty()) {
|
|
parallelChunkUpload = env != "false" && env != "0";
|
|
} else {
|
|
int versionNum = propagator()->account()->serverVersionInt();
|
|
if (versionNum < Account::makeServerVersion(8, 0, 3)) {
|
|
// Disable parallel chunk upload severs older than 8.0.3 to avoid too many
|
|
// internal sever errors (#2743, #2938)
|
|
parallelChunkUpload = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (_currentChunk + _startChunk >= _chunkCount - 1) {
|
|
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
|
// https://github.com/owncloud/core/issues/11106
|
|
parallelChunkUpload = false;
|
|
}
|
|
|
|
if (parallelChunkUpload && (propagator()->_activeJobList.count() < propagator()->maximumActiveTransferJob())
|
|
&& _currentChunk < _chunkCount ) {
|
|
startNextChunk();
|
|
}
|
|
if (!parallelChunkUpload || _chunkCount - _currentChunk <= 0) {
|
|
propagator()->scheduleNextJob();
|
|
}
|
|
}
|
|
|
|
void PropagateUploadFileV1::slotPutFinished()
|
|
{
|
|
PUTFileJob *job = qobject_cast<PUTFileJob *>(sender());
|
|
ASSERT(job);
|
|
|
|
slotJobDestroyed(job); // remove it from the _jobs list
|
|
|
|
propagator()->_activeJobList.removeOne(this);
|
|
|
|
if (_finished) {
|
|
// We have sent the finished signal already. We don't need to handle any remaining jobs
|
|
return;
|
|
}
|
|
|
|
QNetworkReply::NetworkError err = job->reply()->error();
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
|
if (err == QNetworkReply::OperationCanceledError && job->reply()->property("owncloud-should-soft-cancel").isValid()) { // Abort the job and try again later.
|
|
// This works around a bug in QNAM wich might reuse a non-empty buffer for the next request.
|
|
qCWarning(lcPropagateUpload) << "Forcing job abort on HTTP connection reset with Qt < 5.4.2.";
|
|
propagator()->_anotherSyncNeeded = true;
|
|
abortWithError(SyncFileItem::SoftError, tr("Forcing job abort on HTTP connection reset with Qt < 5.4.2."));
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (err != QNetworkReply::NoError) {
|
|
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
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;
|
|
}
|
|
QByteArray replyContent;
|
|
QString errorString = job->errorStringParsingBody(&replyContent);
|
|
qCDebug(lcPropagateUpload) << replyContent; // display the XML error in the debug
|
|
|
|
if (_item->_httpErrorCode == 412) {
|
|
// Precondition Failed: Either an etag or a checksum mismatch.
|
|
|
|
// 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);
|
|
propagator()->_anotherSyncNeeded = true;
|
|
}
|
|
|
|
// Ensure errors that should eventually reset the chunked upload are tracked.
|
|
checkResettingErrors();
|
|
|
|
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
|
&propagator()->_anotherSyncNeeded);
|
|
abortWithError(status, errorString);
|
|
return;
|
|
}
|
|
|
|
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
// The server needs some time to process the request and provide us with a poll URL
|
|
if (_item->_httpErrorCode == 202) {
|
|
_finished = true;
|
|
QString path = QString::fromUtf8(job->reply()->rawHeader("OC-Finish-Poll"));
|
|
if (path.isEmpty()) {
|
|
done(SyncFileItem::NormalError, tr("Poll URL missing"));
|
|
return;
|
|
}
|
|
startPollJob(path);
|
|
return;
|
|
}
|
|
|
|
// Check the file again post upload.
|
|
// Two cases must be considered separately: If the upload is finished,
|
|
// the file is on the server and has a changed ETag. In that case,
|
|
// the etag has to be properly updated in the client journal, and because
|
|
// of that we can bail out here with an error. But we can reschedule a
|
|
// sync ASAP.
|
|
// But if the upload is ongoing, because not all chunks were uploaded
|
|
// yet, the upload can be stopped and an error can be displayed, because
|
|
// the server hasn't registered the new file yet.
|
|
QByteArray etag = getEtagFromReply(job->reply());
|
|
bool finished = etag.length() > 0;
|
|
|
|
// Check if the file still exists
|
|
const QString fullFilePath(propagator()->getFilePath(_item->_file));
|
|
if( !FileSystem::fileExists(fullFilePath) ) {
|
|
if (!finished) {
|
|
abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
|
return;
|
|
} else {
|
|
propagator()->_anotherSyncNeeded = true;
|
|
}
|
|
}
|
|
|
|
// Check whether the file changed since discovery.
|
|
if (! FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
|
propagator()->_anotherSyncNeeded = true;
|
|
if( !finished ) {
|
|
abortWithError(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;
|
|
}
|
|
}
|
|
|
|
if (!finished) {
|
|
// Proceed to next chunk.
|
|
if (_currentChunk >= _chunkCount) {
|
|
if (!_jobs.empty()) {
|
|
// just wait for the other job to finish.
|
|
return;
|
|
}
|
|
_finished = true;
|
|
done(SyncFileItem::NormalError, tr("The server did not acknowledge the last chunk. (No e-tag was present)"));
|
|
return;
|
|
}
|
|
|
|
// Deletes an existing blacklist entry on successful chunk upload
|
|
if (_item->_hasBlacklistEntry) {
|
|
propagator()->_journal->wipeErrorBlacklistEntry(_item->_file);
|
|
_item->_hasBlacklistEntry = false;
|
|
}
|
|
|
|
SyncJournalDb::UploadInfo pi;
|
|
pi._valid = true;
|
|
auto currentChunk = job->_chunk;
|
|
foreach (auto *job, _jobs) {
|
|
// Take the minimum finished one
|
|
if (auto putJob = qobject_cast<PUTFileJob*>(job)) {
|
|
currentChunk = qMin(currentChunk, putJob->_chunk - 1);
|
|
}
|
|
}
|
|
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount ; // next chunk to start with
|
|
pi._transferid = _transferId;
|
|
pi._modtime = Utility::qDateTimeFromTime_t(_item->_modtime);
|
|
pi._errorCount = 0; // successful chunk upload resets
|
|
propagator()->_journal->setUploadInfo(_item->_file, pi);
|
|
propagator()->_journal->commit("Upload info");
|
|
startNextChunk();
|
|
return;
|
|
}
|
|
|
|
// the following code only happens after all chunks were uploaded.
|
|
_finished = true;
|
|
// 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 ) {
|
|
qCWarning(lcPropagateUpload) << "File ID changed!" << _item->_fileId << fid;
|
|
}
|
|
_item->_fileId = fid;
|
|
}
|
|
|
|
_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.
|
|
// Normally Owncloud 6 always puts X-OC-MTime
|
|
qCWarning(lcPropagateUpload) << "Server does not support X-OC-MTime" << job->reply()->rawHeader("X-OC-MTime");
|
|
// Well, the mtime was not set
|
|
done(SyncFileItem::SoftError, "Server does not support X-OC-MTime");
|
|
}
|
|
|
|
#ifdef WITH_TESTING
|
|
// performance logging
|
|
quint64 duration = _stopWatch.stop();
|
|
qCDebug(lcPropagateUpload) << "*==* duration UPLOAD" << _item->_size
|
|
<< _stopWatch.durationOfLap(QLatin1String("ContentChecksum"))
|
|
<< _stopWatch.durationOfLap(QLatin1String("TransmissionChecksum"))
|
|
<< duration;
|
|
// The job might stay alive for the whole sync, release this tiny bit of memory.
|
|
_stopWatch.reset();
|
|
#endif
|
|
|
|
finalize();
|
|
}
|
|
|
|
|
|
void PropagateUploadFileV1::slotUploadProgress(qint64 sent, qint64 total)
|
|
{
|
|
// Completion is signaled with sent=0, total=0; avoid accidentally
|
|
// resetting progress due to the sent being zero by ignoring it.
|
|
// finishedSignal() is bound to be emitted soon anyway.
|
|
// See https://bugreports.qt.io/browse/QTBUG-44782.
|
|
if (sent == 0 && total == 0) {
|
|
return;
|
|
}
|
|
|
|
int progressChunk = _currentChunk + _startChunk - 1;
|
|
if (progressChunk >= _chunkCount)
|
|
progressChunk = _currentChunk - 1;
|
|
|
|
// amount is the number of bytes already sent by all the other chunks that were sent
|
|
// not including this one.
|
|
// FIXME: this assumes all chunks have the same size, which is true only if the last chunk
|
|
// has not been finished (which should not happen because the last chunk is sent sequentially)
|
|
quint64 amount = progressChunk * chunkSize();
|
|
|
|
sender()->setProperty("byteWritten", sent);
|
|
if (_jobs.count() > 1) {
|
|
amount -= (_jobs.count() -1) * chunkSize();
|
|
foreach (QObject *j, _jobs) {
|
|
amount += j->property("byteWritten").toULongLong();
|
|
}
|
|
} else {
|
|
// sender() is the only current job, no need to look at the byteWritten properties
|
|
amount += sent;
|
|
}
|
|
propagator()->reportProgress(*_item, amount);
|
|
}
|
|
|
|
}
|