Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ask server to recalculate checksum(hash) #4100

Merged
merged 5 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/common/checksums.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ ComputeChecksum *ValidateChecksumHeader::prepareStart(const QByteArray &checksum

if (!parseChecksumHeader(checksumHeader, &_expectedChecksumType, &_expectedChecksum)) {
qCWarning(lcChecksums) << "Checksum header malformed:" << checksumHeader;
emit validationFailed(tr("The checksum header is malformed."));
emit validationFailed(tr("The checksum header is malformed."), _calculatedChecksumType, _calculatedChecksum, ChecksumHeaderMalformed);
return nullptr;
}

Expand All @@ -360,15 +360,30 @@ void ValidateChecksumHeader::start(std::unique_ptr<QIODevice> device, const QByt
calculator->start(std::move(device));
}

QByteArray ValidateChecksumHeader::calculatedChecksumType() const
{
return _calculatedChecksumType;
}

QByteArray ValidateChecksumHeader::calculatedChecksum() const
{
return _calculatedChecksum;
}

void ValidateChecksumHeader::slotChecksumCalculated(const QByteArray &checksumType,
const QByteArray &checksum)
{
_calculatedChecksumType = checksumType;
_calculatedChecksum = checksum;

if (checksumType != _expectedChecksumType) {
emit validationFailed(tr("The checksum header contained an unknown checksum type \"%1\"").arg(QString::fromLatin1(_expectedChecksumType)));
emit validationFailed(tr("The checksum header contained an unknown checksum type \"%1\"").arg(QString::fromLatin1(_expectedChecksumType)),
_calculatedChecksumType, _calculatedChecksum, ChecksumTypeUnknown);
return;
}
if (checksum != _expectedChecksum) {
emit validationFailed(tr(R"(The downloaded file does not match the checksum, it will be resumed. "%1" != "%2")").arg(QString::fromUtf8(_expectedChecksum), QString::fromUtf8(checksum)));
emit validationFailed(tr(R"(The downloaded file does not match the checksum, it will be resumed. "%1" != "%2")").arg(QString::fromUtf8(_expectedChecksum), QString::fromUtf8(checksum)),
_calculatedChecksumType, _calculatedChecksum, ChecksumMismatch);
return;
}
emit validated(checksumType, checksum);
Expand Down
17 changes: 16 additions & 1 deletion src/common/checksums.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ class OCSYNC_EXPORT ValidateChecksumHeader : public QObject
{
Q_OBJECT
public:
enum FailureReason {
allexzander marked this conversation as resolved.
Show resolved Hide resolved
Success,
ChecksumHeaderMalformed,
ChecksumTypeUnknown,
ChecksumMismatch,
};
Q_ENUM(FailureReason)

explicit ValidateChecksumHeader(QObject *parent = nullptr);

/**
Expand All @@ -161,9 +169,13 @@ class OCSYNC_EXPORT ValidateChecksumHeader : public QObject
*/
void start(std::unique_ptr<QIODevice> device, const QByteArray &checksumHeader);

QByteArray calculatedChecksumType() const;
QByteArray calculatedChecksum() const;

signals:
void validated(const QByteArray &checksumType, const QByteArray &checksum);
void validationFailed(const QString &errMsg);
void validationFailed(const QString &errMsg, const QByteArray &calculatedChecksumType,
const QByteArray &calculatedChecksum, const ValidateChecksumHeader::FailureReason reason);

private slots:
void slotChecksumCalculated(const QByteArray &checksumType, const QByteArray &checksum);
Expand All @@ -173,6 +185,9 @@ private slots:

QByteArray _expectedChecksumType;
QByteArray _expectedChecksum;

QByteArray _calculatedChecksumType;
QByteArray _calculatedChecksum;
};

/**
Expand Down
15 changes: 13 additions & 2 deletions src/libsync/account.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ using namespace QKeychain;

namespace {
constexpr int pushNotificationsReconnectInterval = 1000 * 60 * 2;
constexpr int usernamePrefillServerVersinMinSupportedMajor = 24;
constexpr int usernamePrefillServerVersionMinSupportedMajor = 24;
constexpr int checksumRecalculateRequestServerVersionMinSupportedMajor = 24;
}

namespace OCC {
Expand Down Expand Up @@ -632,7 +633,17 @@ bool Account::serverVersionUnsupported() const

bool Account::isUsernamePrefillSupported() const
{
return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersinMinSupportedMajor, 0, 0);
return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersionMinSupportedMajor, 0, 0);
}

bool Account::isChecksumRecalculateRequestSupported() const
{
return serverVersionInt() >= makeServerVersion(checksumRecalculateRequestServerVersionMinSupportedMajor, 0, 0);
}

int Account::checksumRecalculateServerVersionMinSupportedMajor() const
{
return checksumRecalculateRequestServerVersionMinSupportedMajor;
}

void Account::setServerVersion(const QString &version)
Expand Down
4 changes: 4 additions & 0 deletions src/libsync/account.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject

bool isUsernamePrefillSupported() const;

bool isChecksumRecalculateRequestSupported() const;

int checksumRecalculateServerVersionMinSupportedMajor() const;

/** True when the server connection is using HTTP2 */
bool isHttp2Supported() { return _http2Supported; }
void setHttp2Supported(bool value) { _http2Supported = value; }
Expand Down
22 changes: 4 additions & 18 deletions src/libsync/deletejob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ namespace OCC {
Q_LOGGING_CATEGORY(lcDeleteJob, "nextcloud.sync.networkjob.delete", QtInfoMsg)

DeleteJob::DeleteJob(AccountPtr account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent)
: SimpleFileJob(account, path, parent)
{
}

DeleteJob::DeleteJob(AccountPtr account, const QUrl &url, QObject *parent)
: AbstractNetworkJob(account, QString(), parent)
: SimpleFileJob(account, QString(), parent)
, _url(url)
{
}
Expand All @@ -39,24 +39,10 @@ void DeleteJob::start()
}

if (_url.isValid()) {
sendRequest("DELETE", _url, req);
startRequest("DELETE", _url, req);
} else {
sendRequest("DELETE", makeDavUrl(path()), req);
startRequest("DELETE", req);
}

if (reply()->error() != QNetworkReply::NoError) {
qCWarning(lcDeleteJob) << " Network error: " << reply()->errorString();
}
AbstractNetworkJob::start();
}

bool DeleteJob::finished()
{
qCInfo(lcDeleteJob) << "DELETE of" << reply()->request().url() << "FINISHED WITH STATUS"
<< replyStatusString();

emit finishedSignal();
return true;
}

QByteArray DeleteJob::folderToken() const
Expand Down
6 changes: 1 addition & 5 deletions src/libsync/deletejob.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,18 @@ namespace OCC {
* @brief The DeleteJob class
* @ingroup libsync
*/
class DeleteJob : public AbstractNetworkJob
class DeleteJob : public SimpleFileJob
{
Q_OBJECT
public:
explicit DeleteJob(AccountPtr account, const QString &path, QObject *parent = nullptr);
explicit DeleteJob(AccountPtr account, const QUrl &url, QObject *parent = nullptr);

void start() override;
bool finished() override;

QByteArray folderToken() const;
void setFolderToken(const QByteArray &folderToken);

signals:
void finishedSignal();

private:
QUrl _url; // Only used if the constructor taking a url is taken.
QByteArray _folderToken;
Expand Down
42 changes: 36 additions & 6 deletions src/libsync/networkjobs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Q_LOGGING_CATEGORY(lcMkColJob, "nextcloud.sync.networkjob.mkcol", QtInfoMsg)
Q_LOGGING_CATEGORY(lcProppatchJob, "nextcloud.sync.networkjob.proppatch", QtInfoMsg)
Q_LOGGING_CATEGORY(lcJsonApiJob, "nextcloud.sync.networkjob.jsonapi", QtInfoMsg)
Q_LOGGING_CATEGORY(lcDetermineAuthTypeJob, "nextcloud.sync.networkjob.determineauthtype", QtInfoMsg)
Q_LOGGING_CATEGORY(lcSimpleFileJob, "nextcloud.sync.networkjob.simplefilejob", QtInfoMsg)
const int notModifiedStatusCode = 304;

QByteArray parseEtag(const char *header)
Expand Down Expand Up @@ -1084,9 +1085,39 @@ bool SimpleNetworkJob::finished()
return true;
}

SimpleFileJob::SimpleFileJob(AccountPtr account, const QString &filePath, QObject *parent)
: AbstractNetworkJob(account, filePath, parent)
{
}

QNetworkReply *SimpleFileJob::startRequest(
const QByteArray &verb, const QNetworkRequest req, QIODevice *requestBody)
{
return startRequest(verb, makeDavUrl(path()), req, requestBody);
}

QNetworkReply *SimpleFileJob::startRequest(
const QByteArray &verb, const QUrl &url, const QNetworkRequest req, QIODevice *requestBody)
{
_verb = verb;
const auto reply = sendRequest(verb, url, req, requestBody);

if (reply->error() != QNetworkReply::NoError) {
qCWarning(lcSimpleFileJob) << verb << " Network error: " << reply->errorString();
}
AbstractNetworkJob::start();
return reply;
}

bool SimpleFileJob::finished()
{
qCInfo(lcSimpleFileJob) << _verb << "for" << reply()->request().url() << "FINISHED WITH STATUS" << replyStatusString();
emit finishedSignal(reply());
return true;
}

DeleteApiJob::DeleteApiJob(AccountPtr account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent)
: SimpleFileJob(account, path, parent)
{

}
Expand All @@ -1095,14 +1126,13 @@ void DeleteApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
QUrl url = Utility::concatUrlPath(account()->url(), path());
sendRequest("DELETE", url, req);
AbstractNetworkJob::start();

startRequest("DELETE", req);
}

bool DeleteApiJob::finished()
{
qCInfo(lcJsonApiJob) << "JsonApiJob of" << reply()->request().url() << "FINISHED WITH STATUS"
qCInfo(lcJsonApiJob) << "DeleteApiJob of" << reply()->request().url() << "FINISHED WITH STATUS"
<< reply()->error()
<< (reply()->error() == QNetworkReply::NoError ? QLatin1String("") : errorString());

Expand All @@ -1118,7 +1148,7 @@ bool DeleteApiJob::finished()
const auto replyData = QString::fromUtf8(reply()->readAll());
qCInfo(lcJsonApiJob()) << "TMX Delete Job" << replyData;
emit result(httpStatus);
return true;
return SimpleFileJob::finished();
}

void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath,
Expand Down
27 changes: 26 additions & 1 deletion src/libsync/networkjobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,39 @@ private slots:
bool finished() override;
};

/**
* @brief A basic file manipulation job
* @ingroup libsync
*/
class OWNCLOUDSYNC_EXPORT SimpleFileJob : public AbstractNetworkJob
{
Q_OBJECT
public:
explicit SimpleFileJob(AccountPtr account, const QString &filePath, QObject *parent = nullptr);

QNetworkReply *startRequest(
const QByteArray &verb, const QNetworkRequest req = QNetworkRequest(), QIODevice *requestBody = nullptr);

QNetworkReply *startRequest(const QByteArray &verb, const QUrl &url, const QNetworkRequest req = QNetworkRequest(),
QIODevice *requestBody = nullptr);

signals:
void finishedSignal(QNetworkReply *reply);
protected slots:
bool finished() override;

private:
QByteArray _verb;
};

/**
* @brief sends a DELETE http request to a url.
*
* See Nextcloud API usage for the possible DELETE requests.
*
* This does *not* delete files, it does a http request.
*/
class OWNCLOUDSYNC_EXPORT DeleteApiJob : public AbstractNetworkJob
class OWNCLOUDSYNC_EXPORT DeleteApiJob : public SimpleFileJob
{
Q_OBJECT
public:
Expand Down
50 changes: 47 additions & 3 deletions src/libsync/propagatedownload.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
#include "common/utility.h"
#include "filesystem.h"
#include "propagatorjobs.h"
#include <common/checksums.h>
#include <common/asserts.h>
#include <common/constants.h>
#include "clientsideencryptionjobs.h"
Expand Down Expand Up @@ -923,8 +922,53 @@ void PropagateDownloadFile::slotGetFinished()
validator->start(_tmpFile.fileName(), checksumHeader);
}

void PropagateDownloadFile::slotChecksumFail(const QString &errMsg)
{
void PropagateDownloadFile::slotChecksumFail(const QString &errMsg,
const QByteArray &calculatedChecksumType, const QByteArray &calculatedChecksum, const ValidateChecksumHeader::FailureReason reason)
{
if (reason == ValidateChecksumHeader::FailureReason::ChecksumMismatch && propagator()->account()->isChecksumRecalculateRequestSupported()) {
const QByteArray calculatedChecksumHeader(calculatedChecksumType + ':' + calculatedChecksum);
const QString fullRemotePathForFile(propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file));
auto *job = new SimpleFileJob(propagator()->account(), fullRemotePathForFile);
QObject::connect(job, &SimpleFileJob::finishedSignal, this,
[this, calculatedChecksumHeader, errMsg](const QNetworkReply *reply) { processChecksumRecalculate(reply, calculatedChecksumHeader, errMsg);
});

qCWarning(lcPropagateDownload) << "Checksum validation has failed for file:" << fullRemotePathForFile
<< " Requesting checksum recalculation on the server...";
QNetworkRequest req;
req.setRawHeader(checksumRecalculateOnServerHeaderC, calculatedChecksumType);
job->startRequest(QByteArrayLiteral("PATCH"), req);
return;
}

checksumValidateFailedAbortDownload(errMsg);
}

void PropagateDownloadFile::processChecksumRecalculate(const QNetworkReply *reply, const QByteArray &originalChecksumHeader, const QString &errorMessage)
{
if (reply->error() != QNetworkReply::NoError) {
qCCritical(lcPropagateDownload) << "Checksum recalculation has failed for file:" << reply->url()
<< " Recalculation request has finished with error:" << reply->errorString();
checksumValidateFailedAbortDownload(errorMessage);
return;
}

const auto newChecksumHeaderFromServer = reply->rawHeader(checkSumHeaderC);
if (newChecksumHeaderFromServer == originalChecksumHeader) {
const auto newChecksumHeaderFromServerSplit = newChecksumHeaderFromServer.split(':');
if (newChecksumHeaderFromServerSplit.size() > 1) {
transmissionChecksumValidated(newChecksumHeaderFromServerSplit.first(), newChecksumHeaderFromServerSplit.last());
return;
}
}

qCCritical(lcPropagateDownload) << "Checksum recalculation has failed for file:" << reply->url() << " "
<< checkSumHeaderC << " received is:" << newChecksumHeaderFromServer;
checksumValidateFailedAbortDownload(errorMessage);
}

void PropagateDownloadFile::checksumValidateFailedAbortDownload(const QString &errMsg)
{
FileSystem::remove(_tmpFile.fileName());
propagator()->_anotherSyncNeeded = true;
done(SyncFileItem::SoftError, errMsg); // tr("The file downloaded with a broken checksum, will be redownloaded."));
Expand Down
6 changes: 5 additions & 1 deletion src/libsync/propagatedownload.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "owncloudpropagator.h"
#include "networkjobs.h"
#include "clientsideencryption.h"
#include <common/checksums.h>

#include <QBuffer>
#include <QFile>
Expand Down Expand Up @@ -235,7 +236,10 @@ private slots:

void abort(PropagatorJob::AbortType abortType) override;
void slotDownloadProgress(qint64, qint64);
void slotChecksumFail(const QString &errMsg);
void slotChecksumFail(const QString &errMsg, const QByteArray &calculatedChecksumType,
const QByteArray &calculatedChecksum, const ValidateChecksumHeader::FailureReason reason);
void processChecksumRecalculate(const QNetworkReply *reply, const QByteArray &originalChecksumHeader, const QString &errorMessage);
void checksumValidateFailedAbortDownload(const QString &errMsg);

private:
void startAfterIsEncryptedIsChecked();
Expand Down
Loading