From: Christian Kamm Date: Wed, 7 Oct 2020 12:51:04 +0000 (+0200) Subject: SocketAPI: Introduce conflict resolution actions #6252 X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~22^2~95^2~9 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=00e901f5a712e778c987b71a09b5816450bce8d6;p=nextcloud-desktop.git SocketAPI: Introduce conflict resolution actions #6252 For conflicts generally as well as new files in read-only directories the context menu will now present delete and move options. Signed-off-by: Kevin Ottens --- diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 1666c4f82..83da1d4cc 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -2031,6 +2031,23 @@ QByteArrayList SyncJournalDb::conflictRecordPaths() return paths; } +QByteArray SyncJournalDb::conflictFileBaseName(const QByteArray &conflictName) +{ + auto conflict = conflictRecord(conflictName); + QByteArray result; + if (conflict.isValid()) { + getFileRecordsByFileId(conflict.baseFileId, [&result](const SyncJournalFileRecord &record) { + if (!record._path.isEmpty()) + result = record._path; + }); + } + + if (result.isEmpty()) { + result = Utility::conflictFileBaseNameFromPattern(conflictName); + } + return result; +} + void SyncJournalDb::clearFileTable() { QMutexLocker lock(&_mutex); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index ef4feaf98..17eb8dee6 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -225,6 +225,13 @@ public: /// Return all paths of files with a conflict tag in the name and records in the db QByteArrayList conflictRecordPaths(); + /** Find the base name for a conflict file name, using journal or name pattern + * + * The path must be sync-folder relative. + * + * Will return an empty string if it's not even a conflict file by pattern. + */ + QByteArray conflictFileBaseName(const QByteArray &conflictName); /** * Delete any file entry. This will force the next sync to re-sync everything as if it was new, diff --git a/src/common/utility.cpp b/src/common/utility.cpp index 6dff6a7e8..3c6fe06b4 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -641,7 +641,7 @@ bool Utility::isConflictFile(const QString &name) return false; } -QByteArray Utility::conflictFileBaseName(const QByteArray &conflictName) +QByteArray Utility::conflictFileBaseNameFromPattern(const QByteArray &conflictName) { // This function must be able to deal with conflict files for conflict files. // To do this, we scan backwards, for the outermost conflict marker and diff --git a/src/common/utility.h b/src/common/utility.h index 36a127757..1058cf719 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -42,6 +42,8 @@ class QSettings; namespace OCC { +class SyncJournal; + Q_DECLARE_LOGGING_CATEGORY(lcUtility) /** \addtogroup libsync @@ -215,14 +217,14 @@ namespace Utility { OCSYNC_EXPORT bool isConflictFile(const char *name); OCSYNC_EXPORT bool isConflictFile(const QString &name); - /** Find the base name for a conflict file name + /** Find the base name for a conflict file name, using name pattern only * * Will return an empty string if it's not a conflict file. * * Prefer to use the data from the conflicts table in the journal to determine - * a conflict's base file. + * a conflict's base file, see SyncJournal::conflictFileBaseName() */ - OCSYNC_EXPORT QByteArray conflictFileBaseName(const QByteArray &conflictName); + OCSYNC_EXPORT QByteArray conflictFileBaseNameFromPattern(const QByteArray &conflictName); #ifdef Q_OS_WIN OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName); diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp index 2632a42a5..3d9b43d13 100644 --- a/src/gui/socketapi.cpp +++ b/src/gui/socketapi.cpp @@ -50,7 +50,7 @@ #include #include #include - +#include #include #include @@ -689,6 +689,68 @@ void SocketApi::copyUrlToClipboard(const QString &link) QApplication::clipboard()->setText(link); } +void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *) +{ + QFileInfo info(localFile); + + auto result = QMessageBox::question( + nullptr, tr("Confirm deletion"), + info.isDir() + ? tr("Do you want to delete the directory %1 and all its contents permanently?").arg(info.dir().dirName()) + : tr("Do you want to delete the file %1 permanently?").arg(info.fileName()), + QMessageBox::Yes, QMessageBox::No); + if (result != QMessageBox::Yes) + return; + + if (info.isDir()) { + FileSystem::removeRecursively(localFile); + } else { + QFile(localFile).remove(); + } +} + +void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *) +{ + const auto fileData = FileData::get(localFile); + const auto parentDir = fileData.parentFolder(); + if (!fileData.folder) + return; // should not have shown menu item + + QString defaultDirAndName = fileData.folderRelativePath; + + // If it's a conflict, we want to save it under the base name by default + if (Utility::isConflictFile(defaultDirAndName)) { + defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8()); + } + + // If the parent doesn't accept new files, go to the root of the sync folder + QFileInfo fileInfo(localFile); + const auto parentRecord = parentDir.journalRecord(); + if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) + || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) { + defaultDirAndName = QFileInfo(defaultDirAndName).fileName(); + } + + // Add back the folder path + defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName); + + const auto target = QFileDialog::getSaveFileName( + nullptr, + tr("Select new location..."), + defaultDirAndName, + QString(), nullptr, QFileDialog::HideNameFilterDetails); + if (target.isEmpty()) + return; + + QString error; + if (!FileSystem::uncheckedRenameReplace(localFile, target, &error)) { + qCWarning(lcSocketApi) << "Rename error:" << error; + QMessageBox::warning( + nullptr, tr("Error"), + tr("Moving file failed:\n\n%1").arg(error)); + } +} + void SocketApi::emailPrivateLink(const QString &link) { Utility::openEmailComposer( @@ -795,12 +857,18 @@ SyncJournalFileRecord SocketApi::FileData::journalRecord() const return record; } +SocketApi::FileData SocketApi::FileData::parentFolder() const +{ + return FileData::get(QFileInfo(localPath).dir().path().toUtf8()); +} + void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener) { listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN")); bool hasSeveralFiles = argument.contains(QLatin1Char('\x1e')); // Record Separator FileData fileData = hasSeveralFiles ? FileData{} : FileData::get(argument); - bool isOnTheServer = fileData.journalRecord().isValid(); + const auto record = fileData.journalRecord(); + const bool isOnTheServer = record.isValid(); const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty(); auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:"); @@ -814,6 +882,50 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe } sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath); + + // Conflict files get conflict resolution actions + bool isConflict = Utility::isConflictFile(fileData.folderRelativePath); + if (isConflict || !isOnTheServer) { + // Check whether this new file is in a read-only directory + QFileInfo fileInfo(fileData.localPath); + const auto parentDir = fileData.parentFolder(); + const auto parentRecord = parentDir.journalRecord(); + const bool canAddToDir = + (fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile)) + || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories)); + const bool canChangeFile = + !isOnTheServer + || (record._remotePerm.hasPermission(RemotePermissions::CanDelete) + && record._remotePerm.hasPermission(RemotePermissions::CanMove) + && record._remotePerm.hasPermission(RemotePermissions::CanRename)); + + if (isConflict && canChangeFile) { + if (canAddToDir) { + if (isOnTheServer) { + // Conflict file that is already uploaded + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Rename...")); + } else { + // Local-only conflict file + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Rename and upload...")); + } + } else { + if (isOnTheServer) { + // Uploaded conflict file in read-only directory + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename...")); + } else { + // Local-only conflict file in a read-only dir + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload...")); + } + } + listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes")); + } + + // File in a read-only directory? + if (!isConflict && !isOnTheServer && !canAddToDir) { + listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload...")); + listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete")); + } + } } listener->sendMessage(QString("GET_MENU_ITEMS:END")); } diff --git a/src/gui/socketapi.h b/src/gui/socketapi.h index ddacc5620..e1d11f919 100644 --- a/src/gui/socketapi.h +++ b/src/gui/socketapi.h @@ -79,6 +79,7 @@ private: static FileData get(const QString &localFile); SyncFileStatus syncFileStatus() const; SyncJournalFileRecord journalRecord() const; + FileData parentFolder() const; Folder *folder; QString localPath; @@ -105,6 +106,8 @@ private: Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener); // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 #ifdef Q_OS_WIN diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index 2dbbdb5c8..70dffef42 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -18,6 +18,7 @@ #include #include +#include #include // Chain in the base include and extend the namespace diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 6ff093ff4..fd980e15d 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -359,7 +359,7 @@ void SyncEngine::conflictRecordMaintenance() record.path = bapath; // Determine fileid of target file - auto basePath = Utility::conflictFileBaseName(bapath); + auto basePath = Utility::conflictFileBaseNameFromPattern(bapath); SyncJournalFileRecord baseRecord; if (_journal->getFileRecord(basePath, &baseRecord) && baseRecord.isValid()) { record.baseFileId = baseRecord._fileId; diff --git a/test/testsyncconflict.cpp b/test/testsyncconflict.cpp index 2cd318164..9bed0e01b 100644 --- a/test/testsyncconflict.cpp +++ b/test/testsyncconflict.cpp @@ -125,7 +125,7 @@ private slots: QVERIFY(conflictMap.contains(a1FileId)); QVERIFY(conflictMap.contains(a2FileId)); QCOMPARE(conflictMap.size(), 2); - QCOMPARE(Utility::conflictFileBaseName(conflictMap[a1FileId].toUtf8()), QByteArray("A/a1")); + QCOMPARE(Utility::conflictFileBaseNameFromPattern(conflictMap[a1FileId].toUtf8()), QByteArray("A/a1")); // Check that the conflict file contains the username QVERIFY(conflictMap[a1FileId].contains(QString("(conflicted copy %1 ").arg(fakeFolder.syncEngine().account()->davDisplayName()))); @@ -384,7 +384,7 @@ private slots: { QFETCH(QString, input); QFETCH(QString, output); - QCOMPARE(Utility::conflictFileBaseName(input.toUtf8()), output.toUtf8()); + QCOMPARE(Utility::conflictFileBaseNameFromPattern(input.toUtf8()), output.toUtf8()); } void testLocalDirRemoteFileConflict()