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 <blackslayer4@gmail.com>
This commit is contained in:
allexzander 2022-10-21 17:11:29 +03:00 committed by alex-z
parent ab8f0dbbbb
commit 6affc6e6ab
18 changed files with 280 additions and 177 deletions

View file

@ -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<void, QString> 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<void, QString> 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<void, QString> 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();

View file

@ -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

View file

@ -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<Folder *> &folders,
SyncResult::Status *status, bool *unresolvedConflicts)
{

View file

@ -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.

View file

@ -19,7 +19,6 @@
#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
#include "folder.h"
#include "folderman.h"
#include "ocssharejob.h"
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
@ -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<QString, QString> &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<OcsShareJob *>(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<OcsShareJob *>(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();

View file

@ -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<qintptr, QMetaObject::Connection> _customStateSocketConnections;
qint64 _isSharedInvalidationInterval = 0;
};

View file

@ -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)) {

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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.

View file

@ -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) {

View file

@ -451,6 +451,7 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &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;

View file

@ -68,6 +68,7 @@ struct RemoteInfo
bool isDirectory = false;
bool isE2eEncrypted = false;
QString e2eMangledName;
bool sharedByMe = false;
[[nodiscard]] bool isValid() const { return !name.isNull(); }

View file

@ -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();

View file

@ -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;
}

View file

@ -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)

View file

@ -13,7 +13,9 @@
#include "folderman.h"
#include "account.h"
#include "accountstate.h"
#include <accountmanager.h>
#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> 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"