From 6affc6e6aba2580a9e83b67f6db7a41dda2afcf7 Mon Sep 17 00:00:00 2001 From: allexzander Date: Fri, 21 Oct 2022 17:11:29 +0300 Subject: [PATCH] Implement 'Leave this share' context menu entry. Fix incorrect sharing state for incoming and my shares in custom state icons and local database. Signed-off-by: allexzander --- src/common/syncjournaldb.cpp | 16 +- src/common/syncjournalfilerecord.h | 3 +- src/gui/folderman.cpp | 15 ++ src/gui/folderman.h | 3 + src/gui/shellextensionsserver.cpp | 227 ++++++++++----------------- src/gui/shellextensionsserver.h | 7 +- src/gui/socketapi/socketapi.cpp | 16 ++ src/gui/socketapi/socketapi.h | 18 +-- src/libsync/account.cpp | 7 +- src/libsync/account.h | 7 + src/libsync/bulkpropagatorjob.cpp | 4 +- src/libsync/discovery.cpp | 13 +- src/libsync/discoveryphase.cpp | 1 + src/libsync/discoveryphase.h | 1 + src/libsync/propagateremotemkdir.cpp | 7 +- src/libsync/syncfileitem.cpp | 6 +- src/libsync/syncfileitem.h | 4 +- test/testfolderman.cpp | 102 +++++++++++- 18 files changed, 280 insertions(+), 177 deletions(-) diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index d5ede0f01..cc4f25fd4 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -49,7 +49,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg) #define GET_FILE_RECORD_QUERY \ "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \ " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \ - " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \ + " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap, sharedByMe" \ " FROM metadata" \ " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" @@ -75,7 +75,8 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que rec._lockstate._lockTime = query.int64Value(17); rec._lockstate._lockTimeout = query.int64Value(18); rec._isShared = query.intValue(19) > 0; - rec._lastShareStateFetchedTimestmap = query.int64Value(20); + rec._lastShareStateFetchedTimestamp = query.int64Value(20); + rec._sharedByMe = query.intValue(21) > 0; } static QByteArray defaultJournalMode(const QString &dbPath) @@ -731,6 +732,7 @@ bool SyncJournalDb::updateMetadataTableStructure() addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER")); + addColumn(QStringLiteral("sharedByMe"), QStringLiteral("INTEGER")); auto uploadInfoColumns = tableColumns("uploadinfo"); if (uploadInfoColumns.isEmpty()) @@ -894,8 +896,9 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & << "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId << "lock editor:" << record._lockstate._lockEditorApp + << "sharedByMe:" << record._sharedByMe << "isShared:" << record._isShared - << "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap; + << "lastShareStateFetchedTimestamp:" << record._lastShareStateFetchedTimestamp; const qint64 phash = getPHash(record._path); if (!checkConnect()) { @@ -921,8 +924,8 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata " "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, " "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, " - "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap) " - "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27);"), + "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap, sharedByMe) " + "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28);"), _db); if (!query) { return query->error(); @@ -954,7 +957,8 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(24, record._lockstate._lockTime); query->bindValue(25, record._lockstate._lockTimeout); query->bindValue(26, record._isShared); - query->bindValue(27, record._lastShareStateFetchedTimestmap); + query->bindValue(27, record._lastShareStateFetchedTimestamp); + query->bindValue(28, record._sharedByMe); if (!query->exec()) { return query->error(); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index 4e011e616..a846aeeb1 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -82,7 +82,8 @@ public: bool _isE2eEncrypted = false; SyncJournalFileLockInfo _lockstate; bool _isShared = false; - qint64 _lastShareStateFetchedTimestmap = 0; + qint64 _lastShareStateFetchedTimestamp = 0; + bool _sharedByMe = false; }; bool OCSYNC_EXPORT diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 003129507..763cb61c1 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -1451,6 +1451,21 @@ void FolderMan::setDirtyNetworkLimits() } } +void FolderMan::leaveShare(const QString &localFile) +{ + if (const auto folder = FolderMan::instance()->folderForPath(localFile)) { + const auto filePathRelative = QString(localFile).remove(folder->path()); + + const auto leaveShareJob = new SimpleApiJob(folder->accountState()->account(), folder->accountState()->account()->davPath() + filePathRelative); + leaveShareJob->setVerb(SimpleApiJob::Verb::Delete); + connect(leaveShareJob, &SimpleApiJob::resultReceived, this, [this, folder](int statusCode) { + Q_UNUSED(statusCode) + scheduleFolder(folder); + }); + leaveShareJob->start(); + } +} + void FolderMan::trayOverallStatus(const QList &folders, SyncResult::Status *status, bool *unresolvedConflicts) { diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 37d91e4d7..5d85b18d1 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -215,6 +215,9 @@ public: void setDirtyProxy(); void setDirtyNetworkLimits(); + /** removes current user from the share **/ + void leaveShare(const QString &localFile); + signals: /** * signal to indicate a folder has changed its sync state. diff --git a/src/gui/shellextensionsserver.cpp b/src/gui/shellextensionsserver.cpp index 2b64c2b99..b01583116 100644 --- a/src/gui/shellextensionsserver.cpp +++ b/src/gui/shellextensionsserver.cpp @@ -19,7 +19,6 @@ #include #include "folder.h" #include "folderman.h" -#include "ocssharejob.h" #include #include #include @@ -28,7 +27,6 @@ namespace { constexpr auto isSharedInvalidationInterval = 2 * 60 * 1000; // 2 minutes, so we don't make fetch sharees requests too often -constexpr auto folderAliasPropertyKey = "folderAlias"; } namespace OCC { @@ -102,6 +100,7 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons sendEmptyDataAndCloseSession(socket); return; } + const auto filePathRelative = QString(customStateRequestInfo.path).remove(folder->path()); SyncJournalFileRecord record; @@ -123,43 +122,14 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons QVariantMap{{VfsShellExtensions::Protocol::CustomStateStatesKey, states}}}}; }; - if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestmap < _isSharedInvalidationInterval) { - qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestmap has less than " << _isSharedInvalidationInterval << " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal."; + if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestamp < _isSharedInvalidationInterval) { + qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestamp has less than " << _isSharedInvalidationInterval << " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal."; sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record)); closeSession(socket); return; } - const auto job = new OcsShareJob(folder->accountState()->account()); - job->setProperty(folderAliasPropertyKey, customStateRequestInfo.folderAlias); - connect(job, &OcsShareJob::shareJobFinished, this, &ShellExtensionsServer::slotSharesFetched); - connect(job, &OcsJob::ocsError, this, &ShellExtensionsServer::slotSharesFetchError); - - { - _customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::fetchSharesJobFinished, [this, socket, filePathRelative, composeMessageReplyFromRecord](const QString &folderAlias) { - { - const auto connection = _customStateSocketConnections[socket->socketDescriptor()]; - if (connection) { - QObject::disconnect(connection); - } - _customStateSocketConnections.remove(socket->socketDescriptor()); - } - - const auto folder = FolderMan::instance()->folder(folderAlias); - SyncJournalFileRecord record; - if (!folder || !folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) { - qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative; - sendEmptyDataAndCloseSession(socket); - return; - } - - qCInfo(lcShellExtServer) << "Sending reply from OcsShareJob for socket: " << socket->socketDescriptor() << " and record: " << record.path(); - sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record)); - closeSession(socket); - })); - } - - const auto sharesPath = [&record, folder, &filePathRelative]() { + const auto lsColJobPath = [folder, &filePathRelative]() { const auto filePathRelativeRemote = QDir(folder->remotePath()).filePath(filePathRelative); // either get parent's path, or, return '/' if we are in the root folder auto recordPathSplit = filePathRelativeRemote.split(QLatin1Char('/'), Qt::SkipEmptyParts); @@ -170,13 +140,88 @@ void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, cons return QStringLiteral("/"); }(); - if (!_runningFetchShareJobsForPaths.contains(sharesPath)) { - _runningFetchShareJobsForPaths.push_back(sharesPath); - qCInfo(lcShellExtServer) << "Started OcsShareJob for path: " << sharesPath; - job->getShares(sharesPath, {{QStringLiteral("subfiles"), QStringLiteral("true")}}); - } else { - qCInfo(lcShellExtServer) << "OcsShareJob is already running for path: " << sharesPath; + if (_runningLsColJobsForPaths.contains(lsColJobPath)) { + qCInfo(lcShellExtServer) << "LsColJob is already running for path: " << lsColJobPath; + sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record)); + closeSession(socket); + return; } + + _customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::directoryListingIterationFinished, [this, socket, filePathRelative, composeMessageReplyFromRecord](const QString &folderAlias) { + { + const auto connection = _customStateSocketConnections[socket->socketDescriptor()]; + if (connection) { + QObject::disconnect(connection); + } + _customStateSocketConnections.remove(socket->socketDescriptor()); + } + + const auto folder = FolderMan::instance()->folder(folderAlias); + SyncJournalFileRecord record; + if (!folder || !folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) { + qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative; + sendEmptyDataAndCloseSession(socket); + return; + } + qCInfo(lcShellExtServer) << "Sending reply from LsColJob for socket: " << socket->socketDescriptor() << " and record: " << record.path(); + sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record)); + closeSession(socket); + })); + + auto *const lsColJob = new LsColJob(folder->accountState()->account(), QDir::cleanPath(folder->remotePath() + lsColJobPath), this); + lsColJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")}); + + const auto folderAlias = customStateRequestInfo.folderAlias; + + QObject::connect(lsColJob, &LsColJob::directoryListingIterated, this, [this, folderAlias, lsColJobPath](const QString &name, const QMap &properties) { + const auto folder = FolderMan::instance()->folder(folderAlias); + + if (!folder) { + qCWarning(lcShellExtServer) << "No folder found for folderAlias: " << folderAlias; + return; + } + + SyncJournalFileRecord record; + const auto filePathWithoutDavPath = QString(name).remove(folder->accountState()->account()->davPathRoot()); + const auto filePathAdjusted = (filePathWithoutDavPath.size() > 1 && filePathWithoutDavPath.startsWith(QLatin1Char('/'))) ? filePathWithoutDavPath.mid(1) : filePathWithoutDavPath; + if (filePathAdjusted.isEmpty() || filePathAdjusted == lsColJobPath) { + // we are skipping the first item as it is the current path, but we are interested in nested items + return; + } + if (!folder || !folder->journalDb()->getFileRecord(filePathAdjusted, &record) || !record.isValid()) { + return; + } + + const auto isIncomingShare = properties.contains(QStringLiteral("permissions")) && RemotePermissions::fromServerString(properties.value(QStringLiteral("permissions"))).hasPermission(OCC::RemotePermissions::IsShared); + + const auto sharedByMe = !properties.value(QStringLiteral("share-types")).isEmpty(); + + record._sharedByMe = sharedByMe; + + record._isShared = isIncomingShare || sharedByMe; + record._lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); + + if (!folder->journalDb()->setFileRecord(record)) { + qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path; + emit directoryListingIterationFinished(folderAlias); + return; + } + }); + + QObject::connect(lsColJob, &LsColJob::finishedWithError, this, [this, folderAlias, lsColJobPath](QNetworkReply *reply) { + _runningLsColJobsForPaths.removeOne(lsColJobPath); + const auto httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qCWarning(lcShellExtServer) << "LSCOL job error" << reply->errorString() << httpCode << reply->error(); + emit directoryListingIterationFinished(folderAlias); + }); + + QObject::connect(lsColJob, &LsColJob::finishedWithoutError, this, [this, folderAlias, lsColJobPath]() { + _runningLsColJobsForPaths.removeOne(lsColJobPath); + emit directoryListingIterationFinished(folderAlias); + }); + + _runningLsColJobsForPaths.push_back(lsColJobPath); + lsColJob->start(); } void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo) @@ -252,108 +297,6 @@ void ShellExtensionsServer::slotNewConnection() return; } -void ShellExtensionsServer::slotSharesFetched(const QJsonDocument &reply) -{ - const auto job = qobject_cast(sender()); - - Q_ASSERT(job); - if (!job) { - qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!"; - return; - } - - const auto sharesPath = job->getParamValue(QStringLiteral("path")); - - _runningFetchShareJobsForPaths.removeAll(sharesPath); - - const auto folderAlias = job->property(folderAliasPropertyKey).toString(); - - Q_ASSERT(!folderAlias.isEmpty()); - if (folderAlias.isEmpty()) { - qCWarning(lcShellExtServer) << "No 'folderAlias' set for OcsShareJob's instance!"; - return; - } - - const auto folder = FolderMan::instance()->folder(folderAlias); - - Q_ASSERT(folder); - if (!folder) { - qCWarning(lcShellExtServer) << "folder not found for folderAlias: " << folderAlias; - return; - } - - const auto timeStamp = QDateTime::currentMSecsSinceEpoch(); - QStringList recortPathsToResetIsSharedFlag; - const QByteArray pathOfSharesToResetIsSharedFlag = sharesPath == QStringLiteral("/") ? QByteArrayLiteral("") : sharesPath.toUtf8(); - if (folder->journalDb()->listFilesInPath(pathOfSharesToResetIsSharedFlag, [&](const SyncJournalFileRecord &rec) { - recortPathsToResetIsSharedFlag.push_back(rec.path()); - })) { - for (const auto &recordPath : recortPathsToResetIsSharedFlag) { - SyncJournalFileRecord record; - if (!folder->journalDb()->getFileRecord(recordPath, &record) || !record.isValid()) { - continue; - } - record._isShared = false; - record._lastShareStateFetchedTimestmap = timeStamp; - if (!folder->journalDb()->setFileRecord(record)) { - qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path; - } - } - } - - const auto sharesFetched = reply.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray(); - - for (const auto &share : sharesFetched) { - const auto shareData = share.toObject(); - - const auto sharePath = [&shareData, folder]() { - const auto sharePathRemote = shareData.value(QStringLiteral("path")).toString(); - - const auto folderPath = folder->remotePath(); - if (folderPath != QLatin1Char('/') && sharePathRemote.startsWith(folderPath)) { - // shares are ruturned with absolute remote path, so, if we have our remote root set to subfolder, we need to adjust share's remote path to relative local path - const auto sharePathLocalRelative = sharePathRemote.midRef(folder->remotePathTrailingSlash().length()); - return sharePathLocalRelative.toString(); - } - return sharePathRemote.size() > 1 && sharePathRemote.startsWith(QLatin1Char('/')) - ? QString(sharePathRemote).remove(0, 1) - : sharePathRemote; - }(); - - SyncJournalFileRecord record; - if (!folder || !folder->journalDb()->getFileRecord(sharePath, &record) || !record.isValid()) { - continue; - } - record._isShared = true; - record._lastShareStateFetchedTimestmap = timeStamp; - - if (!folder->journalDb()->setFileRecord(record)) { - qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path; - } - } - - qCInfo(lcShellExtServer) << "Succeeded OcsShareJob for path: " << sharesPath; - emit fetchSharesJobFinished(folderAlias); -} - -void ShellExtensionsServer::slotSharesFetchError(int statusCode, const QString &message) -{ - const auto job = qobject_cast(sender()); - - Q_ASSERT(job); - if (!job) { - qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!"; - return; - } - - const auto sharesPath = job->getParamValue(QStringLiteral("path")); - - _runningFetchShareJobsForPaths.removeAll(sharesPath); - - emit fetchSharesJobFinished(sharesPath); - qCWarning(lcShellExtServer) << "Failed OcsShareJob for path: " << sharesPath; -} - void ShellExtensionsServer::parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message) { const auto customStateRequestMessage = message.value(VfsShellExtensions::Protocol::CustomStateProviderRequestKey).toMap(); diff --git a/src/gui/shellextensionsserver.h b/src/gui/shellextensionsserver.h index 491d4cfe0..9b22716bf 100644 --- a/src/gui/shellextensionsserver.h +++ b/src/gui/shellextensionsserver.h @@ -22,6 +22,7 @@ class QJsonDocument; class QLocalSocket; +class QNetworkReply; namespace OCC { class ShellExtensionsServer : public QObject @@ -53,7 +54,7 @@ public: void setIsSharedInvalidationInterval(qint64 interval); signals: - void fetchSharesJobFinished(const QString &folderAlias); + void directoryListingIterationFinished(const QString &folderAlias); private: void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message); @@ -67,12 +68,10 @@ private: private slots: void slotNewConnection(); - void slotSharesFetched(const QJsonDocument &reply); - void slotSharesFetchError(int statusCode, const QString &message); private: QLocalServer _localServer; - QStringList _runningFetchShareJobsForPaths; + QStringList _runningLsColJobsForPaths; QMap _customStateSocketConnections; qint64 _isSharedInvalidationInterval = 0; }; diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index ef1f6444a..eee232d55 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -23,6 +23,7 @@ #include "config.h" #include "configfile.h" +#include "deletejob.h" #include "folderman.h" #include "folder.h" #include "theme.h" @@ -541,6 +542,12 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li } } +void SocketApi::processLeaveShareRequest(const QString &localFile, SocketListener *listener) +{ + Q_UNUSED(listener) + FolderMan::instance()->leaveShare(QDir::fromNativeSeparators(localFile)); +} + void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) { QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); @@ -584,6 +591,11 @@ void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener processShareRequest(localFile, listener); } +void SocketApi::command_LEAVESHARE(const QString &localFile, SocketListener *listener) +{ + processLeaveShareRequest(localFile, listener); +} + void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *listener) { Q_UNUSED(listener); @@ -1047,6 +1059,10 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) return; + if (record._isShared && !record._sharedByMe) { + listener->sendMessage(QLatin1String("MENU_ITEM:LEAVESHARE") + flagString + tr("Leave this share")); + } + // If sharing is globally disabled, do not show any sharing entries. // If there is no permission to share for this file, add a disabled entry saying so if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) { diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 550406bcf..5cedd53d7 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -15,9 +15,9 @@ #ifndef SOCKETAPI_H #define SOCKETAPI_H -#include "syncfileitem.h" #include "common/syncfilestatus.h" #include "common/syncjournalfilerecord.h" +#include "syncfileitem.h" #include "config.h" @@ -28,8 +28,8 @@ class QLocalSocket; class QStringList; class QFileInfo; -namespace OCC { - +namespace OCC +{ class SyncFileStatus; class Folder; class SocketListener; @@ -102,6 +102,7 @@ private: // opens share dialog, sends reply void processShareRequest(const QString &localFile, SocketListener *listener); + void processLeaveShareRequest(const QString &localFile, SocketListener *listener); void processFileActivityRequest(const QString &localFile); Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, OCC::SocketListener *listener); @@ -114,6 +115,7 @@ private: // The context menu actions Q_INVOKABLE void command_ACTIVITY(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_SHARE(const QString &localFile, OCC::SocketListener *listener); + Q_INVOKABLE void command_LEAVESHARE(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener); @@ -149,15 +151,13 @@ private: // Sends the context menu options relating to sharing to listener void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled); - void sendLockFileCommandMenuEntries(const QFileInfo &fileInfo, - Folder * const syncFolder, - const FileData &fileData, - const SocketListener * const listener) const; + void + sendLockFileCommandMenuEntries(const QFileInfo &fileInfo, Folder *const syncFolder, const FileData &fileData, const SocketListener *const listener) const; void sendLockFileInfoMenuEntries(const QFileInfo &fileInfo, - Folder * const syncFolder, + Folder* const syncFolder, const FileData &fileData, - const SocketListener * const listener, + const SocketListener* const listener, const SyncJournalFileRecord &record) const; /** Send the list of menu item. (added in version 1.1) diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 6d9f2a4d5..4b0042927 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -96,7 +96,12 @@ Account::~Account() = default; QString Account::davPath() const { - return davPathBase() + QLatin1Char('/') + davUser() + QLatin1Char('/'); + return davPathRoot() + QLatin1Char('/'); +} + +QString Account::davPathRoot() const +{ + return davPathBase() + QLatin1Char('/') + davUser(); } void Account::setSharedThis(AccountPtr sharedThis) diff --git a/src/libsync/account.h b/src/libsync/account.h index 8be18b1de..667c41fb6 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -141,6 +141,13 @@ public: */ [[nodiscard]] QString davPath() const; + /** + * @brief The possibly themed dav path root for the account. It has + * no trailing slash. + * @returns the (themeable) dav path for the account. + */ + [[nodiscard]] QString davPathRoot() const; + /** Returns webdav entry URL, based on url() */ [[nodiscard]] QUrl davUrl() const; diff --git a/src/libsync/bulkpropagatorjob.cpp b/src/libsync/bulkpropagatorjob.cpp index 8afe789e3..04e508b30 100644 --- a/src/libsync/bulkpropagatorjob.cpp +++ b/src/libsync/bulkpropagatorjob.cpp @@ -393,8 +393,8 @@ void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile, singleFile._item->_etag = etag; singleFile._item->_fileId = getHeaderFromJsonReply(fileReply, "fileid"); singleFile._item->_remotePerm = RemotePermissions::fromServerString(getHeaderFromJsonReply(fileReply, "permissions")); - singleFile._item->_isShared = singleFile._item->_remotePerm.hasPermission(RemotePermissions::IsShared); - singleFile._item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch(); + singleFile._item->_isShared = singleFile._item->_remotePerm.hasPermission(RemotePermissions::IsShared) || singleFile._item->_sharedByMe; + singleFile._item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") { // X-OC-MTime is supported since owncloud 5.0. But not when chunking. diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 78e3e47e6..53f2ce22b 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -475,8 +475,9 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_checksumHeader = serverEntry.checksumHeader; item->_fileId = serverEntry.fileId; item->_remotePerm = serverEntry.remotePerm; - item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared); - item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch(); + item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe; + item->_sharedByMe = serverEntry.sharedByMe; + item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile; item->_etag = serverEntry.etag; item->_directDownloadUrl = serverEntry.directDownloadUrl; @@ -1280,7 +1281,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_fileId = base._fileId; item->_remotePerm = base._remotePerm; item->_isShared = base._isShared; - item->_lastShareStateFetchedTimestmap = base._lastShareStateFetchedTimestmap; + item->_sharedByMe = base._sharedByMe; + item->_lastShareStateFetchedTimestamp = base._lastShareStateFetchedTimestamp; item->_etag = base._etag; item->_type = base._type; @@ -1406,8 +1408,9 @@ void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, Proce rec._type = item->_type; rec._fileSize = serverEntry.size; rec._remotePerm = serverEntry.remotePerm; - rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared); - rec._lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch(); + rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe; + rec._sharedByMe = serverEntry.sharedByMe; + rec._lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); rec._checksumHeader = serverEntry.checksumHeader; const auto result = _discoveryData->_statedb->setFileRecord(rec); if (!result) { diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index bda90c97b..c2e06adab 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -451,6 +451,7 @@ static void propertyMapToRemoteInfo(const QMap &map, RemoteInf // if we are the owner or not. // Piggy back on the persmission field result.remotePerm.setPermission(RemotePermissions::IsShared); + result.sharedByMe = true; } } else if (property == "is-encrypted" && value == QStringLiteral("1")) { result.isE2eEncrypted = true; diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 3cd4d9cbb..52471a6dc 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -68,6 +68,7 @@ struct RemoteInfo bool isDirectory = false; bool isE2eEncrypted = false; QString e2eMangledName; + bool sharedByMe = false; [[nodiscard]] bool isValid() const { return !name.isNull(); } diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp index 5c4ea1e48..0a2eb2a3c 100644 --- a/src/libsync/propagateremotemkdir.cpp +++ b/src/libsync/propagateremotemkdir.cpp @@ -140,12 +140,13 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con propagator()->_activeJobList.append(this); auto propfindJob = new PropfindJob(propagator()->account(), jobPath, this); - propfindJob->setProperties({"http://owncloud.org/ns:permissions"}); + propfindJob->setProperties({QByteArrayLiteral("http://owncloud.org/ns:share-types"), QByteArrayLiteral("http://owncloud.org/ns:permissions")}); connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){ propagator()->_activeJobList.removeOne(this); _item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString()); - _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared); - _item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch(); + _item->_sharedByMe = !result.value(QStringLiteral("share-types")).toString().isEmpty(); + _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared) || _item->_sharedByMe; + _item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); if (!_uploadEncryptedHelper && !_item->_isEncrypted) { success(); diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 97971d29f..700b0d3e4 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -42,7 +42,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._fileSize = _size; rec._remotePerm = _remotePerm; rec._isShared = _isShared; - rec._lastShareStateFetchedTimestmap = _lastShareStateFetchedTimestmap; + rec._sharedByMe = _sharedByMe; + rec._lastShareStateFetchedTimestamp = _lastShareStateFetchedTimestamp; rec._serverHasIgnoredFiles = _serverHasIgnoredFiles; rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); @@ -91,8 +92,9 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_lockEditorApp = rec._lockstate._lockEditorApp; item->_lockTime = rec._lockstate._lockTime; item->_lockTimeout = rec._lockstate._lockTimeout; + item->_sharedByMe = rec._sharedByMe; item->_isShared = rec._isShared; - item->_lastShareStateFetchedTimestmap = rec._lastShareStateFetchedTimestmap; + item->_lastShareStateFetchedTimestamp = rec._lastShareStateFetchedTimestamp; return item; } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 1e134eee1..ed94c9eda 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -310,7 +310,9 @@ public: qint64 _lockTimeout = 0; bool _isShared = false; - time_t _lastShareStateFetchedTimestmap = 0; + time_t _lastShareStateFetchedTimestamp = 0; + + bool _sharedByMe = false; }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index e66049f57..9689a8592 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -13,7 +13,9 @@ #include "folderman.h" #include "account.h" #include "accountstate.h" +#include #include "configfile.h" +#include "syncenginetestutils.h" #include "testhelper.h" using namespace OCC; @@ -24,7 +26,105 @@ class TestFolderMan: public QObject FolderMan _fm; +signals: + void incomingShareDeleted(); + private slots: + void testLeaveShare() + { + constexpr auto firstSharePath = "A/sharedwithme_A.txt"; + constexpr auto secondSharePath = "A/B/sharedwithme_B.data"; + + QScopedPointer fakeQnam(new FakeQNAM({})); + OCC::AccountPtr account = OCC::Account::create(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + OCC::AccountManager::instance()->addAccount(account); + + FakeFolder fakeFolder{FileInfo{}}; + fakeFolder.remoteModifier().mkdir("A"); + + fakeFolder.remoteModifier().insert(firstSharePath, 100); + const auto firstShare = fakeFolder.remoteModifier().find(firstSharePath); + QVERIFY(firstShare); + firstShare->permissions.setPermission(OCC::RemotePermissions::IsShared); + + fakeFolder.remoteModifier().mkdir("A/B"); + + fakeFolder.remoteModifier().insert(secondSharePath, 100); + const auto secondShare = fakeFolder.remoteModifier().find(secondSharePath); + QVERIFY(secondShare); + secondShare->permissions.setPermission(OCC::RemotePermissions::IsShared); + + FolderMan *folderman = FolderMan::instance(); + QCOMPARE(folderman, &_fm); + OCC::AccountState *accountState = OCC::AccountManager::instance()->accounts().first().data(); + const auto folder = folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath())); + QVERIFY(folder); + + auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath()); + QVERIFY(realFolder); + + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + fakeQnam->setOverride([this, accountState, &fakeFolder](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { + Q_UNUSED(device); + QNetworkReply *reply = nullptr; + + if (op != QNetworkAccessManager::DeleteOperation) { + reply = new FakeErrorReply(op, req, this, 405); + return reply; + } + + if (req.url().path().isEmpty()) { + reply = new FakeErrorReply(op, req, this, 404); + return reply; + } + + const auto filePathRelative = req.url().path().remove(accountState->account()->davPath()); + + const auto foundFileInRemoteFolder = fakeFolder.remoteModifier().find(filePathRelative); + + if (filePathRelative.isEmpty() || !foundFileInRemoteFolder) { + reply = new FakeErrorReply(op, req, this, 404); + return reply; + } + + fakeFolder.remoteModifier().remove(filePathRelative); + reply = new FakePayloadReply(op, req, {}, nullptr); + + emit incomingShareDeleted(); + + return reply; + }); + + QSignalSpy incomingShareDeletedSignal(this, &TestFolderMan::incomingShareDeleted); + + // verify first share gets deleted + folderman->leaveShare(fakeFolder.localPath() + firstSharePath); + QCOMPARE(incomingShareDeletedSignal.count(), 1); + QVERIFY(!fakeFolder.remoteModifier().find(firstSharePath)); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // verify no share gets deleted + folderman->leaveShare(fakeFolder.localPath() + "A/B/notsharedwithme_B.data"); + QCOMPARE(incomingShareDeletedSignal.count(), 1); + QVERIFY(fakeFolder.remoteModifier().find("A/B/sharedwithme_B.data")); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // verify second share gets deleted + folderman->leaveShare(fakeFolder.localPath() + secondSharePath); + QCOMPARE(incomingShareDeletedSignal.count(), 2); + QVERIFY(!fakeFolder.remoteModifier().find(secondSharePath)); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + OCC::AccountManager::instance()->deleteAccount(accountState); + } + void testCheckPathValidityForNewFolder() { #ifdef Q_OS_WIN @@ -210,5 +310,5 @@ private slots: } }; -QTEST_APPLESS_MAIN(TestFolderMan) +QTEST_GUILESS_MAIN(TestFolderMan) #include "testfolderman.moc"