Skip to content
Open
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
67 changes: 58 additions & 9 deletions src/libsync/discovery.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2018 ownCloud GmbH
Expand Down Expand Up @@ -29,9 +29,8 @@
namespace
{
constexpr const char *editorNamesForDelayedUpload[] = {"PowerPDF"};
constexpr const char *fileExtensionsToCheckIfOpenForSigning[] = {".pdf"};

Check warning on line 32 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrd&open=AZ-kFEDXwDoFqFPrMyrd&pullRequest=9777
constexpr auto delayIntervalForSyncRetryForOpenedForSigningFilesSeconds = 60;

Check warning on line 33 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyre&open=AZ-kFEDXwDoFqFPrMyre&pullRequest=9777
constexpr auto delayIntervalForSyncRetryForFilesExceedQuotaSeconds = 60;
}

namespace OCC {
Expand Down Expand Up @@ -61,7 +60,7 @@
computePinState(parent->_pinState);
}

ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, const SyncFileItemPtr &parentDirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent)

Check warning on line 63 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function has 8 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrf&open=AZ-kFEDXwDoFqFPrMyrf&pullRequest=9777

Check warning on line 63 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "data" of type "class OCC::DiscoveryPhase *" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrg&open=AZ-kFEDXwDoFqFPrMyrg&pullRequest=9777

Check warning on line 63 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "lastSyncTimestamp" of type "long long" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrj&open=AZ-kFEDXwDoFqFPrMyrj&pullRequest=9777
: QObject(parent)
, _dirItem(dirItem)
, _dirParentItem(parentDirItem)
Expand Down Expand Up @@ -252,7 +251,7 @@
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
}

bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &entries, const std::map<QString, Entries> &allEntries, const bool isHidden, const bool isBlacklisted)

Check failure on line 254 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 108 to the 25 allowed.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrn&open=AZ-kFEDXwDoFqFPrMyrn&pullRequest=9777
{
const auto isDirectory = entries.localEntry.isDirectory || entries.serverEntry.isDirectory;

Expand All @@ -276,7 +275,7 @@
|| excluded == CSYNC_FILE_EXCLUDE_TRAILING_SPACE
|| excluded == CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE;

const auto leadingAndTrailingSpacesFilesAllowed = !_discoveryData->_shouldEnforceWindowsFileNameCompatibility || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(_discoveryData->_localDir + path);

Check warning on line 278 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrp&open=AZ-kFEDXwDoFqFPrMyrp&pullRequest=9777
#if defined Q_OS_WINDOWS
if (hasLeadingOrTrailingSpaces && leadingAndTrailingSpacesFilesAllowed) {
#else
Expand Down Expand Up @@ -693,7 +692,7 @@
_pendingAsyncJobs++;
_discoveryData->checkSelectiveSyncNewFolder(path._server,
serverEntry.remotePerm,
[=, this](bool result) {

Check failure on line 695 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly capture the required scope variables.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrw&open=AZ-kFEDXwDoFqFPrMyrw&pullRequest=9777

Check warning on line 695 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "result" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyry&open=AZ-kFEDXwDoFqFPrMyry&pullRequest=9777
--_pendingAsyncJobs;
if (!result) {
processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
Expand Down Expand Up @@ -753,7 +752,7 @@
if (item->_e2eEncryptionStatusRemote != SyncFileItem::EncryptionStatus::NotEncrypted) {
Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted);
}
item->_encryptedFileName = [=, this] {

Check warning on line 755 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Capture variables by reference, it is safe in this context.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrz&open=AZ-kFEDXwDoFqFPrMyrz&pullRequest=9777
auto result = QString{};

if (item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::NotEncrypted) {
Expand Down Expand Up @@ -1079,7 +1078,7 @@
// we need to make a request to the server to know that the original file is deleted on the server
_pendingAsyncJobs++;
const auto job = new RequestEtagJob(_discoveryData->_account, _discoveryData->_remoteFolder + originalPath, this);
connect(job, &RequestEtagJob::finishedWithResult, this, [=, this](const HttpResult<QByteArray> &etag) mutable {

Check failure on line 1081 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly capture the required scope variables.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyr0&open=AZ-kFEDXwDoFqFPrMyr0&pullRequest=9777
_pendingAsyncJobs--;
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
if (etag || etag.error().code != 404 ||
Expand Down Expand Up @@ -1143,27 +1142,55 @@
return unlimitedFreeSpace;
}

// Helper: subtract _quotaBytesReserved from a known quota value (>= 0),
// clamping to 0. Negative sentinels (-1 = new unscanned folder,
// -2 = unknown, -3 = unlimited) are passed through unchanged so the
// caller's "folderQuota > -1" guard correctly skips them.
auto adjustForReserved = [this](const int64_t raw) {
return raw >= 0 ? std::max<int64_t>(0, raw - _quotaBytesReserved) : raw;
};

if (serverEntry == FolderQuota::ServerEntry::Valid) {
qCDebug(lcDisco) << "Returning cached _folderQuota.bytesAvailable for item quota.";
return _folderQuota.bytesAvailable;
return adjustForReserved(_folderQuota.bytesAvailable);
}

// serverEntry == Invalid: the file is new locally and has no server-side
// counterpart yet. Fall through to the DB / _dirItem fallback so the
// quota value stored during the previous PROPFIND cycle is used. If quota
// remains unknown there too, folderBytesAvailable() returns unlimitedFreeSpace
// and the upload is allowed; the propagator's reactive HTTP-507 path catches
// the failure and blacklists the item when quota is unavailable upfront.

if (!_dirItem) {
qCDebug(lcDisco) << "Returning unlimited free space (-3) for item quota with no _dirItem.";
return unlimitedFreeSpace;
}

qCDebug(lcDisco) << "_dirItem->_folderQuota.bytesAvailable:" << _dirItem->_folderQuota.bytesAvailable;

// Priority 1: fresh value from the current PROPFIND cycle stored in _dirItem.
// _dirItem is the SyncFileItem for the parent folder (e.g. "A"), whose
// _folderQuota was populated by processFileAnalyzeRemoteInfo() when "A"
// was itself processed — this happens before any child of "A" is finalized.
if (_dirItem->_folderQuota.bytesAvailable != -1) {
qCDebug(lcDisco) << "Returning _dirItem->_folderQuota.bytesAvailable for item quota (fresh PROPFIND value).";
return adjustForReserved(_dirItem->_folderQuota.bytesAvailable);
}

// Priority 2: value persisted from the previous sync cycle in the journal DB.
SyncJournalFileRecord dirItemDbRecord;
if (_discoveryData->_statedb->getFileRecord(_dirItem->_file, &dirItemDbRecord) && dirItemDbRecord.isValid()) {

Check warning on line 1183 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "dirItemDbRecord" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyr2&open=AZ-kFEDXwDoFqFPrMyr2&pullRequest=9777
const auto dirDbBytesAvailable = dirItemDbRecord._folderQuota.bytesAvailable;
qCDebug(lcDisco) << "Returning for item quota db value dirItemDbRecord._folderQuota.bytesAvailable" << dirDbBytesAvailable;
return dirDbBytesAvailable;
if (dirDbBytesAvailable != -1) {
qCDebug(lcDisco) << "Returning for item quota db value dirItemDbRecord._folderQuota.bytesAvailable" << dirDbBytesAvailable;
return adjustForReserved(dirDbBytesAvailable);
}
}

qCDebug(lcDisco) << "Returning _dirItem->_folderQuota.bytesAvailable for item quota.";
return _dirItem->_folderQuota.bytesAvailable;
// Priority 3: quota unknown, allow the upload; reactive HTTP-507 will catch it.
qCDebug(lcDisco) << "Returning unlimited free space (-3) for item quota: quota unknown from both dirItem and DB.";
return unlimitedFreeSpace;
}

void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
Expand Down Expand Up @@ -1259,9 +1286,22 @@
_currentFolder._server);
}

item->_status = SyncFileItem::Status::NormalError;
_discoveryData->_anotherSyncNeeded = true;
_discoveryData->_filesNeedingScheduledSync.insert(path._original, delayIntervalForSyncRetryForFilesExceedQuotaSeconds);
// Use DetailError so the error appears in the issues tab without a
// prominent pop-up, matching the reactive HTTP 507 path.
// Do NOT set _anotherSyncNeeded or insert into _filesNeedingScheduledSync:
// those were the source of the 60-second retry loop. The discovery check
// re-fires on every normal sync cycle, so the upload remains blocked
// without wasting any network bandwidth.
item->_status = SyncFileItem::Status::DetailError;
} else if (item->_direction == SyncFileItem::Up
&& !item->isDirectory()
&& (item->_instruction == CSYNC_INSTRUCTION_NEW
|| item->_instruction == CSYNC_INSTRUCTION_SYNC)) {
// Upload approved: reserve these bytes so subsequent files processed
// in the same discovery pass see the reduced available quota. Without
// this, two files that individually fit but together exceed the quota
// would both be approved.
_quotaBytesReserved += item->_size;
}

if (item->_type != CSyncEnums::ItemTypeVirtualFile) {
Expand Down Expand Up @@ -1622,7 +1662,7 @@
item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion());
if (item->_e2eEncryptionStatus != item->_e2eEncryptionServerCapability) {
item->_e2eEncryptionStatus = item->_e2eEncryptionServerCapability;
if (base._e2eEncryptionStatus != SyncJournalFileRecord::EncryptionStatus::NotEncrypted) {

Check failure on line 1665 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyr4&open=AZ-kFEDXwDoFqFPrMyr4&pullRequest=9777
Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted);
}
}
Expand All @@ -1638,7 +1678,7 @@
const auto serverHasMountRootProperty = _discoveryData->_account->serverHasMountRootProperty();
const auto isExternalStorage = base._remotePerm.hasPermission(RemotePermissions::IsMounted) && base.isDirectory();
const auto movePerms = checkMovePermissions(base._remotePerm, originalPath, item->isDirectory());
if (!movePerms.sourceOk || !movePerms.destinationOk || (serverHasMountRootProperty && isExternalStorage) || isE2eeMoveOnlineOnlyItemWithCfApi) {

Check warning on line 1681 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "movePerms" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyr3&open=AZ-kFEDXwDoFqFPrMyr3&pullRequest=9777
qCInfo(lcDisco) << "Move without permission to rename base file, "
<< "source:" << movePerms.sourceOk
<< ", target:" << movePerms.destinationOk
Expand Down Expand Up @@ -1723,7 +1763,7 @@
if (base.isVirtualFile() && isVfsWithSuffix())
chopVirtualFileSuffix(serverOriginalPath);
auto job = new RequestEtagJob(_discoveryData->_account, serverOriginalPath, this);
connect(job, &RequestEtagJob::finishedWithResult, this, [=, this](const HttpResult<QByteArray> &etag) mutable {

Check failure on line 1766 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly capture all local variables required in this lambda.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysA&open=AZ-kFEDXwDoFqFPrMysA&pullRequest=9777


if (!etag || (etag.get() != base._etag && !item->isDirectory()) || _discoveryData->isRenamed(originalPath)
Expand Down Expand Up @@ -1970,6 +2010,15 @@
auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer,
_lastSyncTimestamp, this);
job->setInsideEncryptedTree(isInsideEncryptedTree() || item->isEncrypted());
// Propagate the parent folder's quota into the child job so that
// new-local-only files (serverEntry invalid, no DB record yet) can
// read a valid quota via _folderQuota in folderBytesAvailable().
if (item->_folderQuota.bytesAvailable != -1) {
OCC::FolderQuota folderQuota;
folderQuota.bytesUsed = item->_folderQuota.bytesUsed;
folderQuota.bytesAvailable = item->_folderQuota.bytesAvailable;
job->setFolderQuota(folderQuota);
}
if (removed) {
job->setParent(_discoveryData);
_discoveryData->_deletedItem[path._original] = item;
Expand Down Expand Up @@ -2145,8 +2194,8 @@
return matchingEditorsKeepingFileBusy;
}

const auto isMatchingFileExtension = std::find_if(std::cbegin(fileExtensionsToCheckIfOpenForSigning), std::cend(fileExtensionsToCheckIfOpenForSigning),
[path](const auto &matchingExtension) {

Check warning on line 2198 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Capture variables by reference, it is safe in this context.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysG&open=AZ-kFEDXwDoFqFPrMysG&pullRequest=9777
return path._local.endsWith(matchingExtension, Qt::CaseInsensitive);
}) != std::cend(fileExtensionsToCheckIfOpenForSigning);

Check warning on line 2200 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::find_if" that takes a range.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysF&open=AZ-kFEDXwDoFqFPrMysF&pullRequest=9777

Expand All @@ -2158,8 +2207,8 @@
const auto editorsKeepingFileBusy = Utility::queryProcessInfosKeepingFileOpen(fullLocalPath);

for (const auto &detectedEditorName : editorsKeepingFileBusy) {
const auto isMatchingEditorFound = std::find_if(std::cbegin(editorNamesForDelayedUpload), std::cend(editorNamesForDelayedUpload),
[detectedEditorName](const auto &matchingEditorName) {

Check warning on line 2211 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Capture variables by reference, it is safe in this context.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysI&open=AZ-kFEDXwDoFqFPrMysI&pullRequest=9777
return detectedEditorName.processName.startsWith(matchingEditorName, Qt::CaseInsensitive);
}) != std::cend(editorNamesForDelayedUpload);

Check warning on line 2213 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::find_if" that takes a range.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysH&open=AZ-kFEDXwDoFqFPrMysH&pullRequest=9777
if (isMatchingEditorFound) {
Expand Down Expand Up @@ -2219,7 +2268,7 @@
if (job->_dirItem)
emit _discoveryData->itemDiscovered(job->_dirItem);

int count = _runningJobs.removeAll(job);

Check warning on line 2271 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

implicit conversion loses integer precision: 'qsizetype' (aka 'long long') to 'int'

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMyrZ&open=AZ-kFEDXwDoFqFPrMyrZ&pullRequest=9777
ASSERT(count == 1);
job->deleteLater();
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
Expand Down Expand Up @@ -2298,7 +2347,7 @@
}
}

DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()

Check failure on line 2350 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 27 to the 25 allowed.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysJ&open=AZ-kFEDXwDoFqFPrMysJ&pullRequest=9777
{
if (_dirItem && _dirItem->isEncrypted() && _dirItem->_encryptedFileName.isEmpty()) {
_discoveryData->_topLevelE2eeFolderPaths.insert(_discoveryData->_remoteFolder + _dirItem->_file);
Expand Down Expand Up @@ -2481,13 +2530,13 @@
}
}

bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &absoluteFileName,

Check warning on line 2533 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysM&open=AZ-kFEDXwDoFqFPrMysM&pullRequest=9777
CSYNC_EXCLUDE_TYPE excludeReason)

Check warning on line 2534 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "excludeReason" of type "enum CSYNC_EXCLUDE_TYPE" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysO&open=AZ-kFEDXwDoFqFPrMysO&pullRequest=9777
{
auto result = true;

const auto leadingAndTrailingSpacesFilesAllowed = !_discoveryData->_shouldEnforceWindowsFileNameCompatibility || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(absoluteFileName);
if (leadingAndTrailingSpacesFilesAllowed) {

Check warning on line 2539 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "leadingAndTrailingSpacesFilesAllowed" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysL&open=AZ-kFEDXwDoFqFPrMysL&pullRequest=9777
return result;
}

Expand All @@ -2510,7 +2559,7 @@
break;
case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE:
case CSYNC_FILE_EXCLUDE_LEADING_SPACE:
case CSYNC_FILE_EXCLUDE_TRAILING_SPACE:

Check warning on line 2562 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce this switch case number of lines from 14 to at most 5, for example by extracting code into methods.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEDXwDoFqFPrMysP&open=AZ-kFEDXwDoFqFPrMysP&pullRequest=9777
{
const auto removeTrailingSpaces = [] (QString string) -> QString {
for (int n = string.size() - 1; n >= 0; -- n) {
Expand Down
5 changes: 5 additions & 0 deletions src/libsync/discovery.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#pragma once

#include <QObject>

Check failure on line 9 in src/libsync/discovery.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/discovery.h:9:10 [clang-diagnostic-error]

'QObject' file not found
#include <cstdint>
#include "csync_exclude.h"
#include "discoveryphase.h"
Expand Down Expand Up @@ -249,7 +249,7 @@
*/
void setupDbPinStateActions(SyncJournalFileRecord &record);

bool maybeRenameForWindowsCompatibility(const QString &absoluteFileName,

Check warning on line 252 in src/libsync/discovery.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEHswDoFqFPrMysU&open=AZ-kFEHswDoFqFPrMysU&pullRequest=9777
CSYNC_EXCLUDE_TYPE excludeReason);

[[nodiscard]] bool checkNewDeleteConflict(const SyncFileItemPtr &item) const;
Expand Down Expand Up @@ -300,6 +300,11 @@
bool _isInsideEncryptedTree = false; // this directory is encrypted or is within the tree of directories with root directory encrypted

FolderQuota _folderQuota;
// Running total of bytes already reserved for approved uploads in this
// directory during the current discovery pass. Subtracted from the quota
// returned by folderBytesAvailable() so that two files which individually
// fit but together exceed quota cannot both be approved in one pass.
qint64 _quotaBytesReserved = 0;

int64_t folderBytesAvailable(const SyncFileItemPtr &item, const FolderQuota::ServerEntry serverEntry) const;

Expand Down
41 changes: 35 additions & 6 deletions src/libsync/propagateupload.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
* SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "config.h"

Check failure on line 7 in src/libsync/propagateupload.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/propagateupload.cpp:7:10 [clang-diagnostic-error]

'config.h' file not found
#include "propagateupload.h"
#include "propagateuploadencrypted.h"
#include "owncloudpropagator_p.h"
Expand Down Expand Up @@ -72,7 +72,7 @@
qCWarning(lcPutJob) << " Network error: " << reply()->errorString();
}

connect(reply(), &QNetworkReply::uploadProgress, this, [requestID] (qint64 bytesSent, qint64 bytesTotal) {

Check warning on line 75 in src/libsync/propagateupload.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "bytesSent" of type "long long" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEJNwDoFqFPrMysZ&open=AZ-kFEJNwDoFqFPrMysZ&pullRequest=9777
qCDebug(lcPutJob()) << requestID << "upload progress" << bytesSent << bytesTotal;
});

Expand Down Expand Up @@ -160,7 +160,7 @@
_item->_fileId = json["fileId"].toString().toUtf8();

if (SyncJournalFileRecord oldRecord; _journal->getFileRecord(_item->destination(), &oldRecord) && oldRecord.isValid()) {
if (oldRecord._etag != _item->_etag) {

Check warning on line 163 in src/libsync/propagateupload.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this "if" statement with the enclosing one.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEJNwDoFqFPrMysc&open=AZ-kFEJNwDoFqFPrMysc&pullRequest=9777
_item->updateLockStateFromDbRecord(oldRecord);
}
}
Expand Down Expand Up @@ -282,14 +282,42 @@
return;
}

// Check if we believe that the upload will fail due to remote quota limits
const qint64 quotaGuess = propagator()->_folderQuota.value(
QFileInfo(_fileToUpload._file).path(), std::numeric_limits<qint64>::max());
// Check if we believe that the upload will fail due to remote quota limits.
// _folderQuota is seeded at the start of each sync cycle from the quota data
// returned by PROPFIND during discovery (see SyncEngine::finishSync).
// It is also tightened reactively whenever the server returns HTTP 507.
//
// A PROPFIND quota entry applies to a directory and all its descendants.
// Walk up the path hierarchy to find the nearest quota entry, so that a
// quota set on "A" also guards uploads into "A/B/C/".
qint64 quotaGuess = std::numeric_limits<qint64>::max();
QString lookupPath = QFileInfo(_fileToUpload._file).path();
while (!lookupPath.isEmpty()) {
if (propagator()->_folderQuota.contains(lookupPath)) {
quotaGuess = propagator()->_folderQuota.value(lookupPath);
break;
}
if (lookupPath == QLatin1String(".")) {
lookupPath.clear(); // reached sync root with no quota entry; terminates the loop
} else {
const auto slash = lookupPath.lastIndexOf(QLatin1Char('/'));
lookupPath = slash >= 0 ? lookupPath.left(slash) : QStringLiteral(".");
}
}
if (_fileToUpload._size > quotaGuess) {
// Necessary for blacklisting logic
// quotaGuess is never std::numeric_limits<qint64>::max() here: reaching
// this branch requires _size > quotaGuess, which is impossible when
// quotaGuess == max(). So Utility::octetsToString(quotaGuess) in the
// message below always formats a real quota value, never "max".
//
// Set httpErrorCode so blacklistUpdate creates an InsufficientRemoteStorage
// entry, which suppresses automatic retries until quota becomes sufficient.
_item->_httpErrorCode = 507;
emit propagator()->insufficientRemoteStorage();
done(SyncFileItem::DetailError, tr("Upload of %1 exceeds the quota for the folder").arg(Utility::octetsToString(_fileToUpload._size)));
done(SyncFileItem::DetailError,
tr("Upload of %1 exceeds %2 of remaining storage quota for this folder")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tr("Upload of %1 exceeds %2 of remaining storage quota for this folder")
tr("Upload of %1 exceeds %2 of remaining storage quota for this folder.")

.arg(Utility::octetsToString(_fileToUpload._size),
Utility::octetsToString(quotaGuess)));
return;
}

Expand Down Expand Up @@ -696,8 +724,8 @@
// _item when the file was discovered via local (not remote) discovery, so
// check the DB record directly rather than guarding on _item->_lockToken.
SyncJournalFileRecord record;
if (propagator()->_journal->getFileRecord(_item->_file, &record) && record.isValid()
&& !record._lockstate._lockToken.isEmpty()) {

Check warning on line 728 in src/libsync/propagateupload.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "record" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEJNwDoFqFPrMysd&open=AZ-kFEJNwDoFqFPrMysd&pullRequest=9777
record._lockstate._lockToken.clear();
record._lockstate._locked = false;
if (const auto result = propagator()->_journal->setFileRecord(record); !result) {
Expand Down Expand Up @@ -734,7 +762,8 @@

// Set up the error
status = SyncFileItem::DetailError;
errorString = tr("Upload of %1 exceeds the quota for the folder").arg(Utility::octetsToString(_fileToUpload._size));
errorString = tr("Upload of %1 exceeds the remaining storage quota for this folder")
.arg(Utility::octetsToString(_fileToUpload._size));
emit propagator()->insufficientRemoteStorage();
} else if (_item->_httpErrorCode == 400) {
const auto exception = job->errorStringParsingBodyException(replyContent);
Expand All @@ -755,7 +784,7 @@

job->setTimeout(qBound(
// Calculate 3 minutes for each gigabyte of data
qMin(thirtyMinutes - 1, qRound64(threeMinutes * fileSize / 1e9)),

Check warning on line 787 in src/libsync/propagateupload.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

implicit conversion from 'qint64' (aka 'long long') to 'double' may lose precision

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEJNwDoFqFPrMysV&open=AZ-kFEJNwDoFqFPrMysV&pullRequest=9777
job->timeoutMsec(),
// Maximum of 30 minutes
thirtyMinutes));
Expand Down
32 changes: 31 additions & 1 deletion src/libsync/syncengine.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
Expand Down Expand Up @@ -241,7 +241,7 @@

// Delete the stales chunk on the server.
if (account()->capabilities().chunkingNg()) {
for (uint transferId : std::as_const(ids)) {

Check warning on line 244 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "transferId" of type "unsigned int" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq1&open=AZ-kFDwAwDoFqFPrMyq1&pullRequest=9777
if (!transferId)
continue; // Was not a chunked upload
QUrl url = Utility::concatUrlPath(account()->url(), QLatin1String("remote.php/dav/uploads/") + account()->davUser() + QLatin1Char('/') + QString::number(transferId));
Expand Down Expand Up @@ -397,7 +397,7 @@

// Update on-disk virtual file metadata
if (modificationHappened && item->_type == ItemTypeVirtualFile) {
auto r = _syncOptions._vfs->updateMetadata(*item, filePath, {});

Check warning on line 400 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "r" of type "class OCC::Result<enum OCC::Vfs::ConvertToPlaceholderResult, class QString>" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq3&open=AZ-kFDwAwDoFqFPrMyq3&pullRequest=9777
if (!r) {
item->_status = SyncFileItem::Status::NormalError;
item->_instruction = CSYNC_INSTRUCTION_ERROR;
Expand Down Expand Up @@ -474,7 +474,7 @@
_needsUpdate = true;

// Insert sorted
auto it = std::lower_bound( _syncItems.begin(), _syncItems.end(), item ); // the _syncItems is sorted

Check warning on line 477 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::lower_bound" that takes a range.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq4&open=AZ-kFDwAwDoFqFPrMyq4&pullRequest=9777
_syncItems.insert( it, item );

slotNewItem(item);
Expand All @@ -501,15 +501,15 @@
const auto e2EeLockedFolders = _journal->e2EeLockedFolders();

if (!e2EeLockedFolders.isEmpty()) {
for (const auto &e2EeLockedFolder : e2EeLockedFolders) {

Check warning on line 504 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this declaration by a structured binding declaration.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq7&open=AZ-kFDwAwDoFqFPrMyq7&pullRequest=9777
const auto folderId = e2EeLockedFolder.first;

Check warning on line 505 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid this unnecessary copy by using a "const" reference.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq8&open=AZ-kFDwAwDoFqFPrMyq8&pullRequest=9777
qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId;
const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), e2EeLockedFolder.second);
if (!folderToken) {
qCWarning(lcEngine()) << "decrypt failed";
return;
}
// TODO: We need to rollback changes done to metadata in case we have an active lock, this needs to be implemented on the server first

Check warning on line 512 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq0&open=AZ-kFDwAwDoFqFPrMyq0&pullRequest=9777
const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this);
unlockJob->setShouldRollbackMetadataChanges(true);
unlockJob->start();
Expand Down Expand Up @@ -638,7 +638,7 @@
_discoveryPhase->_account = _account;
_discoveryPhase->_excludes = _excludedFiles.data();
const QString excludeFilePath = _localPath + QStringLiteral(".sync-exclude.lst");
if (FileSystem::fileExists(excludeFilePath)) {

Check warning on line 641 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "excludeFilePath" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq5&open=AZ-kFDwAwDoFqFPrMyq5&pullRequest=9777
_discoveryPhase->_excludes->addExcludeFilePath(excludeFilePath);
_discoveryPhase->_excludes->reloadExcludeFiles();
}
Expand Down Expand Up @@ -721,7 +721,7 @@
const auto databaseFingerprint = _journal->dataFingerprint();
_discoveryPhase->_dataFingerprint = databaseFingerprint;
ProcessDirectoryJob::PathTuple path = {};
path._local = path._original = path._server = path._target = singleItemDiscoveryOptions().discoveryPath;

Check warning on line 724 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment to "_original" from this expression.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq-&open=AZ-kFDwAwDoFqFPrMyq-&pullRequest=9777

Check warning on line 724 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment to "_target" from this expression.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrA&open=AZ-kFDwAwDoFqFPrMyrA&pullRequest=9777

Check warning on line 724 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment to "_server" from this expression.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyq_&open=AZ-kFDwAwDoFqFPrMyq_&pullRequest=9777

SyncJournalFileRecord rec;
const auto localQueryMode = _journal->getFileRecord(singleItemDiscoveryOptions().discoveryDirItem->_file, &rec) && rec.isValid()
Expand Down Expand Up @@ -917,7 +917,7 @@
detectFileLock(item);
}

void SyncEngine::slotPropagationFinished(OCC::SyncFileItem::Status status)

Check warning on line 920 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "status" of type "enum OCC::SyncFileItem::Status" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrC&open=AZ-kFDwAwDoFqFPrMyrC&pullRequest=9777
{
if (_propagator->_anotherSyncNeeded && _anotherSyncNeeded == NoFollowUpSync) {
_anotherSyncNeeded = ImmediateFollowUp;
Expand Down Expand Up @@ -1042,6 +1042,31 @@
}
}

void SyncEngine::seedPropagatorQuota()
{
// Seed the propagator's per-folder quota cache from the quota data gathered
// during the discovery phase (PROPFIND responses). The propagator is recreated
// fresh every sync cycle, so without this seeding the pre-upload quota check in
// PropagateUploadFileCommon::startUploadFile() would always pass on the first
// attempt of a new cycle, causing a full upload that the server then rejects with
// HTTP 507 and wasting bandwidth. By initialising the cache here, the pre-upload
// check can block oversized files from the very first attempt of each cycle.
for (const auto &syncItem : std::as_const(_syncItems)) {
if (!syncItem->isDirectory() || syncItem->_folderQuota.bytesAvailable < 0) {
continue;
}
// OwncloudPropagator keys _folderQuota by QFileInfo::path() of the file
// being uploaded, which yields "." for items at the root of the sync
// folder. Normalise the empty-string root path accordingly.
const auto key = syncItem->_file.isEmpty() ? QStringLiteral(".") : syncItem->_file;
// Only insert when there is no tighter bound already present (e.g. set
// from a prior HTTP 507 reply within the same cycle).
if (!_propagator->_folderQuota.contains(key)) {
_propagator->_folderQuota.insert(key, syncItem->_folderQuota.bytesAvailable);
}
}
}

void SyncEngine::finishSync()
{
auto databaseFingerprint = _journal->dataFingerprint();
Expand Down Expand Up @@ -1094,6 +1119,10 @@
_propagator = QSharedPointer<OwncloudPropagator>(
new OwncloudPropagator(_account, _localPath, _remotePath, _journal, _bulkUploadBlackList));
_propagator->setSyncOptions(_syncOptions);

// Must be called before _propagator->start(std::move(_syncItems)) below,
// because start() transfers ownership of _syncItems and leaves it empty.
seedPropagatorQuota();
connect(_propagator.data(), &OwncloudPropagator::itemCompleted,
this, &SyncEngine::slotItemCompleted);
connect(_propagator.data(), &OwncloudPropagator::progress,
Expand Down Expand Up @@ -1167,7 +1196,7 @@
++deletionCounter;
}
});
if (!result) {

Check failure on line 1199 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrI&open=AZ-kFDwAwDoFqFPrMyrI&pullRequest=9777
qCDebug(lcEngine()) << "unable to find the number of files within a deleted folder:" << oneItem->_file;
}
} else {
Expand All @@ -1177,7 +1206,7 @@
}
const auto filesDeletedThresholdExceeded = deletionCounter > ConfigFile().deleteFilesThreshold();

if ((allFilesDeleted || filesDeletedThresholdExceeded) && displayDialog) {

Check warning on line 1209 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the init-statement to declare "filesDeletedThresholdExceeded" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrF&open=AZ-kFDwAwDoFqFPrMyrF&pullRequest=9777
qCWarning(lcEngine) << "Many files are going to be deleted, asking the user";
int side = 0; // > 0 means more deleted on the server. < 0 means more deleted on the client
for (const auto &it : std::as_const(_syncItems)) {
Expand Down Expand Up @@ -1228,7 +1257,7 @@
{
QPointer<QObject> guard = new QObject();
QPointer<QObject> self = this;
auto callback = [this, self, guard](bool cancel) -> void {

Check warning on line 1260 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "cancel" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrM&open=AZ-kFDwAwDoFqFPrMyrM&pullRequest=9777
// use a guard to ensure its only called once...
// qpointer to self to ensure we still exist
if (!guard || !self) {
Expand Down Expand Up @@ -1315,14 +1344,14 @@
// This invariant is used in SyncEngine::shouldDiscoverLocally
QString prev;
auto it = _localDiscoveryPaths.begin();
while(it != _localDiscoveryPaths.end()) {
if (!prev.isNull() && it->startsWith(prev) && (prev.endsWith('/') || *it == prev || it->at(prev.size()) <= '/')) {
it = _localDiscoveryPaths.erase(it);
} else {
prev = *it;
++it;
}
}

Check warning on line 1354 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this loop with a "std::erase_if" call.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrP&open=AZ-kFDwAwDoFqFPrMyrP&pullRequest=9777
}

void SyncEngine::setSingleItemDiscoveryOptions(const SingleItemDiscoveryOptions &singleItemDiscoveryOptions)
Expand All @@ -1335,7 +1364,7 @@
return _singleItemDiscoveryOptions;
}

void SyncEngine::setFilesystemPermissionsReliable(bool reliable)

Check warning on line 1367 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrQ&open=AZ-kFDwAwDoFqFPrMyrQ&pullRequest=9777

Check warning on line 1367 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "reliable" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrR&open=AZ-kFDwAwDoFqFPrMyrR&pullRequest=9777
{
_filesystemPermissionsReliable = reliable;
}
Expand Down Expand Up @@ -1486,7 +1515,8 @@

void SyncEngine::slotInsufficientRemoteStorage()
{
auto msg = tr("There is insufficient space available on the server for some uploads.");
auto msg = tr("Upload paused: one or more files exceed your remaining Nextcloud storage quota. "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
auto msg = tr("Upload paused: one or more files exceed your remaining Nextcloud storage quota. "
const auto msg = tr("Current files upload size exceeds your remaining storage quota."

"Free up server space or contact your administrator to increase your quota.");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Free up server space or contact your administrator to increase your quota.");
"Free up server space or contact your administrator to increase your user quota.");

if (_uniqueErrors.contains(msg)) {
return;
}
Expand Down Expand Up @@ -1573,7 +1603,7 @@
it != _discoveryPhase->_filesNeedingScheduledSync.cend();
++it) {

const auto file = it.key();

Check warning on line 1606 in src/libsync/syncengine.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid this unnecessary copy by using a "const" reference.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFDwAwDoFqFPrMyrT&open=AZ-kFDwAwDoFqFPrMyrT&pullRequest=9777
const auto syncScheduledSecs = it.value();

// We don't want to schedule syncs again for files we have already discovered needing a
Expand Down
2 changes: 2 additions & 0 deletions src/libsync/syncengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#pragma once

#include <cstdint>

Check failure on line 9 in src/libsync/syncengine.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/syncengine.h:9:10 [clang-diagnostic-error]

'cstdint' file not found

#include <QMutex>
#include <QThread>
Expand Down Expand Up @@ -125,7 +125,7 @@
[[nodiscard]] QSharedPointer<OwncloudPropagator> getPropagator() const { return _propagator; } // for the test
[[nodiscard]] const SyncEngine::SingleItemDiscoveryOptions &singleItemDiscoveryOptions() const;

void setFilesystemPermissionsReliable(bool reliable);

Check warning on line 128 in src/libsync/syncengine.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEBGwDoFqFPrMyrW&open=AZ-kFEBGwDoFqFPrMyrW&pullRequest=9777

public slots:
void setSingleItemDiscoveryOptions(const OCC::SyncEngine::SingleItemDiscoveryOptions &singleItemDiscoveryOptions);
Expand All @@ -152,7 +152,7 @@
*/
void setLocalDiscoveryOptions(OCC::LocalDiscoveryEnums::LocalDiscoveryStyle style, std::set<QString> paths = {});
void addAcceptedInvalidFileName(const QString& filePath);
void setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value);

Check warning on line 155 in src/libsync/syncengine.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this identifier to be shorter or equal to 31 characters.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ-kFEBGwDoFqFPrMyrX&open=AZ-kFEBGwDoFqFPrMyrX&pullRequest=9777

signals:
// During update, before reconcile
Expand Down Expand Up @@ -363,6 +363,8 @@

void finishSync();

void seedPropagatorQuota();

[[nodiscard]] bool shouldRestartSync() const;

bool handleMassDeletion();
Expand Down
79 changes: 28 additions & 51 deletions test/testlocaldiscovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* any purpose.
*/

#include <QtTest>

Check failure on line 11 in test/testlocaldiscovery.cpp

View workflow job for this annotation

GitHub Actions / build

test/testlocaldiscovery.cpp:11:10 [clang-diagnostic-error]

'QtTest' file not found
#include "syncenginetestutils.h"
#include <syncengine.h>
#include <localdiscoverytracker.h>
Expand Down Expand Up @@ -870,106 +870,83 @@

void testDiscoveryUsesCorrectQuotaSource()
{
//setup sync folder
// Verifies that the discovery-phase quota check uses the FRESH
// PROPFIND value (from the current sync cycle) rather than the
// stale value persisted in the journal DB from a previous cycle.

FakeFolder fakeFolder{FileInfo{}};

// create folder
const QString folderA("A");
fakeFolder.localModifier().mkdir(folderA);
fakeFolder.remoteModifier().mkdir(folderA);
fakeFolder.remoteModifier().setFolderQuota(folderA, {0, 500});

// sync folderA
// Initial sync — folder A is created, DB stores quota = 500
ItemCompletedSpy syncSpy(fakeFolder);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(folderA)->_status, SyncFileItem::Status::NoStatus);

// check db quota for folderA - bytesAvailable is 500
SyncJournalFileRecord recordFolderA;
QVERIFY(fakeFolder.syncJournal().getFileRecord(folderA, &recordFolderA));
QCOMPARE(recordFolderA._folderQuota.bytesAvailable, 500);

// add fileNameA to folderA - size < quota in db
// ── Case 1: upload succeeds when size < fresh PROPFIND quota ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ── Case 1: upload succeeds when size < fresh PROPFIND quota ──
// Case 1: upload succeeds when size < fresh PROPFIND quota

const QString fileNameA("A/A.data");
fakeFolder.localModifier().insert(fileNameA, 200);

// set different quota for folderA - remote change does not change etag yet
fakeFolder.remoteModifier().setFolderQuota(folderA, {0, 0});

// sync filenameA - size == quota => success
syncSpy.clear();
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(fileNameA)->_status, SyncFileItem::Status::Success);

// add smallFile to folderA - size < quota in db
const QString smallFile("A/smallFile.data");
fakeFolder.localModifier().insert(smallFile, 100);
// ── Case 2: fresh PROPFIND value (0) wins over stale DB (500) ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ── Case 2: fresh PROPFIND value (0) wins over stale DB (500) ──
// Case 2: fresh PROPFIND value (0) wins over stale DB (500)

// Reduce quota to 0 without invalidating the etag. The DB still
// holds 500, but the root PROPFIND now returns 0 for folder A.
// A new file must be blocked proactively.
fakeFolder.remoteModifier().setFolderQuota(folderA, {0, 0});

const QString fileBlocked("A/blocked.data");
fakeFolder.localModifier().insert(fileBlocked, 100);

// sync smallFile - size < quota in db => success => update quota in db
syncSpy.clear();
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(smallFile)->_status, SyncFileItem::Status::Success);
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(fileBlocked)->_status, SyncFileItem::Status::DetailError);

// create remoteFileA - size > bytes available
// ── Case 3: downloads are not affected by upload quota ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ── Case 3: downloads are not affected by upload quota ──
// Case 3: downloads are not affected by upload quota

const QString remoteFileA("A/remoteA.data");
fakeFolder.remoteModifier().insert(remoteFileA, 200);

// sync remoteFile - it is a download
syncSpy.clear();
QVERIFY(fakeFolder.syncOnce());
// Sync still fails overall (blocked.data remains blocked), but the
// download of remoteFileA must succeed independently.
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(remoteFileA)->_status, SyncFileItem::Status::Success);
QCOMPARE(syncSpy.findItem(fileBlocked)->_status, SyncFileItem::Status::DetailError);

// check db quota for folderA - bytesAvailable have changed to 0 due to new PROPFIND
QVERIFY(fakeFolder.syncJournal().getFileRecord(folderA, &recordFolderA));
QCOMPARE(recordFolderA._folderQuota.bytesAvailable, 0);

// create local fileNameB - size < quota in db
const QString fileNameB("A/B.data");
fakeFolder.localModifier().insert(fileNameB, 0);

// set different quota for folderA - remote change does not change etag yet
// ── Case 4: after quota increase, previously blocked file uploads ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ── Case 4: after quota increase, previously blocked file uploads ──
// Case 4: after quota increase, previously blocked file uploads

fakeFolder.remoteModifier().setFolderQuota(folderA, {500, 600});

// sync fileNameB - size < quota in db => success
syncSpy.clear();
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(fileNameB)->_status, SyncFileItem::Status::Success);

// create remoteFileB - it is a download
const QString remoteFileB("A/remoteB.data");
fakeFolder.remoteModifier().insert(remoteFileA, 100);
QCOMPARE(syncSpy.findItem(fileBlocked)->_status, SyncFileItem::Status::Success);

// create local fileNameC - size < quota in db
const QString fileNameC("A/C.data");
fakeFolder.localModifier().insert(fileNameC, 0);

// sync filenameC - size < quota in db => success
syncSpy.clear();
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(fileNameC)->_status, SyncFileItem::Status::Success);

// check db quota for folderA - bytesAvailable have changed to 600 due to new PROPFIND
// DB now reflects the fresh PROPFIND value (600)
QVERIFY(fakeFolder.syncJournal().getFileRecord(folderA, &recordFolderA));
QCOMPARE(recordFolderA._folderQuota.bytesAvailable, 600);

QCOMPARE(syncSpy.findItem(remoteFileB)->_status, SyncFileItem::Status::NoStatus);

// create local fileNameD - size > quota in db
// ── Case 5: file exceeding fresh quota is blocked ──
const QString fileNameD("A/D.data");
fakeFolder.localModifier().insert(fileNameD, 700);

// sync fileNameD - size > quota in db => error
syncSpy.clear();
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(syncSpy.findItem(fileNameD)->_status, SyncFileItem::Status::NormalError);
QCOMPARE(syncSpy.findItem(fileNameD)->_status, SyncFileItem::Status::DetailError);

// create local fileNameE - size < quota in db
// ── Case 6: file within quota succeeds even while another is blocked ──

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ── Case 6: file within quota succeeds even while another is blocked ──
// Case 6: file within quota succeeds even while another is blocked

const QString fileNameE("A/E.data");
fakeFolder.localModifier().insert(fileNameE, 400);

// sync fileNameE - size < quota in db => success
syncSpy.clear();
QVERIFY(!fakeFolder.syncOnce());
QVERIFY(!fakeFolder.syncOnce()); // fileNameD still blocked
QCOMPARE(syncSpy.findItem(fileNameE)->_status, SyncFileItem::Status::Success);
}

Expand Down
Loading
Loading