diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 5a7495d21d011..8135edff975f6 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -31,6 +31,7 @@ namespace constexpr const char *editorNamesForDelayedUpload[] = {"PowerPDF"}; constexpr const char *fileExtensionsToCheckIfOpenForSigning[] = {".pdf"}; constexpr auto delayIntervalForSyncRetryForOpenedForSigningFilesSeconds = 60; +constexpr auto delayIntervalForSyncRetryForFilesExceedQuotaSeconds = 60; } namespace OCC { @@ -766,6 +767,9 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it item->_isLivePhoto = serverEntry.isLivePhoto; item->_livePhotoFile = serverEntry.livePhotoFile; + item->_folderQuota.bytesUsed = serverEntry.folderQuota.bytesUsed; + item->_folderQuota.bytesAvailable = serverEntry.folderQuota.bytesAvailable; + // Check for missing server data { QStringList missingData; @@ -1072,6 +1076,40 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); } +int64_t ProcessDirectoryJob::folderQuotaAvailable(const SyncFileItemPtr &item) +{ + const auto unlimitedFreeSpace = -3; + if (const auto isNewItem = item->_instruction == CSYNC_INSTRUCTION_NEW; + _queryServer != QueryMode::InBlackList && _queryServer != QueryMode::ParentDontExist + && !item->isDirectory() + && item->_direction == SyncFileItem::Up + && item->_size > 0 + && (item->_instruction == CSYNC_INSTRUCTION_SYNC || isNewItem)) { + + const auto unknownFreeSpace = -2; + const auto bytesAvailable = _folderQuota.bytesAvailable; + + if (!_dirItem) { + return bytesAvailable; + } + + const auto isDirItemRenamed = _dirItem && _dirItem->_instruction == CSYNC_INSTRUCTION_RENAME; + + // quota is unknown at this point + if (isDirItemRenamed && isNewItem) { + return unknownFreeSpace; + } + + if (isDirItemRenamed) { + return bytesAvailable; + } + + return _dirItem->_folderQuota.bytesAvailable; + } + + return unlimitedFreeSpace; +} + void ProcessDirectoryJob::processFileAnalyzeLocalInfo( const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry, QueryMode recurseQueryServer) @@ -1126,6 +1164,25 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_status = SyncFileItem::Status::NormalError; } + if (const auto folderQuota = folderQuotaAvailable(item);item->_size > folderQuota && folderQuota > -1) { + qCDebug(lcDisco) << "Folder" << item->_file + << "- quota used: " << serverEntry.folderQuota.bytesUsed + << "- quota available: " << serverEntry.folderQuota.bytesAvailable; + item->_instruction = CSYNC_INSTRUCTION_ERROR; + if (_currentFolder._server.isEmpty()) { + item->_errorString = tr("Upload of %1 exceeds %2 of space left in personal files.").arg(Utility::octetsToString(item->_size), + Utility::octetsToString(folderQuota)); + } else { + item->_errorString = tr("Upload of %1 exceeds %2 of space left in folder %3.").arg(Utility::octetsToString(item->_size), + Utility::octetsToString(folderQuota), + _currentFolder._server); + } + + item->_status = SyncFileItem::Status::NormalError; + _discoveryData->_anotherSyncNeeded = true; + _discoveryData->_filesNeedingScheduledSync.insert(path._original, delayIntervalForSyncRetryForFilesExceedQuotaSeconds); + } + if (item->_type != CSyncEnums::ItemTypeVirtualFile) { const auto foundEditorsKeepingFileBusy = queryEditorsKeepingFileBusy(item, path); if (!foundEditorsKeepingFileBusy.isEmpty()) { @@ -2153,6 +2210,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() } connect(serverJob, &DiscoverySingleDirectoryJob::etag, this, &ProcessDirectoryJob::etag); + connect(serverJob, &DiscoverySingleDirectoryJob::setfolderQuota, this, &ProcessDirectoryJob::setFolderQuota); _discoveryData->_currentlyActiveJobs++; _pendingAsyncJobs++; connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) { @@ -2179,6 +2237,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() _serverQueryDone = true; if (!serverJob->_dataFingerprint.isEmpty() && _discoveryData->_dataFingerprint.isEmpty()) _discoveryData->_dataFingerprint = serverJob->_dataFingerprint; + if (_localQueryDone) this->process(); } else { @@ -2209,6 +2268,12 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() return serverJob; } +void ProcessDirectoryJob::setFolderQuota(const FolderQuota &folderQuota) +{ + _folderQuota.bytesUsed = folderQuota.bytesUsed; + _folderQuota.bytesAvailable = folderQuota.bytesAvailable; +} + void ProcessDirectoryJob::startAsyncLocalQuery() { QString localPath = _discoveryData->_localDir + _currentFolder._local; @@ -2337,5 +2402,4 @@ bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &abso } return result; } - } diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index 715b416de5eaa..cf441373c9e02 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -296,9 +296,16 @@ class ProcessDirectoryJob : public QObject PinState _pinState = PinState::Unspecified; // The directory's pin-state, see computePinState() bool _isInsideEncryptedTree = false; // this directory is encrypted or is within the tree of directories with root directory encrypted + FolderQuota _folderQuota; + + int64_t folderQuotaAvailable(const SyncFileItemPtr &item); + signals: void finished(); // The root etag of this directory was fetched void etag(const QByteArray &, const QDateTime &time); + +private slots: + void setFolderQuota(const FolderQuota &folderQuota); }; } diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 8c0a108188f34..94e89ae691b49 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -409,6 +409,8 @@ void DiscoverySingleDirectoryJob::start() << "getlastmodified" << "getcontentlength" << "getetag" + << "quota-available-bytes" + << "quota-used-bytes" << "http://owncloud.org/ns:size" << "http://owncloud.org/ns:id" << "http://owncloud.org/ns:fileid" @@ -581,6 +583,14 @@ static void propertyMapToRemoteInfo(const QMap &map, RemotePer if (result.isDirectory && map.contains("size")) { result.sizeOfFolder = map.value("size").toInt(); } + + if (result.isDirectory && map.contains("quota-used-bytes")) { + result.folderQuota.bytesUsed = map.value("quota-used-bytes").toLongLong(); + } + + if (result.isDirectory && map.contains("quota-available-bytes")) { + result.folderQuota.bytesAvailable = map.value("quota-available-bytes").toLongLong(); + } } void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &file, const QMap &map) @@ -615,11 +625,22 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi if (map.contains("size")) { _size = map.value("size").toInt(); } + + // all folders will contain both + if (map.contains("quota-used-bytes") && map.contains("quota-available-bytes")) { + emit setfolderQuota(FolderQuota{map.value("quota-used-bytes").toLongLong(), map.value("quota-available-bytes").toLongLong()}); + } } else { RemoteInfo result; int slash = file.lastIndexOf('/'); result.name = file.mid(slash + 1); result.size = -1; + if (map.contains("quota-used-bytes")) { + result.folderQuota.bytesUsed = map.value("quota-used-bytes").toInt(); + } + if (map.contains("quota-available-bytes")) { + result.folderQuota.bytesAvailable = map.value("quota-available-bytes").toInt(); + } propertyMapToRemoteInfo(map, _account->serverHasMountRootProperty() ? RemotePermissions::MountedPermissionAlgorithm::UseMountRootProperty : RemotePermissions::MountedPermissionAlgorithm::WildGuessMountedSubProperty, result); diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index a29937d1f0b78..521967a8dd003 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -45,6 +45,20 @@ class ProcessDirectoryJob; enum class ErrorCategory; +/** + * Represent the quota for each folder retrieved from the server + * bytesUsed: space used in bytes + * bytesAvailale: free space available in bytes or + * -1: Uncomputed free space - new folder (externally created) not yet scanned by the server + * -2: Unknown free space + * -3: Unlimited free space. + */ +struct FolderQuota +{ + int64_t bytesUsed = 0; + int64_t bytesAvailable = 0; +}; + /** * Represent all the meta-data about a file in the server */ @@ -83,6 +97,8 @@ struct RemoteInfo bool isLivePhoto = false; QString livePhotoFile; + + FolderQuota folderQuota; }; struct LocalInfo @@ -164,6 +180,7 @@ class DiscoverySingleDirectoryJob : public QObject void firstDirectoryPermissions(OCC::RemotePermissions); void etag(const QByteArray &, const QDateTime &time); void finished(const OCC::HttpResult> &result); + void setfolderQuota(const FolderQuota &folderQuota); private slots: void directoryListingIteratedSlot(const QString &, const QMap &); @@ -208,6 +225,7 @@ private slots: public: QByteArray _dataFingerprint; + FolderQuota _folderQuota; }; class DiscoveryPhase : public QObject diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 9ca1a3b713bc2..7e878c476c232 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -753,8 +753,9 @@ void PropagateUploadFileCommon::slotJobDestroyed(QObject *job) // This function is used whenever there is an error occurring and jobs might be in progress void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error) { - if (_aborting) + if (_aborting) { return; + } abort(AbortType::Synchronous); done(status, error); } diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 47c98444390a7..23dd4b9f26b80 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -1442,8 +1442,9 @@ void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientRemoteStorage() { auto msg = tr("There is insufficient space available on the server for some uploads."); - if (_uniqueErrors.contains(msg)) + if (_uniqueErrors.contains(msg)) { return; + } _uniqueErrors.insert(msg); emit syncError(msg, ErrorCategory::InsufficientRemoteStorage); diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 8ff5eeedcf482..aacacfab2722e 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -343,6 +343,13 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem /// if true, requests the file to be permanently deleted instead of moved to the trashbin /// only relevant for when `_instruction` is set to `CSYNC_INSTRUCTION_REMOVE` bool _wantsPermanentDeletion = false; + + struct FolderQuota { + int64_t bytesUsed = 0; + int64_t bytesAvailable = 0; + }; + + FolderQuota _folderQuota; }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 58fecc6e8e6cd..745cbbe5d3c8d 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -406,6 +406,8 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate); xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size)); xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag))); + xml.writeTextElement(ocUri, QStringLiteral("quota-available-bytes"), std::to_string(fileInfo.quota.bytesAvailable)); + xml.writeTextElement(ocUri, QStringLiteral("quota-used-bytes"), std::to_string(fileInfo.quota.bytesUsed)); xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); xml.writeTextElement(ocUri, QStringLiteral("share-permissions"), QString::number(static_cast(OCC::SharePermissions(OCC::SharePermissionRead | OCC::SharePermissionUpdate | diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index 44ed5edda7e3d..2a70f54b2d260 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -192,6 +192,12 @@ class FileInfo : public FileModifier bool isEncrypted = false; bool isLivePhoto = false; + struct FileInfoQuota { + int64_t bytesUsed = 0; + int64_t bytesAvailable = 5000000000; + }; + FileInfoQuota quota; + // Sorted by name to be able to compare trees QMap children; QString parentPath;