diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 55e217a0c47f3..6db7d3c0cd58f 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1217,7 +1217,7 @@ void Folder::startSync(const QStringList &pathList) } _engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles); - _engine->setFilesystemPermissionsReliable(_folderWatcher->canSetPermissions()); + _engine->setFilesystemPermsReliable(_folderWatcher->canSetPermissions()); correctPlaceholderFiles(); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 103518e8a7f68..7768b135577bf 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -13,6 +13,7 @@ #include "progressdispatcher.h" #include #include +#include #include #include #include @@ -28,10 +29,10 @@ namespace { -constexpr const char *editorNamesForDelayedUpload[] = {"PowerPDF"}; -constexpr const char *fileExtensionsToCheckIfOpenForSigning[] = {".pdf"}; -constexpr auto delayIntervalForSyncRetryForOpenedForSigningFilesSeconds = 60; -constexpr auto delayIntervalForSyncRetryForFilesExceedQuotaSeconds = 60; +constexpr std::array editorNamesForDelayedUpload = {"PowerPDF"}; +constexpr std::array kFileExtsForSigning = {".pdf"}; +constexpr auto kRetryDelaySigningFileSec = 60; +constexpr auto kRetryDelayQuotaExceedSec = 60; } namespace OCC { @@ -61,7 +62,7 @@ ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileIt computePinState(parent->_pinState); } -ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, const SyncFileItemPtr &parentDirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent) +ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *const data, const PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, const SyncFileItemPtr &parentDirItem, const QueryMode queryLocal, const qint64 lastSyncTimestamp, QObject *const parent) : QObject(parent) , _dirItem(dirItem) , _dirParentItem(parentDirItem) @@ -230,9 +231,8 @@ void ProcessDirectoryJob::process() const auto isBlacklisted = _queryServer == InBlackList || _discoveryData->isInSelectiveSyncBlackList(path._original) || isEncryptedFolderButE2eIsNotSetup; - const auto willBeExcluded = handleExcluded(path._target, e, entries, isHidden, isBlacklisted); - - if (willBeExcluded) { + if (const auto willBeExcluded = handleExcluded(path._target, e, entries, isHidden, isBlacklisted); + willBeExcluded) { continue; } @@ -277,11 +277,12 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent || 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); #if defined Q_OS_WINDOWS - if (hasLeadingOrTrailingSpaces && leadingAndTrailingSpacesFilesAllowed) { + if (const auto spacesFilesAllowed = !_discoveryData->_enforceWindowsFilenameCompat || _discoveryData->_spacesFilesAllowed.contains(_discoveryData->_localDir + path); + hasLeadingOrTrailingSpaces && spacesFilesAllowed) { #else - if (hasLeadingOrTrailingSpaces && (wasSyncedAlready || leadingAndTrailingSpacesFilesAllowed)) { + if (const auto spacesFilesAllowed = !_discoveryData->_enforceWindowsFilenameCompat || _discoveryData->_spacesFilesAllowed.contains(_discoveryData->_localDir + path); + hasLeadingOrTrailingSpaces && (wasSyncedAlready || spacesFilesAllowed)) { #endif excluded = CSYNC_NOT_EXCLUDED; } @@ -319,8 +320,7 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent auto forbiddenCharMatch = QString{}; const auto containsForbiddenCharacters = - std::any_of(forbiddenChars.cbegin(), - forbiddenChars.cend(), + std::ranges::any_of(forbiddenChars, [&localName, &forbiddenCharMatch](const QString &charPattern) { if (localName.contains(charPattern)) { forbiddenCharMatch = charPattern; @@ -374,6 +374,27 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent return true; } + const auto setSpaceExcludeError = [&](const QString &msg) { + item->_errorString = msg; + item->_status = SyncFileItem::FileNameInvalid; + if (isLocal && !maybeRenameForWinCompatibility(_discoveryData->_localDir + item->_file, excluded)) + item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded.")); + }; + + const auto buildBlacklistErrorString = [&]() { + const auto errorBase = tr("The filename is blacklisted on the server."); + QString reason; + if (hasForbiddenFilename) + reason = tr("Reason: the entire filename is forbidden."); + if (hasForbiddenBasename) + reason = tr("Reason: the filename has a forbidden base name (filename start)."); + if (hasForbiddenExtension) + reason = tr("Reason: the file has a forbidden extension (.%1).").arg(extension); + if (containsForbiddenCharacters) + reason = tr("Reason: the filename contains a forbidden character (%1).").arg(forbiddenCharMatch); + return reason.isEmpty() ? errorBase : QStringLiteral("%1 %2").arg(errorBase, reason); + }; + if (entries.localEntry.isSymLink) { /* Symbolic links are ignored. */ item->_errorString = tr("Symbolic links are not supported in syncing."); @@ -416,25 +437,13 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent item->_status = SyncFileItem::FileNameInvalid; break; case CSYNC_FILE_EXCLUDE_TRAILING_SPACE: - item->_errorString = tr("Filename contains trailing spaces."); - item->_status = SyncFileItem::FileNameInvalid; - if (isLocal && !maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) { - item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded.")); - } + setSpaceExcludeError(tr("Filename contains trailing spaces.")); break; case CSYNC_FILE_EXCLUDE_LEADING_SPACE: - item->_errorString = tr("Filename contains leading spaces."); - item->_status = SyncFileItem::FileNameInvalid; - if (isLocal && !maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) { - item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded.")); - } + setSpaceExcludeError(tr("Filename contains leading spaces.")); break; case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE: - item->_errorString = tr("Filename contains leading and trailing spaces."); - item->_status = SyncFileItem::FileNameInvalid; - if (isLocal && !maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) { - item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded.")); - } + setSpaceExcludeError(tr("Filename contains leading and trailing spaces.")); break; case CSYNC_FILE_EXCLUDE_LONG_FILENAME: item->_errorString = tr("Filename is too long."); @@ -458,25 +467,10 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent item->_errorString = tr("The filename cannot be encoded on your file system."); break; case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED: - const auto errorString = tr("The filename is blacklisted on the server."); - QString reasonString; - if (hasForbiddenFilename) { - reasonString = tr("Reason: the entire filename is forbidden."); - } - if (hasForbiddenBasename) { - reasonString = tr("Reason: the filename has a forbidden base name (filename start)."); - } - if (hasForbiddenExtension) { - reasonString = tr("Reason: the file has a forbidden extension (.%1).").arg(extension); - } - if (containsForbiddenCharacters) { - reasonString = tr("Reason: the filename contains a forbidden character (%1).").arg(forbiddenCharMatch); - } - item->_errorString = reasonString.isEmpty() ? errorString : QStringLiteral("%1 %2").arg(errorString, reasonString); + item->_errorString = buildBlacklistErrorString(); item->_status = SyncFileItem::FileNameInvalidOnServer; - if (isLocal && !maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) { + if (isLocal && !maybeRenameForWinCompatibility(_discoveryData->_localDir + item->_file, excluded)) item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded.")); - } break; } } @@ -502,7 +496,7 @@ bool ProcessDirectoryJob::canRemoveCaseClashConflictedCopy(const QString &path, const auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByPath(path.toUtf8()); const auto originalBaseFileName = QFileInfo(QString(_discoveryData->_localDir + "/" + conflictRecord.initialBasePath)).fileName(); - if (allEntries.find(originalBaseFileName) == allEntries.end()) { + if (!allEntries.contains(originalBaseFileName)) { // 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; @@ -683,7 +677,7 @@ void ProcessDirectoryJob::postProcessServerNew(const SyncFileItemPtr &item, _pendingAsyncJobs++; _discoveryData->checkSelectiveSyncNewFolder(path._server, serverEntry.remotePerm, - [=, this](bool result) { + [this, item, path, localEntry, serverEntry, dbEntry](const bool result) { --_pendingAsyncJobs; if (!result) { processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); @@ -743,7 +737,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it if (item->_e2eEncryptionStatusRemote != SyncFileItem::EncryptionStatus::NotEncrypted) { Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); } - item->_encryptedFileName = [=, this] { + item->_encryptedFileName = [this, &serverEntry] { if (serverEntry.e2eMangledName.isEmpty()) { return QString(); } @@ -1050,7 +1044,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it // 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 &etag) mutable { + connect(job, &RequestEtagJob::finishedWithResult, this, [this, item, path, localEntry, serverEntry, dbEntry, originalPath, postProcessRename](const HttpResult &etag) mutable { _pendingAsyncJobs--; QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); if (etag || etag.error().code != 404 || @@ -1126,8 +1120,7 @@ int64_t ProcessDirectoryJob::folderBytesAvailable(const SyncFileItemPtr &item, c qCDebug(lcDisco) << "_dirItem->_folderQuota.bytesAvailable:" << _dirItem->_folderQuota.bytesAvailable; - SyncJournalFileRecord dirItemDbRecord; - if (_discoveryData->_statedb->getFileRecord(_dirItem->_file, &dirItemDbRecord) && dirItemDbRecord.isValid()) { + if (SyncJournalFileRecord dirItemDbRecord; _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; @@ -1137,6 +1130,22 @@ int64_t ProcessDirectoryJob::folderBytesAvailable(const SyncFileItemPtr &item, c return _dirItem->_folderQuota.bytesAvailable; } +void ProcessDirectoryJob::applyE2eeEncryptForLocalNew(const SyncFileItemPtr &item, + const SyncJournalFileRecord &base) const +{ + // renaming the encrypted folder is done via remove + re-upload hence we need to mark the + // newly created folder as encrypted. base is the DB record with the old name and encryption info. + item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus); + item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion( + _discoveryData->_account->capabilities().clientSideEncryptionVersion()); + if (item->_e2eEncryptionStatus != item->_e2eEncryptionServerCapability) { + item->_e2eEncryptionStatus = item->_e2eEncryptionServerCapability; + } + const auto wasEncrypted = base._e2eEncryptionStatus != SyncJournalFileRecord::EncryptionStatus::NotEncrypted; + Q_ASSERT(!wasEncrypted || item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); + Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); +} + void ProcessDirectoryJob::processFileAnalyzeLocalInfo( const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry, QueryMode recurseQueryServer) @@ -1222,7 +1231,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_status = SyncFileItem::Status::NormalError; _discoveryData->_anotherSyncNeeded = true; - _discoveryData->_filesNeedingScheduledSync.insert(path._original, delayIntervalForSyncRetryForFilesExceedQuotaSeconds); + _discoveryData->_filesNeedingScheduledSync.insert(path._original, kRetryDelayQuotaExceedSec); } if (item->_type != CSyncEnums::ItemTypeVirtualFile) { @@ -1234,7 +1243,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_errorString = tr("Could not upload file, because it is open in \"%1\".").arg(editorsString); item->_status = SyncFileItem::Status::SoftError; _discoveryData->_anotherSyncNeeded = true; - _discoveryData->_filesNeedingScheduledSync.insert(path._original, delayIntervalForSyncRetryForOpenedForSigningFilesSeconds); + _discoveryData->_filesNeedingScheduledSync.insert(path._original, kRetryDelaySigningFileSec); } } @@ -1467,7 +1476,14 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( 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; + if (localEntry.isDirectory && !localEntry.isVirtualFile) + item->_type = ItemTypeDirectory; + else if (localEntry.isDirectory) + item->_type = ItemTypeVirtualDirectory; + else if (localEntry.isVirtualFile) + item->_type = ItemTypeVirtualFile; + else + item->_type = ItemTypeFile; _childModified = true; if (!localEntry.caseClashConflictingName.isEmpty()) { @@ -1560,14 +1576,14 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( const auto isE2eeMove = isMove && (base.isE2eEncrypted() || isInsideEncryptedTree()); const auto isCfApiVfsMode = _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() == Vfs::WindowsCfApi; const bool isOnlineOnlyItem = isCfApiVfsMode && (localEntry.isDirectory || _discoveryData->_syncOptions._vfs->isDehydratedPlaceholder(_discoveryData->_localDir + path._local)); - const auto isE2eeMoveOnlineOnlyItemWithCfApi = isE2eeMove && isOnlineOnlyItem; + const auto isE2eeMoveOnlineOnlyWithCfApi = isE2eeMove && isOnlineOnlyItem; if (isE2eeMove) { qCDebug(lcDisco) << "requesting permanent deletion for" << originalPath; _discoveryData->_permanentDeletionRequests.insert(originalPath); } - if (isE2eeMoveOnlineOnlyItemWithCfApi) { + if (isE2eeMoveOnlineOnlyWithCfApi) { item->_instruction = CSYNC_INSTRUCTION_NEW; item->_direction = SyncFileItem::Down; item->_isRestoration = true; @@ -1575,19 +1591,9 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( } // If it's not a move it's just a local-NEW - if (!isMove || (isE2eeMove && !isE2eeMoveOnlineOnlyItemWithCfApi)) { + if (!isMove || (isE2eeMove && !isE2eeMoveOnlineOnlyWithCfApi)) { if (base.isE2eEncrypted()) { - // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted - // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information - item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus); - item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); - if (item->_e2eEncryptionStatus != item->_e2eEncryptionServerCapability) { - item->_e2eEncryptionStatus = item->_e2eEncryptionServerCapability; - if (base._e2eEncryptionStatus != SyncJournalFileRecord::EncryptionStatus::NotEncrypted) { - Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); - } - } - Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); + applyE2eeEncryptForLocalNew(item, base); } postProcessLocalNew(); finalize(); @@ -1598,8 +1604,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( // Technically we should use the permissions from the server, but we'll assume it is the same 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) { + if (const auto movePerms = checkMovePermissions(base._remotePerm, originalPath, item->isDirectory()); + !movePerms.sourceOk || !movePerms.destinationOk || (serverHasMountRootProperty && isExternalStorage) || isE2eeMoveOnlineOnlyWithCfApi) { qCInfo(lcDisco) << "Move without permission to rename base file, " << "source:" << movePerms.sourceOk << ", target:" << movePerms.destinationOk @@ -1617,14 +1623,14 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( // If the destination upload will work, we're fine with the source deletion. // If the source deletion can't work, checkPermissions will error. // In case of external storage mounted folders we are never allowed to move/delete them - if (movePerms.destinationNewOk && !isExternalStorage && !isE2eeMoveOnlineOnlyItemWithCfApi) { + if (movePerms.destinationNewOk && !isExternalStorage && !isE2eeMoveOnlineOnlyWithCfApi) { return; } // Here we know the new location can't be uploaded: must prevent the source delete. // Two cases: either the source item was already processed or not. - auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath); - if (wasDeletedOnClient.first) { + const auto [wasDeleted, deletedEtag] = _discoveryData->findAndCancelDeletedJob(originalPath); + if (wasDeleted) { // More complicated. The REMOVE is canceled. Restore will happen next sync. qCInfo(lcDisco) << "Undid remove instruction on source" << originalPath; if (!_discoveryData->_statedb->deleteFileRecord(originalPath, true)) { @@ -1640,7 +1646,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( return; } - auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath); + const auto [wasDeletedOnClient, deletedItemEtag] = _discoveryData->findAndCancelDeletedJob(originalPath); auto processRename = [item, originalPath, base, this](PathTuple &path) { auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Down); @@ -1674,8 +1680,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( qCInfo(lcDisco) << "Rename detected (up) " << item->_file << " -> " << item->_renameTarget; }; - if (wasDeletedOnClient.first) { - recurseQueryServer = wasDeletedOnClient.second == base._etag ? ParentNotChanged : NormalQuery; + if (wasDeletedOnClient) { + recurseQueryServer = deletedItemEtag == base._etag ? ParentNotChanged : NormalQuery; processRename(path); } else { // We must query the server to know if the etag has not changed @@ -1684,7 +1690,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( if (base.isVirtualFile() && isVfsWithSuffix()) chopVirtualFileSuffix(serverOriginalPath); auto job = new RequestEtagJob(_discoveryData->_account, serverOriginalPath, this); - connect(job, &RequestEtagJob::finishedWithResult, this, [=, this](const HttpResult &etag) mutable { + connect(job, &RequestEtagJob::finishedWithResult, this, [this, base, item, originalPath, postProcessLocalNew, processRename, path, recurseQueryServer, serverEntry, dbEntry](const HttpResult &etag) mutable { if (!etag || (etag.get() != base._etag && !item->isDirectory()) || _discoveryData->isRenamed(originalPath) @@ -1730,7 +1736,7 @@ void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, Proce // If there's no content hash, use heuristics if (serverEntry.checksumHeader.isEmpty()) { // If the size or mtime is different, it's definitely a conflict. - bool isConflict = (serverEntry.size != localEntry.size) || (serverEntry.modtime != localEntry.modtime) || (dbEntry.isValid() && dbEntry._modtime != localEntry.modtime && serverEntry.modtime == localEntry.modtime); + const bool isConflict = (serverEntry.size != localEntry.size) || (serverEntry.modtime != localEntry.modtime) || (dbEntry.isValid() && dbEntry._modtime != localEntry.modtime && serverEntry.modtime == localEntry.modtime); // It could be a conflict even if size and mtime match! // @@ -1853,7 +1859,7 @@ void ProcessDirectoryJob::processFileFinalize( if (_discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != OCC::Vfs::Off && (item->_type == CSyncEnums::ItemTypeFile || item->_type == CSyncEnums::ItemTypeDirectory) && item->_instruction == CSyncEnums::CSYNC_INSTRUCTION_NONE && - FileSystem::isLnkFile((_discoveryData->_localDir + path._local)) && + FileSystem::isLnkFile(_discoveryData->_localDir + path._local) && !_discoveryData->_syncOptions._vfs->isPlaceHolderInSync(_discoveryData->_localDir + path._local)) { item->_instruction = CSyncEnums::CSYNC_INSTRUCTION_SYNC; item->_direction = SyncFileItem::Down; @@ -2106,12 +2112,11 @@ QStringList ProcessDirectoryJob::queryEditorsKeepingFileBusy(const SyncFileItemP 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); - - if (!isMatchingFileExtension) { + if (const auto isMatchingFileExtension = std::ranges::find_if(kFileExtsForSigning, + [&path](const auto &matchingExtension) { + return path._local.endsWith(matchingExtension, Qt::CaseInsensitive); + }) != std::cend(kFileExtsForSigning); + !isMatchingFileExtension) { return matchingEditorsKeepingFileBusy; } @@ -2119,8 +2124,8 @@ QStringList ProcessDirectoryJob::queryEditorsKeepingFileBusy(const SyncFileItemP 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) { + const auto isMatchingEditorFound = std::ranges::find_if(editorNamesForDelayedUpload, + [&detectedEditorName](const auto &matchingEditorName) { return detectedEditorName.processName.startsWith(matchingEditorName, Qt::CaseInsensitive); }) != std::cend(editorNamesForDelayedUpload); if (isMatchingEditorFound) { @@ -2180,7 +2185,7 @@ void ProcessDirectoryJob::subJobFinished() if (job->_dirItem) emit _discoveryData->itemDiscovered(job->_dirItem); - int count = _runningJobs.removeAll(job); + const auto count = _runningJobs.removeAll(job); ASSERT(count == 1); job->deleteLater(); QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); @@ -2259,9 +2264,33 @@ void ProcessDirectoryJob::chopVirtualFileSuffix(QString &str) const } } +void ProcessDirectoryJob::updateDirItemFromServerJob(const DiscoverySingleDirectoryJob *const serverJob) +{ + if (_dirItem->isEncrypted()) { + _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); + + SyncJournalFileRecord record; + const auto alreadyDownloaded = _discoveryData->_statedb->getFileRecord(_dirItem->_file, &record) && record.isValid(); + // we need to make sure we first download all e2ee files/folders before migrating + _dirItem->_isEncryptedMetadataNeedUpdate = alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); + _dirItem->_e2eEncryptionStatus = serverJob->currentEncryptionStatus(); + _dirItem->_e2eEncryptionStatusRemote = serverJob->currentEncryptionStatus(); + _dirItem->_e2eEncryptionServerCapability = serverJob->requiredEncryptionStatus(); + qCDebug(lcDisco()) << _dirItem->_e2eEncryptionStatus << _dirItem->_e2eEncryptionServerCapability; + Q_ASSERT(_dirItem->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::Encrypted); + Q_ASSERT(_dirItem->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); + _discoveryData->_anotherSyncNeeded = !alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); + } + qCDebug(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file + << "and it has _isFileDropDetected:" << _dirItem->_isFileDropDetected + << "with quota bytesUsed:" << _dirItem->_folderQuota.bytesUsed + << "bytesAvailable:" << _dirItem->_folderQuota.bytesAvailable; +} + DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() { - if (_dirItem && _dirItem->isEncrypted() && _dirItem->_encryptedFileName.isEmpty()) { + if (const auto shouldAddE2eeFolderPath = _dirItem && _dirItem->isEncrypted() && _dirItem->_encryptedFileName.isEmpty(); + shouldAddE2eeFolderPath) { _discoveryData->_topLevelE2eeFolderPaths.insert(_discoveryData->_remoteFolder + _dirItem->_file); } auto serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account, @@ -2281,24 +2310,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() _pendingAsyncJobs++; connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) { if (_dirItem) { - if (_dirItem->isEncrypted()) { - _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); - - SyncJournalFileRecord record; - const auto alreadyDownloaded = _discoveryData->_statedb->getFileRecord(_dirItem->_file, &record) && record.isValid(); - // we need to make sure we first download all e2ee files/folders before migrating - _dirItem->_isEncryptedMetadataNeedUpdate = alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); - _dirItem->_e2eEncryptionStatus = serverJob->currentEncryptionStatus(); - _dirItem->_e2eEncryptionStatusRemote = serverJob->currentEncryptionStatus(); - _dirItem->_e2eEncryptionServerCapability = serverJob->requiredEncryptionStatus(); - qCDebug(lcDisco()) << _dirItem->_e2eEncryptionStatus << _dirItem->_e2eEncryptionServerCapability; - Q_ASSERT(_dirItem->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::Encrypted); - Q_ASSERT(_dirItem->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); - _discoveryData->_anotherSyncNeeded = !alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); - } - qCDebug(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << _dirItem->_isFileDropDetected - << "with quota bytesUsed:" << _dirItem->_folderQuota.bytesUsed - << "bytesAvailable:" << _dirItem->_folderQuota.bytesAvailable; + updateDirItemFromServerJob(serverJob); } _discoveryData->_currentlyActiveJobs--; _pendingAsyncJobs--; @@ -2354,7 +2366,7 @@ void ProcessDirectoryJob::setFolderQuota(const FolderQuota &folderQuota) void ProcessDirectoryJob::startAsyncLocalQuery() { QString localPath = _discoveryData->_localDir + _currentFolder._local; - auto localJob = new DiscoverySingleLocalDirectoryJob(_discoveryData->_account, localPath, _discoveryData->_syncOptions._vfs.data(), _discoveryData->_fileSystemReliablePermissions); + auto *const localJob = new DiscoverySingleLocalDirectoryJob(_discoveryData->_account, localPath, _discoveryData->_syncOptions._vfs.data(), _discoveryData->_fileSystemReliablePermissions); _discoveryData->_currentlyActiveJobs++; _pendingAsyncJobs++; @@ -2442,13 +2454,13 @@ void ProcessDirectoryJob::setupDbPinStateActions(SyncJournalFileRecord &record) } } -bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &absoluteFileName, - CSYNC_EXCLUDE_TYPE excludeReason) +bool ProcessDirectoryJob::maybeRenameForWinCompatibility(const QString &absoluteFileName, + const CSYNC_EXCLUDE_TYPE excludeReason) const { auto result = true; - const auto leadingAndTrailingSpacesFilesAllowed = !_discoveryData->_shouldEnforceWindowsFileNameCompatibility || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(absoluteFileName); - if (leadingAndTrailingSpacesFilesAllowed) { + if (const auto spacesFilesAllowed = !_discoveryData->_enforceWindowsFilenameCompat || _discoveryData->_spacesFilesAllowed.contains(absoluteFileName); + spacesFilesAllowed) { return result; } @@ -2472,8 +2484,8 @@ bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &abso case CSYNC_FILE_EXCLUDE_LEADING_SPACE: case CSYNC_FILE_EXCLUDE_TRAILING_SPACE: { - const auto removeTrailingSpaces = [] (QString string) -> QString { - for (int n = string.size() - 1; n >= 0; -- n) { + const auto removeTrailingSpaces = [] (QString string) { + for (qsizetype n = string.size() - 1; n >= 0; -- n) { if (!string.at(n).isSpace()) { string.truncate(n + 1); break; @@ -2489,6 +2501,9 @@ bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &abso result = FileSystem::rename(absoluteFileName, renameTarget); break; } + default: + // All known CSYNC_EXCLUDE_TYPE values are handled above. + break; } return result; } diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index f59cf75250863..1a849fdda497c 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -103,7 +103,7 @@ class ProcessDirectoryJob : public QObject ProcessDirectoryJob *parent); explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, const SyncFileItemPtr &parentDirItem, - QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent); + const QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent); void start(); /** Start up to nbJobs, return the number of job started; emit finished() when done */ @@ -204,6 +204,8 @@ class ProcessDirectoryJob : public QObject */ MovePermissionResult checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath, bool isDirectory); + void updateDirItemFromServerJob(const DiscoverySingleDirectoryJob *const serverJob); + void applyE2eeEncryptForLocalNew(const SyncFileItemPtr &item, const SyncJournalFileRecord &base) const; void processBlacklisted(const PathTuple &, const LocalInfo &, const SyncJournalFileRecord &dbEntry); void subJobFinished(); @@ -249,8 +251,8 @@ class ProcessDirectoryJob : public QObject */ void setupDbPinStateActions(SyncJournalFileRecord &record); - bool maybeRenameForWindowsCompatibility(const QString &absoluteFileName, - CSYNC_EXCLUDE_TYPE excludeReason); + bool maybeRenameForWinCompatibility(const QString &absoluteFileName, + const CSYNC_EXCLUDE_TYPE excludeReason) const; [[nodiscard]] bool checkNewDeleteConflict(const SyncFileItemPtr &item) const; diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index e54cecf566a68..f6331e688b6c7 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -113,9 +113,9 @@ class DiscoverySingleDirectoryJob : public QObject explicit DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, const QString &remoteRootFolderPath, - /* TODO for topLevelE2eeFolderPaths, from review: I still do not get why giving the whole QSet instead of just the parent of the folder we are in - sounds to me like it would be much more efficient to just have the e2ee parent folder that we are - inside*/ + // TODO for topLevelE2eeFolderPaths, from review: I still do not get why giving the whole QSet instead of just the parent of the folder we are in + // sounds to me like it would be much more efficient to just have the e2ee parent folder that we are + // inside const QSet &topLevelE2eeFolderPaths, SyncFileItem::EncryptionStatus parentEncryptionStatus, QObject *parent = nullptr); @@ -295,8 +295,8 @@ class DiscoveryPhase : public QObject ExcludedFiles *_excludes = nullptr; QRegularExpression _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles QStringList _serverBlacklistedFiles; // The blacklist from the capabilities - QStringList _leadingAndTrailingSpacesFilesAllowed; - bool _shouldEnforceWindowsFileNameCompatibility = false; + QStringList _spacesFilesAllowed; + bool _enforceWindowsFilenameCompat = false; bool _ignoreHiddenFiles = false; std::function _shouldDiscoverLocaly; diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 5011eb1084fe5..3706e0089d648 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -71,7 +71,7 @@ void PUTFileJob::start() qCWarning(lcPutJob) << " Network error: " << reply()->errorString(); } - connect(reply(), &QNetworkReply::uploadProgress, this, [requestID] (qint64 bytesSent, qint64 bytesTotal) { + connect(reply(), &QNetworkReply::uploadProgress, this, [requestID] (const qint64 bytesSent, const qint64 bytesTotal) { qCDebug(lcPutJob()) << requestID << "upload progress" << bytesSent << bytesTotal; }); @@ -114,9 +114,9 @@ bool PollJob::finished() _item->_requestId = requestId(); _item->_status = classifyError(err, _item->_httpErrorCode); _item->_errorString = errorString(); - const auto exceptionParsed = getExceptionFromReply(reply()); - _item->_errorExceptionName = exceptionParsed.first; - _item->_errorExceptionMessage = exceptionParsed.second; + const auto [exceptionName, exceptionMessage] = getExceptionFromReply(reply()); + _item->_errorExceptionName = exceptionName; + _item->_errorExceptionMessage = exceptionMessage; if (_item->_status == SyncFileItem::FatalError || _item->_httpErrorCode >= 400) { if (_item->_status != SyncFileItem::FatalError @@ -158,10 +158,8 @@ bool PollJob::finished() _item->_status = SyncFileItem::Success; _item->_fileId = json["fileId"].toString().toUtf8(); - if (SyncJournalFileRecord oldRecord; _journal->getFileRecord(_item->destination(), &oldRecord) && oldRecord.isValid()) { - if (oldRecord._etag != _item->_etag) { - _item->updateLockStateFromDbRecord(oldRecord); - } + if (SyncJournalFileRecord oldRecord; _journal->getFileRecord(_item->destination(), &oldRecord) && oldRecord.isValid() && oldRecord._etag != _item->_etag) { + _item->updateLockStateFromDbRecord(oldRecord); } _item->_etag = parseEtag(json["ETag"].toString().toUtf8()); @@ -182,9 +180,6 @@ bool PollJob::finished() PropagateUploadFileCommon::PropagateUploadFileCommon(OwncloudPropagator *propagator, const SyncFileItemPtr &item) : PropagateItemJob(propagator, item) - , _finished(false) - , _deleteExisting(false) - , _aborting(false) { const auto path = _item->_file; const auto slashPosition = path.lastIndexOf('/'); @@ -741,7 +736,7 @@ void PropagateUploadFileCommon::adjustLastJobTimeout(AbstractNetworkJob *job, qi job->setTimeout(qBound( // Calculate 3 minutes for each gigabyte of data - qMin(thirtyMinutes - 1, qRound64(threeMinutes * fileSize / 1e9)), + qMin(thirtyMinutes - 1, qRound64(threeMinutes * static_cast(fileSize) / 1e9)), job->timeoutMsec(), // Maximum of 30 minutes thirtyMinutes)); @@ -749,7 +744,7 @@ void PropagateUploadFileCommon::adjustLastJobTimeout(AbstractNetworkJob *job, qi void PropagateUploadFileCommon::slotJobDestroyed(QObject *job) { - _jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end()); + _jobs.erase(std::ranges::remove(_jobs, job).begin(), _jobs.end()); } // This function is used whenever there is an error occurring and jobs might be in progress diff --git a/src/libsync/propagateupload.h b/src/libsync/propagateupload.h index a32c0806bad54..b171ece64da77 100644 --- a/src/libsync/propagateupload.h +++ b/src/libsync/propagateupload.h @@ -204,15 +204,15 @@ class PropagateUploadFileCommon : public PropagateItemJob protected: QVector _jobs; /// network jobs that are currently in transit - bool _finished BITFIELD(1); /// Tells that all the jobs have been finished - bool _deleteExisting BITFIELD(1); + bool _finished BITFIELD(1) = false; /// Tells that all the jobs have been finished + bool _deleteExisting BITFIELD(1) = false; /** Whether an abort is currently ongoing. * * Important to avoid duplicate aborts since each finishing PUTFileJob might * trigger an abort on error. */ - bool _aborting BITFIELD(1); + bool _aborting BITFIELD(1) = false; /* This is a minified version of the SyncFileItem, * that holds only the specifics about the file that's diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 1afd7cff4031b..332849eaf704e 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -241,11 +241,12 @@ void SyncEngine::deleteStaleUploadInfos(const SyncFileItemVector &syncItems) // Delete the stales chunk on the server. if (account()->capabilities().chunkingNg()) { - for (uint transferId : std::as_const(ids)) { + for (const uint transferId : std::as_const(ids)) { 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(); + auto *const deleteJob = new DeleteJob(account(), url, {}, this); + deleteJob->start(); } } } @@ -319,6 +320,22 @@ void SyncEngine::caseClashConflictRecordMaintenance() } +void SyncEngine::updateVirtualFileReadOnlyState(const SyncFileItemPtr &item, const QString &filePath) const +{ + const auto lockOwnerTypeToSkipReadonly = _account->capabilities().filesLockTypeAvailable() + ? SyncFileItem::LockOwnerType::TokenLock + : SyncFileItem::LockOwnerType::UserLock; + if (item->_locked == SyncFileItem::LockStatus::LockedItem + && (item->_lockOwnerType != lockOwnerTypeToSkipReadonly || item->_lockOwnerId != account()->davUser())) { + qCDebug(lcEngine()) << filePath << "file is locked: making it read only"; + FileSystem::setFileReadOnly(filePath, true); + } else { + const auto isReadOnly = !item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite); + qCDebug(lcEngine()) << filePath << "file is not locked: making it" << (isReadOnly ? "read only" : "read write"); + FileSystem::setFileReadOnlyWeak(filePath, isReadOnly); + } +} + void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) { emit itemDiscovered(item); @@ -380,24 +397,12 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) } if (item->_type == CSyncEnums::ItemTypeVirtualFile) { - const auto lockOwnerTypeToSkipReadonly = _account->capabilities().filesLockTypeAvailable() - ? SyncFileItem::LockOwnerType::TokenLock - : SyncFileItem::LockOwnerType::UserLock; - if (item->_locked == SyncFileItem::LockStatus::LockedItem - && (item->_lockOwnerType != lockOwnerTypeToSkipReadonly || item->_lockOwnerId != account()->davUser())) { - qCDebug(lcEngine()) << filePath << "file is locked: making it read only"; - FileSystem::setFileReadOnly(filePath, true); - } else { - qCDebug(lcEngine()) << filePath << "file is not locked: making it" - << ((!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) ? "read only" - : "read write"); - FileSystem::setFileReadOnlyWeak(filePath, (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite))); - } + updateVirtualFileReadOnlyState(item, filePath); } // Update on-disk virtual file metadata if (modificationHappened && item->_type == ItemTypeVirtualFile) { - auto r = _syncOptions._vfs->updateMetadata(*item, filePath, {}); + const auto r = _syncOptions._vfs->updateMetadata(*item, filePath, {}); if (!r) { item->_status = SyncFileItem::Status::NormalError; item->_instruction = CSYNC_INSTRUCTION_ERROR; @@ -474,8 +479,8 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) _needsUpdate = true; // Insert sorted - auto it = std::lower_bound( _syncItems.begin(), _syncItems.end(), item ); // the _syncItems is sorted - _syncItems.insert( it, item ); + const auto it = std::ranges::lower_bound(_syncItems, item); // the _syncItems is sorted + _syncItems.insert(it, item); slotNewItem(item); @@ -484,6 +489,27 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) } } +bool SyncEngine::unlockE2eeFolders(const QList> &e2EeLockedFolders) +{ + for (const auto &[folderId, folderTokenEncrypted] : e2EeLockedFolders) { + qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId; + const auto folderToken = EncryptionHelper::decryptStringAsymmetric( + _account->e2e()->getCertificateInformation(), + _account->e2e()->paddingMode(), + *_account->e2e(), + folderTokenEncrypted); + if (!folderToken) { + qCWarning(lcEngine()) << "decrypt failed"; + return false; + } + // 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 + auto *const unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this); + unlockJob->setShouldRollbackMetadataChanges(true); + unlockJob->start(); + } + return true; +} + void SyncEngine::startSync() { if (_journal->exists()) { @@ -500,20 +526,8 @@ void SyncEngine::startSync() const auto e2EeLockedFolders = _journal->e2EeLockedFolders(); - if (!e2EeLockedFolders.isEmpty()) { - for (const auto &e2EeLockedFolder : e2EeLockedFolders) { - const auto folderId = e2EeLockedFolder.first; - 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 - const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this); - unlockJob->setShouldRollbackMetadataChanges(true); - unlockJob->start(); - } + if (!e2EeLockedFolders.isEmpty() && !unlockE2eeFolders(e2EeLockedFolders)) { + return; } } @@ -634,11 +648,11 @@ void SyncEngine::startSync() _discoveryPhase = std::make_unique(); _discoveryPhase->_fileSystemReliablePermissions = _filesystemPermissionsReliable; - _discoveryPhase->_leadingAndTrailingSpacesFilesAllowed = _leadingAndTrailingSpacesFilesAllowed; + _discoveryPhase->_spacesFilesAllowed = _spacesFilesAllowed; _discoveryPhase->_account = _account; _discoveryPhase->_excludes = _excludedFiles.data(); - const QString excludeFilePath = _localPath + QStringLiteral(".sync-exclude.lst"); - if (FileSystem::fileExists(excludeFilePath)) { + if (const QString excludeFilePath = _localPath + QStringLiteral(".sync-exclude.lst"); + FileSystem::fileExists(excludeFilePath)) { _discoveryPhase->_excludes->addExcludeFilePath(excludeFilePath); _discoveryPhase->_excludes->reloadExcludeFiles(); } @@ -673,12 +687,12 @@ void SyncEngine::startSync() !forbiddenBasenames.isEmpty() && !forbiddenExtensions.isEmpty() && !forbiddenChars.isEmpty()) { - _shouldEnforceWindowsFileNameCompatibility = true; - _discoveryPhase->_shouldEnforceWindowsFileNameCompatibility = _shouldEnforceWindowsFileNameCompatibility; + _enforceWindowsFilenameCompat = true; + _discoveryPhase->_enforceWindowsFilenameCompat = _enforceWindowsFilenameCompat; } #if defined Q_OS_WINDOWS - _shouldEnforceWindowsFileNameCompatibility = true; - _discoveryPhase->_shouldEnforceWindowsFileNameCompatibility = _shouldEnforceWindowsFileNameCompatibility; + _enforceWindowsFilenameCompat = true; + _discoveryPhase->_enforceWindowsFilenameCompat = _enforceWindowsFilenameCompat; #endif // Check for invalid character in old server version @@ -700,7 +714,7 @@ void SyncEngine::startSync() connect(_discoveryPhase.get(), &DiscoveryPhase::itemDiscovered, this, &SyncEngine::slotItemDiscovered); connect(_discoveryPhase.get(), &DiscoveryPhase::newBigFolder, this, &SyncEngine::newBigFolder); connect(_discoveryPhase.get(), &DiscoveryPhase::existingFolderNowBig, this, &SyncEngine::existingFolderNowBig); - connect(_discoveryPhase.get(), &DiscoveryPhase::fatalError, this, [this](const QString &errorString, ErrorCategory errorCategory) { + connect(_discoveryPhase.get(), &DiscoveryPhase::fatalError, this, [this](const QString &errorString, const ErrorCategory errorCategory) { Q_EMIT syncError(errorString, errorCategory); finalize(false); }); @@ -916,7 +930,7 @@ void SyncEngine::slotItemCompleted(const SyncFileItemPtr &item, const ErrorCateg detectFileLock(item); } -void SyncEngine::slotPropagationFinished(OCC::SyncFileItem::Status status) +void SyncEngine::slotPropagationFinished(const OCC::SyncFileItem::Status status) { if (_propagator->_anotherSyncNeeded && _anotherSyncNeeded == NoFollowUpSync) { _anotherSyncNeeded = ImmediateFollowUp; @@ -969,7 +983,7 @@ void SyncEngine::finalize(bool success) _localDiscoveryStyle = LocalDiscoveryStyle::FilesystemOnly; _clearTouchedFilesTimer.start(); - _leadingAndTrailingSpacesFilesAllowed.clear(); + _spacesFilesAllowed.clear(); } void SyncEngine::processCaseClashConflictsBeforeDiscovery() @@ -1031,7 +1045,7 @@ void SyncEngine::restoreOldFiles(SyncFileItemVector &syncItems) } } -void SyncEngine::cancelSyncOrContinue(bool cancel) +void SyncEngine::cancelSyncOrContinue(const bool cancel) { if (cancel) { qCInfo(lcEngine) << "User aborted sync"; @@ -1143,40 +1157,41 @@ bool SyncEngine::shouldRestartSync() const return false; } - for (const auto &syncItem : _syncItems) { - // If there's at least one remove instruction to be propagated to the remote: bail out, we might have lost a local rename. - if (syncItem->_instruction == CSYNC_INSTRUCTION_REMOVE && syncItem->_direction == SyncFileItem::Up) { - return true; - } - } - return false; + return std::ranges::any_of(_syncItems, [](const auto &syncItem) { + return syncItem->_instruction == CSYNC_INSTRUCTION_REMOVE && syncItem->_direction == SyncFileItem::Up; + }); } -bool SyncEngine::handleMassDeletion() +int SyncEngine::countDeletedFiles() const { - const auto displayDialog = ConfigFile().promptDeleteFiles() && !_syncOptions.isCmd(); - const auto allFilesDeleted = !_hasNoneFiles && _hasRemoveFile; - - auto deletionCounter = 0; + auto counter = 0; for (const auto &oneItem : std::as_const(_syncItems)) { - if (oneItem->_instruction == CSYNC_INSTRUCTION_REMOVE) { - if (oneItem->isDirectory()) { - const auto result = _journal->listFilesInPath(oneItem->_file.toUtf8(), [&deletionCounter] (const auto &oneRecord) { - if (oneRecord.isFile()) { - ++deletionCounter; - } - }); - if (!result) { - qCDebug(lcEngine()) << "unable to find the number of files within a deleted folder:" << oneItem->_file; + if (oneItem->_instruction != CSYNC_INSTRUCTION_REMOVE) { + continue; + } + if (oneItem->isDirectory()) { + const auto result = _journal->listFilesInPath(oneItem->_file.toUtf8(), [&counter](const auto &oneRecord) { + if (oneRecord.isFile()) { + ++counter; } - } else { - ++deletionCounter; + }); + if (!result) { + qCDebug(lcEngine()) << "unable to find the number of files within a deleted folder:" << oneItem->_file; } + } else { + ++counter; } } - const auto filesDeletedThresholdExceeded = deletionCounter > ConfigFile().deleteFilesThreshold(); + return counter; +} + +bool SyncEngine::handleMassDeletion() +{ + const auto displayDialog = ConfigFile().promptDeleteFiles() && !_syncOptions.isCmd(); + const auto allFilesDeleted = !_hasNoneFiles && _hasRemoveFile; - if ((allFilesDeleted || filesDeletedThresholdExceeded) && displayDialog) { + if (const auto filesDeletedThresholdExceeded = countDeletedFiles() > ConfigFile().deleteFilesThreshold(); + (allFilesDeleted || filesDeletedThresholdExceeded) && displayDialog) { 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)) { @@ -1185,7 +1200,7 @@ bool SyncEngine::handleMassDeletion() } } - promptUserBeforePropagation([this, side](auto &&callback){ + promptUserBeforePropagation([this, side](auto &callback){ emit aboutToRemoveAllFiles(side >= 0 ? SyncFileItem::Down : SyncFileItem::Up, callback); }); return true; @@ -1210,7 +1225,7 @@ void SyncEngine::handleRemnantReadOnlyFolders() slotAddTouchedFile(_localPath + oneFolder->_file); if (oneFolder->_type == ItemType::ItemTypeDirectory) { - const auto deletionCallback = [this] (const QString &deleteItem, bool) { + const auto deletionCallback = [this] (const QString &deleteItem, const bool) { slotAddTouchedFile(deleteItem); }; @@ -1225,9 +1240,9 @@ void SyncEngine::handleRemnantReadOnlyFolders() template void SyncEngine::promptUserBeforePropagation(T &&lambda) { - QPointer guard = new QObject(); - QPointer self = this; - auto callback = [this, self, guard](bool cancel) -> void { + const QPointer guard = new QObject(this); + const QPointer self = this; + auto callback = [this, self, guard](const bool cancel) { // use a guard to ensure its only called once... // qpointer to self to ensure we still exist if (!guard || !self) { @@ -1274,12 +1289,12 @@ void SyncEngine::slotClearTouchedFiles() void SyncEngine::addAcceptedInvalidFileName(const QString& filePath) { - _leadingAndTrailingSpacesFilesAllowed.append(filePath); + _spacesFilesAllowed.append(filePath); } -void SyncEngine::setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value) +void SyncEngine::setEnforceWindowsFilenameCompat(const bool value) { - _shouldEnforceWindowsFileNameCompatibility = value; + _enforceWindowsFilenameCompat = value; } bool SyncEngine::wasFileTouched(const QString &fn) const @@ -1334,7 +1349,7 @@ const SyncEngine::SingleItemDiscoveryOptions &SyncEngine::singleItemDiscoveryOpt return _singleItemDiscoveryOptions; } -void SyncEngine::setFilesystemPermissionsReliable(bool reliable) +void SyncEngine::setFilesystemPermsReliable(const bool reliable) { _filesystemPermissionsReliable = reliable; } @@ -1572,7 +1587,7 @@ QHash SyncEngine::groupNeededScheduledS it != _discoveryPhase->_filesNeedingScheduledSync.cend(); ++it) { - const auto file = it.key(); + const auto &file = it.key(); const auto syncScheduledSecs = it.value(); // We don't want to schedule syncs again for files we have already discovered needing a diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index a84b92c8be1bd..2200078a4ff57 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -125,7 +125,7 @@ class OWNCLOUDSYNC_EXPORT SyncEngine : public QObject [[nodiscard]] QSharedPointer getPropagator() const { return _propagator; } // for the test [[nodiscard]] const SyncEngine::SingleItemDiscoveryOptions &singleItemDiscoveryOptions() const; - void setFilesystemPermissionsReliable(bool reliable); + void setFilesystemPermsReliable(bool reliable); public slots: void setSingleItemDiscoveryOptions(const OCC::SyncEngine::SingleItemDiscoveryOptions &singleItemDiscoveryOptions); @@ -152,7 +152,7 @@ public slots: */ void setLocalDiscoveryOptions(OCC::LocalDiscoveryEnums::LocalDiscoveryStyle style, std::set paths = {}); void addAcceptedInvalidFileName(const QString& filePath); - void setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value); + void setEnforceWindowsFilenameCompat(const bool value); signals: // During update, before reconcile @@ -363,6 +363,14 @@ private slots: void finishSync(); + void updateVirtualFileReadOnlyState(const SyncFileItemPtr &item, const QString &filePath) const; + + // Returns false if decryption fails (caller should abort startSync in that case). + bool unlockE2eeFolders(const QList> &e2EeLockedFolders); + + // Count files that will be deleted in the current sync pass. + [[nodiscard]] int countDeletedFiles() const; + [[nodiscard]] bool shouldRestartSync() const; bool handleMassDeletion(); @@ -405,8 +413,8 @@ private slots: LocalDiscoveryStyle _localDiscoveryStyle = LocalDiscoveryStyle::FilesystemOnly; std::set _localDiscoveryPaths; - QStringList _leadingAndTrailingSpacesFilesAllowed; - bool _shouldEnforceWindowsFileNameCompatibility = false; + QStringList _spacesFilesAllowed; + bool _enforceWindowsFilenameCompat = false; // 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. diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index 503454e4d785e..063428efbce84 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -2296,7 +2296,7 @@ private slots: FakeFolder fakeFolder{FileInfo{}}; fakeFolder.enableEnforceWindowsFileNameCompatibility(); - fakeFolder.syncEngine().setLocalDiscoveryEnforceWindowsFileNameCompatibility(true); + fakeFolder.syncEngine().setEnforceWindowsFilenameCompat(true); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());