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 @@ -28,10 +28,9 @@

namespace
{
constexpr const char *editorNamesForDelayedUpload[] = {"PowerPDF"};

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RCWzwQiKxZVfuB&open=AZ1Sm6RCWzwQiKxZVfuB&pullRequest=9777
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

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RCWzwQiKxZVfuC&open=AZ1Sm6RCWzwQiKxZVfuC&pullRequest=9777
constexpr auto delayIntervalForSyncRetryForOpenedForSigningFilesSeconds = 60;
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

Unmodified variable "basePinState" of type "enum OCC::PinState" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfuH&open=AZ1Sm6RDWzwQiKxZVfuH&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=AZ1Sm6RCWzwQiKxZVfuG&open=AZ1Sm6RCWzwQiKxZVfuG&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=AZ1Sm6RDWzwQiKxZVfuJ&open=AZ1Sm6RDWzwQiKxZVfuJ&pullRequest=9777
: QObject(parent)
, _dirItem(dirItem)
, _dirParentItem(parentDirItem)
Expand Down Expand Up @@ -232,7 +231,7 @@

const auto willBeExcluded = handleExcluded(path._target, e, entries, isHidden, isBlacklisted);

if (willBeExcluded) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfuL&open=AZ1Sm6RDWzwQiKxZVfuL&pullRequest=9777
continue;
}

Expand All @@ -253,7 +252,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 255 in src/libsync/discovery.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

Expand Down Expand Up @@ -319,15 +318,15 @@

auto forbiddenCharMatch = QString{};
const auto containsForbiddenCharacters =
std::any_of(forbiddenChars.cbegin(),
forbiddenChars.cend(),
[&localName, &forbiddenCharMatch](const QString &charPattern) {
if (localName.contains(charPattern)) {
forbiddenCharMatch = charPattern;
return true;
}
return false;
});

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

if (excluded == CSYNC_NOT_EXCLUDED && !localName.isEmpty()
&& !wasSyncedAlready
Expand Down Expand Up @@ -429,7 +428,7 @@
item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded."));
}
break;
case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE:

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfuS&open=AZ1Sm6RDWzwQiKxZVfuS&pullRequest=9777
item->_errorString = tr("Filename contains leading and trailing spaces.");
item->_status = SyncFileItem::FileNameInvalid;
if (isLocal && !maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) {
Expand Down Expand Up @@ -457,7 +456,7 @@
case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
item->_errorString = tr("The filename cannot be encoded on your file system.");
break;
case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED:

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfuR&open=AZ1Sm6RDWzwQiKxZVfuR&pullRequest=9777
const auto errorString = tr("The filename is blacklisted on the server.");
QString reasonString;
if (hasForbiddenFilename) {
Expand Down Expand Up @@ -502,7 +501,7 @@
const auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByPath(path.toUtf8());
const auto originalBaseFileName = QFileInfo(QString(_discoveryData->_localDir + "/" + conflictRecord.initialBasePath)).fileName();

if (allEntries.find(originalBaseFileName) == allEntries.end()) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "contains" member function.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfuV&open=AZ1Sm6RDWzwQiKxZVfuV&pullRequest=9777
// original entry is no longer on the server, remove conflicted copy
qCDebug(lcDisco) << "original entry:" << originalBaseFileName << "is no longer on the server, remove conflicted copy:" << path;
return true;
Expand Down Expand Up @@ -683,7 +682,7 @@
_pendingAsyncJobs++;
_discoveryData->checkSelectiveSyncNewFolder(path._server,
serverEntry.remotePerm,
[=, this](bool result) {

Check failure on line 685 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=AZ1Sm6RDWzwQiKxZVfuW&open=AZ1Sm6RDWzwQiKxZVfuW&pullRequest=9777
--_pendingAsyncJobs;
if (!result) {
processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
Expand Down Expand Up @@ -1050,7 +1049,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 1052 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=AZ1Sm6RDWzwQiKxZVfub&open=AZ1Sm6RDWzwQiKxZVfub&pullRequest=9777
_pendingAsyncJobs--;
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
if (etag || etag.error().code != 404 ||
Expand Down Expand Up @@ -1114,27 +1113,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()) {
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 @@ -1220,9 +1247,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 @@ -1467,7 +1507,7 @@
item->_checksumHeader.clear();
item->_size = localEntry.size;
item->_modtime = localEntry.modtime;
item->_type = localEntry.isDirectory && !localEntry.isVirtualFile ? ItemTypeDirectory : localEntry.isDirectory ? ItemTypeVirtualDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional operator into an independent statement.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfui&open=AZ1Sm6RDWzwQiKxZVfui&pullRequest=9777
_childModified = true;

if (!localEntry.caseClashConflictingName.isEmpty()) {
Expand Down Expand Up @@ -1583,7 +1623,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 1626 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=AZ1Sm6RDWzwQiKxZVfug&open=AZ1Sm6RDWzwQiKxZVfug&pullRequest=9777
Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted);
}
}
Expand All @@ -1599,7 +1639,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 1642 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=AZ1Sm6RDWzwQiKxZVfuf&open=AZ1Sm6RDWzwQiKxZVfuf&pullRequest=9777
qCInfo(lcDisco) << "Move without permission to rename base file, "
<< "source:" << movePerms.sourceOk
<< ", target:" << movePerms.destinationOk
Expand Down Expand Up @@ -1684,7 +1724,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 1727 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=AZ1Sm6RDWzwQiKxZVfuo&open=AZ1Sm6RDWzwQiKxZVfuo&pullRequest=9777


if (!etag || (etag.get() != base._etag && !item->isDirectory()) || _discoveryData->isRenamed(originalPath)
Expand Down Expand Up @@ -1931,6 +1971,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 @@ -2106,12 +2155,12 @@
return matchingEditorsKeepingFileBusy;
}

const auto isMatchingFileExtension = std::find_if(std::cbegin(fileExtensionsToCheckIfOpenForSigning), std::cend(fileExtensionsToCheckIfOpenForSigning),
[path](const auto &matchingExtension) {
return path._local.endsWith(matchingExtension, Qt::CaseInsensitive);
}) != std::cend(fileExtensionsToCheckIfOpenForSigning);

Check warning on line 2161 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=AZ1Sm6RDWzwQiKxZVfut&open=AZ1Sm6RDWzwQiKxZVfut&pullRequest=9777

if (!isMatchingFileExtension) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6RDWzwQiKxZVfus&open=AZ1Sm6RDWzwQiKxZVfus&pullRequest=9777
return matchingEditorsKeepingFileBusy;
}

Expand Down Expand Up @@ -2259,7 +2308,7 @@
}
}

DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()

Check failure on line 2311 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=AZ1Sm6RDWzwQiKxZVfux&open=AZ1Sm6RDWzwQiKxZVfux&pullRequest=9777
{
if (_dirItem && _dirItem->isEncrypted() && _dirItem->_encryptedFileName.isEmpty()) {
_discoveryData->_topLevelE2eeFolderPaths.insert(_discoveryData->_remoteFolder + _dirItem->_file);
Expand Down Expand Up @@ -2354,7 +2403,7 @@
void ProcessDirectoryJob::startAsyncLocalQuery()
{
QString localPath = _discoveryData->_localDir + _currentFolder._local;
auto localJob = new DiscoverySingleLocalDirectoryJob(_discoveryData->_account, localPath, _discoveryData->_syncOptions._vfs.data(), _discoveryData->_fileSystemReliablePermissions);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "localJob" of type "class OCC::DiscoverySingleLocalDirectoryJob *" should be const-qualified.

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

_discoveryData->_currentlyActiveJobs++;
_pendingAsyncJobs++;
Expand Down Expand Up @@ -2470,10 +2519,10 @@
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 2522 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=AZ1Sm6RDWzwQiKxZVfu4&open=AZ1Sm6RDWzwQiKxZVfu4&pullRequest=9777
{
const auto removeTrailingSpaces = [] (QString string) -> QString {
for (int n = string.size() - 1; n >= 0; -- n) {

Check warning on line 2525 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=AZ1Sm6RCWzwQiKxZVfuA&open=AZ1Sm6RCWzwQiKxZVfuA&pullRequest=9777
if (!string.at(n).isSpace()) {
string.truncate(n + 1);
break;
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 @@ -39,7 +39,7 @@
*
* Results are fed outwards via the DiscoveryPhase::itemDiscovered() signal.
*/
class ProcessDirectoryJob : public QObject

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this structure so it has no more than 20 fields, rather than the 22 it currently has.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6S-WzwQiKxZVfu8&open=AZ1Sm6S-WzwQiKxZVfu8&pullRequest=9777
{
Q_OBJECT

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 @@ -71,7 +71,7 @@
qCWarning(lcPutJob) << " Network error: " << reply()->errorString();
}

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

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

Expand Down Expand Up @@ -159,7 +159,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 162 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=AZ1Sm6VQWzwQiKxZVfvG&open=AZ1Sm6VQWzwQiKxZVfvG&pullRequest=9777
_item->updateLockStateFromDbRecord(oldRecord);
}
}
Expand All @@ -182,7 +182,7 @@

PropagateUploadFileCommon::PropagateUploadFileCommon(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
: PropagateItemJob(propagator, item)
, _finished(false)

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use the constructor's initializer list for data member "_finished". Use the in-class initializer instead.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6VQWzwQiKxZVfvC&open=AZ1Sm6VQWzwQiKxZVfvC&pullRequest=9777
, _deleteExisting(false)
, _aborting(false)
{
Expand Down Expand Up @@ -281,14 +281,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")
.arg(Utility::octetsToString(_fileToUpload._size),
Utility::octetsToString(quotaGuess)));
return;
}

Expand Down Expand Up @@ -720,7 +748,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 @@ -741,7 +770,7 @@

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

Check warning on line 773 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=AZ1Sm6VQWzwQiKxZVfu_&open=AZ1Sm6VQWzwQiKxZVfu_&pullRequest=9777
job->timeoutMsec(),
// Maximum of 30 minutes
thirtyMinutes));
Expand All @@ -749,7 +778,7 @@

void PropagateUploadFileCommon::slotJobDestroyed(QObject *job)
{
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end());

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

// This function is used whenever there is an error occurring and jobs might be in progress
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 @@ -245,7 +245,7 @@
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));
(new DeleteJob(account(), url, {}, this))->start();

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVftg&open=AZ1Sm6KiWzwQiKxZVftg&pullRequest=9777
}
}
}
Expand Down Expand Up @@ -383,7 +383,7 @@
const auto lockOwnerTypeToSkipReadonly = _account->capabilities().filesLockTypeAvailable()
? SyncFileItem::LockOwnerType::TokenLock
: SyncFileItem::LockOwnerType::UserLock;
if (item->_locked == SyncFileItem::LockStatus::LockedItem

Check failure on line 386 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=AZ1Sm6KiWzwQiKxZVfth&open=AZ1Sm6KiWzwQiKxZVfth&pullRequest=9777
&& (item->_lockOwnerType != lockOwnerTypeToSkipReadonly || item->_lockOwnerId != account()->davUser())) {
qCDebug(lcEngine()) << filePath << "file is locked: making it read only";
FileSystem::setFileReadOnly(filePath, true);
Expand Down Expand Up @@ -501,16 +501,16 @@
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=AZ1Sm6KiWzwQiKxZVftm&open=AZ1Sm6KiWzwQiKxZVftm&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=AZ1Sm6KiWzwQiKxZVftn&open=AZ1Sm6KiWzwQiKxZVftn&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) {

Check failure on line 508 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=AZ1Sm6KiWzwQiKxZVftl&open=AZ1Sm6KiWzwQiKxZVftl&pullRequest=9777
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=AZ1Sm6KiWzwQiKxZVfte&open=AZ1Sm6KiWzwQiKxZVfte&pullRequest=9777
const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVfto&open=AZ1Sm6KiWzwQiKxZVfto&pullRequest=9777
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=AZ1Sm6KiWzwQiKxZVftk&open=AZ1Sm6KiWzwQiKxZVftk&pullRequest=9777
_discoveryPhase->_excludes->addExcludeFilePath(excludeFilePath);
_discoveryPhase->_excludes->reloadExcludeFiles();
}
Expand Down Expand Up @@ -871,7 +871,7 @@

if (isNewlyUploadedFile && item->_locked != SyncFileItem::LockStatus::LockedItem && _account->capabilities().filesLockAvailable() &&
FileSystem::isMatchingOfficeFileExtension(item->_file)) {
{

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested code block into a separate function.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVftq&open=AZ1Sm6KiWzwQiKxZVftq&pullRequest=9777
SyncJournalFileRecord rec;
if (!_journal->getFileRecord(item->_file, &rec) || !rec.isValid()) {
qCWarning(lcEngine) << "Newly-created office file just uploaded but not in sync journal. Not going to lock it." << item->_file;
Expand Down Expand Up @@ -916,7 +916,7 @@
detectFileLock(item);
}

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

Check warning on line 919 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=AZ1Sm6KiWzwQiKxZVftr&open=AZ1Sm6KiWzwQiKxZVftr&pullRequest=9777
{
if (_propagator->_anotherSyncNeeded && _anotherSyncNeeded == NoFollowUpSync) {
_anotherSyncNeeded = ImmediateFollowUp;
Expand Down Expand Up @@ -1041,6 +1041,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 @@ -1093,6 +1118,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 @@ -1152,7 +1181,7 @@
return false;
}

bool SyncEngine::handleMassDeletion()

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVftw&open=AZ1Sm6KiWzwQiKxZVftw&pullRequest=9777
{
const auto displayDialog = ConfigFile().promptDeleteFiles() && !_syncOptions.isCmd();
const auto allFilesDeleted = !_hasNoneFiles && _hasRemoveFile;
Expand All @@ -1162,7 +1191,7 @@
if (oneItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
if (oneItem->isDirectory()) {
const auto result = _journal->listFilesInPath(oneItem->_file.toUtf8(), [&deletionCounter] (const auto &oneRecord) {
if (oneRecord.isFile()) {

Check failure on line 1194 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=AZ1Sm6KiWzwQiKxZVftx&open=AZ1Sm6KiWzwQiKxZVftx&pullRequest=9777
++deletionCounter;
}
});
Expand All @@ -1185,7 +1214,7 @@
}
}

promptUserBeforePropagation([this, side](auto &&callback){

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"std::forward" is never called on this forwarding reference argument.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVftz&open=AZ1Sm6KiWzwQiKxZVftz&pullRequest=9777
emit aboutToRemoveAllFiles(side >= 0 ? SyncFileItem::Down : SyncFileItem::Up, callback);
});
return true;
Expand All @@ -1210,7 +1239,7 @@
slotAddTouchedFile(_localPath + oneFolder->_file);

if (oneFolder->_type == ItemType::ItemTypeDirectory) {
const auto deletionCallback = [this] (const QString &deleteItem, bool) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ1Sm6KiWzwQiKxZVft0&open=AZ1Sm6KiWzwQiKxZVft0&pullRequest=9777
slotAddTouchedFile(deleteItem);
};

Expand All @@ -1227,7 +1256,7 @@
{
QPointer<QObject> guard = new QObject();
QPointer<QObject> self = this;
auto callback = [this, self, guard](bool cancel) -> void {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the redundant return type of this lambda.

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

Check warning on line 1259 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=AZ1Sm6KiWzwQiKxZVft2&open=AZ1Sm6KiWzwQiKxZVft2&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 @@ -1277,7 +1306,7 @@
_leadingAndTrailingSpacesFilesAllowed.append(filePath);
}

void SyncEngine::setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value)

Check warning on line 1309 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=AZ1Sm6KiWzwQiKxZVft3&open=AZ1Sm6KiWzwQiKxZVft3&pullRequest=9777
{
_shouldEnforceWindowsFileNameCompatibility = value;
}
Expand Down Expand Up @@ -1314,14 +1343,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 1353 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=AZ1Sm6KiWzwQiKxZVft5&open=AZ1Sm6KiWzwQiKxZVft5&pullRequest=9777
}

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

void SyncEngine::setFilesystemPermissionsReliable(bool reliable)

Check warning on line 1366 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=AZ1Sm6KiWzwQiKxZVft6&open=AZ1Sm6KiWzwQiKxZVft6&pullRequest=9777
{
_filesystemPermissionsReliable = reliable;
}
Expand Down Expand Up @@ -1485,7 +1514,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. "
"Free up server space or contact your administrator to increase your quota.");
if (_uniqueErrors.contains(msg)) {
return;
}
Expand Down Expand Up @@ -1572,7 +1602,7 @@
it != _discoveryPhase->_filesNeedingScheduledSync.cend();
++it) {

const auto file = it.key();

Check warning on line 1605 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=AZ1Sm6KiWzwQiKxZVft9&open=AZ1Sm6KiWzwQiKxZVft9&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 @@ -363,6 +363,8 @@

void finishSync();

void seedPropagatorQuota();

[[nodiscard]] bool shouldRestartSync() const;

bool handleMassDeletion();
Expand Down Expand Up @@ -406,7 +408,7 @@
std::set<QString> _localDiscoveryPaths;

QStringList _leadingAndTrailingSpacesFilesAllowed;
bool _shouldEnforceWindowsFileNameCompatibility = false;

Check warning on line 411 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=AZ1TUfU_V80h1Qdmofdz&open=AZ1TUfU_V80h1Qdmofdz&pullRequest=9777

// Hash of files we have scheduled for later sync runs, along with a
// pointer to the timer which will trigger the sync run for it.
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 ──
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) ──
// 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 ──
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 ──
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 ──
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