소스 검색

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 <kevin.ottens@nextcloud.com>
Christian Kamm 5 년 전
부모
커밋
00e901f5a7
9개의 변경된 파일151개의 추가작업 그리고 9개의 파일을 삭제
  1. 17 0
      src/common/syncjournaldb.cpp
  2. 7 0
      src/common/syncjournaldb.h
  3. 1 1
      src/common/utility.cpp
  4. 5 3
      src/common/utility.h
  5. 114 2
      src/gui/socketapi.cpp
  6. 3 0
      src/gui/socketapi.h
  7. 1 0
      src/libsync/filesystem.h
  8. 1 1
      src/libsync/syncengine.cpp
  9. 2 2
      test/testsyncconflict.cpp

+ 17 - 0
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);

+ 7 - 0
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,

+ 1 - 1
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

+ 5 - 3
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);

+ 114 - 2
src/gui/socketapi.cpp

@@ -50,7 +50,7 @@
 #include <QStringBuilder>
 #include <QMessageBox>
 #include <QInputDialog>
-
+#include <QFileDialog>
 #include <QClipboard>
 #include <QDesktopServices>
 
@@ -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 <i>%1</i> and all its contents permanently?").arg(info.dir().dirName())
+            : tr("Do you want to delete the file <i>%1</i> 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"));
 }

+ 3 - 0
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

+ 1 - 0
src/libsync/filesystem.h

@@ -18,6 +18,7 @@
 
 #include <QString>
 #include <ctime>
+#include <functional>
 
 #include <owncloudlib.h>
 // Chain in the base include and extend the namespace

+ 1 - 1
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;

+ 2 - 2
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()