Merge pull request #4420 from nextcloud/feature/files_lock

Feature/files lock
This commit is contained in:
Matthieu Gallien 2022-05-02 14:50:20 +02:00 committed by GitHub
commit eeb0d20a7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1353 additions and 135 deletions

View file

@ -48,7 +48,8 @@ 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 " \
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \
" FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
@ -66,6 +67,13 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
rec._checksumHeader = query.baValue(9);
rec._e2eMangledName = query.baValue(10);
rec._isE2eEncrypted = query.intValue(11) > 0;
rec._lockstate._locked = query.intValue(12) > 0;
rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
rec._lockstate._lockOwnerId = query.stringValue(14);
rec._lockstate._lockOwnerType = query.int64Value(15);
rec._lockstate._lockEditorApp = query.stringValue(16);
rec._lockstate._lockTime = query.int64Value(17);
rec._lockstate._lockTimeout = query.int64Value(18);
}
static QByteArray defaultJournalMode(const QString &dbPath)
@ -658,39 +666,31 @@ bool SyncJournalDb::updateMetadataTableStructure()
return false;
}
if (columns.indexOf("fileid") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN fileid VARCHAR(128);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: Add column fileid"), query);
re = false;
}
const auto addColumn = [this, &columns, &re] (const QString &columnName, const QString &dataType, const bool withIndex = false) {
const auto latin1ColumnName = columnName.toLatin1();
if (columns.indexOf(latin1ColumnName) == -1) {
SqlQuery query(_db);
const auto request = QStringLiteral("ALTER TABLE metadata ADD COLUMN %1 %2;").arg(columnName).arg(dataType);
query.prepare(request.toLatin1());
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add %1 column").arg(columnName), query);
re = false;
}
query.prepare("CREATE INDEX metadata_file_id ON metadata(fileid);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index fileid"), query);
re = false;
if (withIndex) {
query.prepare(QStringLiteral("CREATE INDEX metadata_%1 ON metadata(%1);").arg(columnName).toLatin1());
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index %1").arg(columnName), query);
re = false;
}
}
commitInternal(QStringLiteral("update database structure: add %1 column").arg(columnName));
}
commitInternal(QStringLiteral("update database structure: add fileid col"));
}
if (columns.indexOf("remotePerm") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN remotePerm VARCHAR(128);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add column remotePerm"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure (remotePerm)"));
}
if (columns.indexOf("filesize") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN filesize BIGINT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateDatabaseStructure: add column filesize"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add filesize col"));
}
};
addColumn(QStringLiteral("fileid"), QStringLiteral("VARCHAR(128)"), true);
addColumn(QStringLiteral("remotePerm"), QStringLiteral("VARCHAR(128)"));
addColumn(QStringLiteral("filesize"), QStringLiteral("BIGINT"));
if (true) {
SqlQuery query(_db);
@ -722,54 +722,11 @@ bool SyncJournalDb::updateMetadataTableStructure()
commitInternal(QStringLiteral("update database structure: add parent index"));
}
if (columns.indexOf("ignoredChildrenRemote") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN ignoredChildrenRemote INT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add ignoredChildrenRemote column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add ignoredChildrenRemote col"));
}
if (columns.indexOf("contentChecksum") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN contentChecksum TEXT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add contentChecksum column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add contentChecksum col"));
}
if (columns.indexOf("contentChecksumTypeId") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN contentChecksumTypeId INTEGER;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add contentChecksumTypeId column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add contentChecksumTypeId col"));
}
if (!columns.contains("e2eMangledName")) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN e2eMangledName TEXT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add e2eMangledName column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add e2eMangledName col"));
}
if (!columns.contains("isE2eEncrypted")) {
SqlQuery query(_db);
query.prepare("ALTER TABLE metadata ADD COLUMN isE2eEncrypted INTEGER;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add isE2eEncrypted column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add isE2eEncrypted col"));
}
addColumn(QStringLiteral("ignoredChildrenRemote"), QStringLiteral("INT"));
addColumn(QStringLiteral("contentChecksum"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
auto uploadInfoColumns = tableColumns("uploadinfo");
if (uploadInfoColumns.isEmpty())
@ -806,6 +763,14 @@ bool SyncJournalDb::updateMetadataTableStructure()
commitInternal(QStringLiteral("update database structure: add e2eMangledName index"));
}
addColumn(QStringLiteral("lock"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockType"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockOwnerDisplayName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockOwnerId"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockOwnerEditor"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockTime"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockTimeout"), QStringLiteral("INTEGER"));
return re;
}
@ -919,62 +884,76 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
<< "modtime:" << record._modtime << "type:" << record._type
<< "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted;
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
<< "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType
<< "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId
<< "lock editor:" << record._lockstate._lockEditorApp;
const qint64 phash = getPHash(record._path);
if (checkConnect()) {
int plen = record._path.length();
QByteArray etag(record._etag);
if (etag.isEmpty())
etag = "";
QByteArray fileId(record._fileId);
if (fileId.isEmpty())
fileId = "";
QByteArray remotePerm = record._remotePerm.toDbValue();
QByteArray checksumType, checksum;
parseChecksumHeader(record._checksumHeader, &checksumType, &checksum);
int contentChecksumTypeId = mapChecksumType(checksumType);
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) "
"VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18);"),
_db);
if (!query) {
return query->error();
}
query->bindValue(1, phash);
query->bindValue(2, plen);
query->bindValue(3, record._path);
query->bindValue(4, record._inode);
query->bindValue(5, 0); // uid Not used
query->bindValue(6, 0); // gid Not used
query->bindValue(7, 0); // mode Not used
query->bindValue(8, record._modtime);
query->bindValue(9, record._type);
query->bindValue(10, etag);
query->bindValue(11, fileId);
query->bindValue(12, remotePerm);
query->bindValue(13, record._fileSize);
query->bindValue(14, record._serverHasIgnoredFiles ? 1 : 0);
query->bindValue(15, checksum);
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, record._isE2eEncrypted);
if (!query->exec()) {
return query->error();
}
// Can't be true anymore.
_metadataTableIsEmpty = false;
return {};
} else {
if (!checkConnect()) {
qCWarning(lcDb) << "Failed to connect database.";
return tr("Failed to connect database."); // checkConnect failed.
}
int plen = record._path.length();
QByteArray etag(record._etag);
if (etag.isEmpty()) {
etag = "";
}
QByteArray fileId(record._fileId);
if (fileId.isEmpty()) {
fileId = "";
}
QByteArray remotePerm = record._remotePerm.toDbValue();
QByteArray checksumType, checksum;
parseChecksumHeader(record._checksumHeader, &checksumType, &checksum);
int contentChecksumTypeId = mapChecksumType(checksumType);
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) "
"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);"),
_db);
if (!query) {
return query->error();
}
query->bindValue(1, phash);
query->bindValue(2, plen);
query->bindValue(3, record._path);
query->bindValue(4, record._inode);
query->bindValue(5, 0); // uid Not used
query->bindValue(6, 0); // gid Not used
query->bindValue(7, 0); // mode Not used
query->bindValue(8, record._modtime);
query->bindValue(9, record._type);
query->bindValue(10, etag);
query->bindValue(11, fileId);
query->bindValue(12, remotePerm);
query->bindValue(13, record._fileSize);
query->bindValue(14, record._serverHasIgnoredFiles ? 1 : 0);
query->bindValue(15, checksum);
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, record._isE2eEncrypted);
query->bindValue(19, record._lockstate._locked ? 1 : 0);
query->bindValue(20, record._lockstate._lockOwnerType);
query->bindValue(21, record._lockstate._lockOwnerDisplayName);
query->bindValue(22, record._lockstate._lockOwnerId);
query->bindValue(23, record._lockstate._lockEditorApp);
query->bindValue(24, record._lockstate._lockTime);
query->bindValue(25, record._lockstate._lockTimeout);
if (!query->exec()) {
return query->error();
}
// Can't be true anymore.
_metadataTableIsEmpty = false;
return {};
}
void SyncJournalDb::keyValueStoreSet(const QString &key, QVariant value)

View file

@ -31,6 +31,16 @@ namespace OCC {
class SyncFileItem;
struct SyncJournalFileLockInfo {
bool _locked = false;
QString _lockOwnerDisplayName;
QString _lockOwnerId;
qint64 _lockOwnerType = 0;
QString _lockEditorApp;
qint64 _lockTime = 0;
qint64 _lockTimeout = 0;
};
/**
* @brief The SyncJournalFileRecord class
* @ingroup libsync
@ -70,6 +80,7 @@ public:
QByteArray _checksumHeader;
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
SyncJournalFileLockInfo _lockstate;
};
bool OCSYNC_EXPORT

View file

@ -392,6 +392,8 @@ AccountPtr AccountManager::createAccount()
acc->setSslErrorHandler(new SslDialogErrorHandler);
connect(acc.data(), &Account::proxyAuthenticationRequired,
ProxyAuthHandler::instance(), &ProxyAuthHandler::handleProxyAuthenticationRequired);
connect(acc.data(), &Account::lockFileError,
Systray::instance(), &Systray::showErrorMessageDialog);
return acc;
}

View file

@ -639,7 +639,7 @@ void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &l
w = _shareDialogs[localPath];
} else {
qCInfo(lcApplication) << "Opening share dialog" << sharePath << localPath << maxSharingPermissions;
w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), startPage);
w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), fileRecord._lockstate, startPage);
w->setAttribute(Qt::WA_DeleteOnClose, true);
_shareDialogs[localPath] = w;

View file

@ -59,6 +59,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
const QString &localPath,
SharePermissions maxSharingPermissions,
const QByteArray &numericFileId,
SyncJournalFileLockInfo filelockState,
ShareDialogStartPage startPage,
QWidget *parent)
: QDialog(parent)
@ -67,6 +68,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
, _sharePath(sharePath)
, _localPath(localPath)
, _maxSharingPermissions(maxSharingPermissions)
, _filelockState(std::move(filelockState))
, _privateLinkUrl(accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded))
, _startPage(startPage)
{
@ -95,6 +97,14 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
f.setPointSize(qRound(f.pointSize() * 1.4));
_ui->label_name->setFont(f);
if (_filelockState._locked) {
static constexpr auto SECONDS_PER_MINUTE = 60;
const auto lockExpirationTime = _filelockState._lockTime + _filelockState._lockTimeout;
const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
const auto remainingTimeInMinute = static_cast<int>(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0);
_ui->label_lockinfo->setText(tr("Locked by %1 - Expire in %2 minutes", "remaining time before lock expire", remainingTimeInMinute).arg(_filelockState._lockOwnerDisplayName).arg(remainingTimeInMinute));
}
QString ocDir(_sharePath);
ocDir.truncate(ocDir.length() - fileName.length());

View file

@ -18,6 +18,7 @@
#include "accountstate.h"
#include "sharepermissions.h"
#include "owncloudgui.h"
#include "common/syncjournalfilerecord.h"
#include <QSharedPointer>
#include <QPointer>
@ -51,6 +52,7 @@ public:
const QString &localPath,
SharePermissions maxSharingPermissions,
const QByteArray &numericFileId,
SyncJournalFileLockInfo filelockState,
ShareDialogStartPage startPage,
QWidget *parent = nullptr);
~ShareDialog() override;
@ -91,6 +93,7 @@ private:
QString _localPath;
SharePermissions _maxSharingPermissions;
QByteArray _numericFileId;
SyncJournalFileLockInfo _filelockState;
QString _privateLinkUrl;
ShareDialogStartPage _startPage;
ShareManager *_manager = nullptr;

View file

@ -101,7 +101,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QLabel" name="label_sharePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
@ -132,6 +132,31 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_lockinfo">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>315</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>

View file

@ -958,6 +958,32 @@ void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
solver.setRemoteVersionFilename(target);
}
void SocketApi::command_LOCK_FILE(const QString &localFile, SocketListener *listener)
{
Q_UNUSED(listener)
setFileLock(localFile, SyncFileItem::LockStatus::LockedItem);
}
void SocketApi::command_UNLOCK_FILE(const QString &localFile, SocketListener *listener)
{
Q_UNUSED(listener)
setFileLock(localFile, SyncFileItem::LockStatus::UnlockedItem);
}
void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const
{
const auto fileData = FileData::get(localFile);
const auto shareFolder = fileData.folder;
if (!shareFolder || !shareFolder->accountState()->isConnected()) {
return;
}
shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState);
}
void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const
{
QJsonArray out;
@ -1047,6 +1073,39 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
//listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …"));
}
void SocketApi::sendLockFileCommandMenuEntries(const QFileInfo &fileInfo,
Folder* const syncFolder,
const FileData &fileData,
const OCC::SocketListener* const listener) const
{
if (!fileInfo.isDir() && syncFolder->accountState()->account()->capabilities().filesLockAvailable()) {
if (syncFolder->accountState()->account()->fileLockStatus(syncFolder->journalDb(), fileData.folderRelativePath) == SyncFileItem::LockStatus::UnlockedItem) {
listener->sendMessage(QLatin1String("MENU_ITEM:LOCK_FILE::") + tr("Lock file"));
} else {
if (syncFolder->accountState()->account()->fileCanBeUnlocked(syncFolder->journalDb(), fileData.folderRelativePath)) {
listener->sendMessage(QLatin1String("MENU_ITEM:UNLOCK_FILE::") + tr("Unlock file"));
}
}
}
}
void SocketApi::sendLockFileInfoMenuEntries(const QFileInfo &fileInfo,
Folder * const syncFolder,
const FileData &fileData,
const SocketListener * const listener,
const SyncJournalFileRecord &record) const
{
static constexpr auto SECONDS_PER_MINUTE = 60;
if (!fileInfo.isDir() && syncFolder->accountState()->account()->capabilities().filesLockAvailable() &&
syncFolder->accountState()->account()->fileLockStatus(syncFolder->journalDb(), fileData.folderRelativePath) == SyncFileItem::LockStatus::LockedItem) {
listener->sendMessage(QLatin1String("MENU_ITEM:LOCKED_FILE_OWNER:d:") + tr("Locked by %1").arg(record._lockstate._lockOwnerDisplayName));
const auto lockExpirationTime = record._lockstate._lockTime + record._lockstate._lockTimeout;
const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
const auto remainingTimeInMinute = static_cast<int>(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0);
listener->sendMessage(QLatin1String("MENU_ITEM:LOCKED_FILE_DATE:d:") + tr("Expire in %1 minutes", "remaining time before lock expire", remainingTimeInMinute).arg(remainingTimeInMinute));
}
}
SocketApi::FileData SocketApi::FileData::get(const QString &localFile)
{
FileData data;
@ -1133,6 +1192,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
const QFileInfo fileInfo(fileData.localPath);
sendLockFileInfoMenuEntries(fileInfo, syncFolder, fileData, listener, record);
if (!fileInfo.isDir()) {
listener->sendMessage(QLatin1String("MENU_ITEM:ACTIVITY") + flagString + tr("Activity"));
}
@ -1145,6 +1205,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser"));
}
sendLockFileCommandMenuEntries(fileInfo, syncFolder, fileData, listener);
sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
// Conflict files get conflict resolution actions

View file

@ -27,6 +27,7 @@
class QUrl;
class QLocalSocket;
class QStringList;
class QFileInfo;
namespace OCC {
@ -124,6 +125,10 @@ private:
Q_INVOKABLE void command_RESOLVE_CONFLICT(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);
Q_INVOKABLE void command_LOCK_FILE(const QString &localFile, SocketListener *listener);
Q_INVOKABLE void command_UNLOCK_FILE(const QString &localFile, SocketListener *listener);
void setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const;
// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
#ifdef Q_OS_WIN
@ -145,6 +150,17 @@ 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 sendLockFileInfoMenuEntries(const QFileInfo &fileInfo,
Folder * const syncFolder,
const FileData &fileData,
const SocketListener * const listener,
const SyncJournalFileRecord &record) const;
/** Send the list of menu item. (added in version 1.1)
* argument is a list of files for which the menu should be shown, separated by '\x1e'
* Reply with GET_MENU_ITEMS:BEGIN

View file

@ -98,6 +98,7 @@ signals:
void openShareDialog(const QString &sharePath, const QString &localPath);
void showFileActivityDialog(const QString &objectName, const int objectId);
void sendChatMessage(const QString &token, const QString &message, const QString &replyTo);
void showErrorMessageDialog(const QString &error);
public slots:
void slotNewUserSelected();

View file

@ -5,6 +5,7 @@ import QtQuick.Window 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import Qt.labs.platform 1.1 as NativeDialogs
import "../"
// Custom qml modules are in /theme (and included by resources.qrc)
@ -62,6 +63,19 @@ Window {
}
}
Component {
id: errorMessageDialog
NativeDialogs.MessageDialog {
id: dialog
title: Systray.windowTitle
onAccepted: destroy()
onRejected: destroy()
}
}
Connections {
target: Systray
function onShowWindow() {
@ -84,6 +98,12 @@ Window {
function onShowFileActivityDialog(objectName, objectId) {
openFileActivityDialog(objectName, objectId)
}
function onShowErrorMessageDialog(error) {
var newErrorDialog = errorMessageDialog.createObject(trayWindow)
newErrorDialog.text = error
newErrorDialog.open()
}
}
OpacityMask {

View file

@ -110,6 +110,8 @@ set(libsync_SRCS
userstatusconnector.cpp
ocsprofileconnector.h
ocsprofileconnector.cpp
lockfilejobs.h
lockfilejobs.cpp
creds/dummycredentials.h
creds/dummycredentials.cpp
creds/abstractcredentials.h

View file

@ -25,8 +25,10 @@
#include "pushnotifications.h"
#include "version.h"
#include <deletejob.h>
#include "deletejob.h"
#include "lockfilejobs.h"
#include "common/syncjournaldb.h"
#include "common/asserts.h"
#include "clientsideencryption.h"
#include "ocsuserstatusconnector.h"
@ -113,6 +115,11 @@ AccountPtr Account::sharedFromThis()
return _sharedThis.toStrongRef();
}
AccountPtr Account::sharedFromThis() const
{
return _sharedThis.toStrongRef();
}
QString Account::davUser() const
{
return _davUser.isEmpty() && _credentials ? _credentials->user() : _davUser;
@ -850,4 +857,58 @@ std::shared_ptr<UserStatusConnector> Account::userStatusConnector() const
return _userStatusConnector;
}
void Account::setLockFileState(const QString &serverRelativePath,
SyncJournalDb * const journal,
const SyncFileItem::LockStatus lockStatus)
{
auto job = std::make_unique<LockFileJob>(sharedFromThis(), journal, serverRelativePath, lockStatus);
connect(job.get(), &LockFileJob::finishedWithoutError, this, [this]() {
Q_EMIT lockFileSuccess();
});
connect(job.get(), &LockFileJob::finishedWithError, this, [lockStatus, serverRelativePath, this](const int httpErrorCode, const QString &errorString, const QString &lockOwnerName) {
auto errorMessage = QString{};
const auto filePath = serverRelativePath.mid(1);
if (httpErrorCode == LockFileJob::LOCKED_HTTP_ERROR_CODE) {
errorMessage = tr("File %1 is already locked by %2.").arg(filePath, lockOwnerName);
} else if (lockStatus == SyncFileItem::LockStatus::LockedItem) {
errorMessage = tr("Lock operation on %1 failed with error %2").arg(filePath, errorString);
} else if (lockStatus == SyncFileItem::LockStatus::UnlockedItem) {
errorMessage = tr("Unlock operation on %1 failed with error %2").arg(filePath, errorString);
}
Q_EMIT lockFileError(errorMessage);
});
job->start();
static_cast<void>(job.release());
}
SyncFileItem::LockStatus Account::fileLockStatus(SyncJournalDb * const journal,
const QString &folderRelativePath) const
{
SyncJournalFileRecord record;
if (journal->getFileRecord(folderRelativePath, &record)) {
return record._lockstate._locked ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem;
}
return SyncFileItem::LockStatus::UnlockedItem;
}
bool Account::fileCanBeUnlocked(SyncJournalDb * const journal,
const QString &folderRelativePath) const
{
SyncJournalFileRecord record;
if (journal->getFileRecord(folderRelativePath, &record)) {
if (record._lockstate._lockOwnerType != static_cast<int>(SyncFileItem::LockOwnerType::UserLock)) {
return false;
}
if (record._lockstate._lockOwnerId != sharedFromThis()->davUser()) {
return false;
}
return true;
}
return false;
}
} // namespace OCC

View file

@ -35,6 +35,7 @@
#include <memory>
#include "capabilities.h"
#include "clientsideencryption.h"
#include "syncfileitem.h"
class QSettings;
class QNetworkReply;
@ -56,6 +57,7 @@ class AccessManager;
class SimpleNetworkJob;
class PushNotifications;
class UserStatusConnector;
class SyncJournalDb;
/**
* @brief Reimplement this to handle SSL errors from libsync
@ -89,6 +91,8 @@ public:
AccountPtr sharedFromThis();
AccountPtr sharedFromThis() const;
/**
* The user that can be used in dav url.
*
@ -275,6 +279,15 @@ public:
std::shared_ptr<UserStatusConnector> userStatusConnector() const;
void setLockFileState(const QString &serverRelativePath,
SyncJournalDb * const journal,
const SyncFileItem::LockStatus lockStatus);
SyncFileItem::LockStatus fileLockStatus(SyncJournalDb * const journal,
const QString &folderRelativePath) const;
bool fileCanBeUnlocked(SyncJournalDb * const journal, const QString &folderRelativePath) const;
public slots:
/// Used when forgetting credentials
void clearQNAMCache();
@ -311,6 +324,9 @@ signals:
void capabilitiesChanged();
void lockFileSuccess();
void lockFileError(const QString&);
protected Q_SLOTS:
void slotCredentialsFetched();
void slotCredentialsAsked();

View file

@ -221,6 +221,11 @@ bool Capabilities::bulkUpload() const
return _capabilities["dav"].toMap()["bulkupload"].toByteArray() >= "1.0";
}
bool Capabilities::filesLockAvailable() const
{
return _capabilities["files"].toMap()["locking"].toByteArray() >= "1.0";
}
bool Capabilities::userStatus() const
{
if (!_capabilities.contains("user_status")) {

View file

@ -65,6 +65,7 @@ public:
int shareDefaultPermissions() const;
bool chunkingNg() const;
bool bulkUpload() const;
bool filesLockAvailable() const;
bool userStatus() const;
bool userStatusSupportsEmoji() const;
QColor serverColor() const;

View file

@ -363,6 +363,8 @@ void ProcessDirectoryJob::processFile(PathTuple path,
{
const char *hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false";
const char *hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false";
const auto serverFileIsLocked = serverEntry.locked == SyncFileItem::LockStatus::LockedItem ? "locked" : "not locked";
const auto localFileIsLocked = dbEntry._lockstate._locked ? "locked" : "not locked";
qCInfo(lcDisco).nospace() << "Processing " << path._original
<< " | valid: " << dbEntry.isValid() << "/" << hasLocal << "/" << hasServer
<< " | mtime: " << dbEntry._modtime << "/" << localEntry.modtime << "/" << serverEntry.modtime
@ -374,7 +376,8 @@ void ProcessDirectoryJob::processFile(PathTuple path,
<< " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/"
<< " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile)
<< " | e2ee: " << dbEntry._isE2eEncrypted << "/" << serverEntry.isE2eEncrypted
<< " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName;
<< " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName
<< " | file lock: " << localFileIsLocked << "//" << serverFileIsLocked;
if (localEntry.isValid()
&& !serverEntry.isValid()
@ -483,6 +486,14 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
Q_ASSERT(serverEntry.e2eMangledName.startsWith(rootPath));
return serverEntry.e2eMangledName.mid(rootPath.length());
}();
item->_locked = serverEntry.locked;
item->_lockOwnerDisplayName = serverEntry.lockOwnerDisplayName;
item->_lockOwnerId = serverEntry.lockOwnerId;
item->_lockOwnerType = serverEntry.lockOwnerType;
item->_lockEditorApp = serverEntry.lockEditorApp;
item->_lockTime = serverEntry.lockTime;
item->_lockTimeout = serverEntry.lockTimeout;
qCInfo(lcDisco()) << item->_locked << item->_lockOwnerDisplayName << item->_lockOwnerId << item->_lockOwnerType << item->_lockEditorApp << item->_lockTime << item->_lockTimeout;
// Check for missing server data
{

View file

@ -378,6 +378,15 @@ void DiscoverySingleDirectoryJob::start()
if (_account->capabilities().clientSideEncryptionAvailable()) {
props << "http://nextcloud.org/ns:is-encrypted";
}
if (_account->capabilities().filesLockAvailable()) {
props << "http://nextcloud.org/ns:lock"
<< "http://nextcloud.org/ns:lock-owner-displayname"
<< "http://nextcloud.org/ns:lock-owner"
<< "http://nextcloud.org/ns:lock-owner-type"
<< "http://nextcloud.org/ns:lock-owner-editor"
<< "http://nextcloud.org/ns:lock-time"
<< "http://nextcloud.org/ns:lock-timeout";
}
lsColJob->setProperties(props);
@ -445,7 +454,46 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInf
}
} else if (property == "is-encrypted" && value == QStringLiteral("1")) {
result.isE2eEncrypted = true;
} else if (property == "lock") {
result.locked = (value == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem);
}
if (property == "lock-owner-displayname") {
result.lockOwnerDisplayName = value;
}
if (property == "lock-owner") {
result.lockOwnerId = value;
}
if (property == "lock-owner-type") {
auto ok = false;
const auto intConvertedValue = value.toULongLong(&ok);
if (ok) {
result.lockOwnerType = static_cast<SyncFileItem::LockOwnerType>(intConvertedValue);
} else {
result.lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
}
}
if (property == "lock-owner-editor") {
result.lockEditorApp = value;
}
if (property == "lock-time") {
auto ok = false;
const auto intConvertedValue = value.toULongLong(&ok);
if (ok) {
result.lockTime = intConvertedValue;
} else {
result.lockTime = 0;
}
}
if (property == "lock-timeout") {
auto ok = false;
const auto intConvertedValue = value.toULongLong(&ok);
if (ok) {
result.lockTimeout = intConvertedValue;
} else {
result.lockTimeout = 0;
}
}
}
if (result.isDirectory && map.contains("size")) {

View file

@ -65,6 +65,14 @@ struct RemoteInfo
QString directDownloadUrl;
QString directDownloadCookies;
SyncFileItem::LockStatus locked = SyncFileItem::LockStatus::UnlockedItem;
QString lockOwnerDisplayName;
QString lockOwnerId;
SyncFileItem::LockOwnerType lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
QString lockEditorApp;
qint64 lockTime = 0;
qint64 lockTimeout = 0;
};
struct LocalInfo

View file

@ -0,0 +1,223 @@
/*
* Copyright (C) by Matthieu Gallien <matthieu.gallien@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "lockfilejobs.h"
#include "account.h"
#include "common/syncjournaldb.h"
#include "filesystem.h"
#include <QLoggingCategory>
#include <QXmlStreamReader>
namespace OCC {
Q_LOGGING_CATEGORY(lcLockFileJob, "nextcloud.sync.networkjob.lockfile", QtInfoMsg)
LockFileJob::LockFileJob(const AccountPtr account,
SyncJournalDb* const journal,
const QString &path,
const SyncFileItem::LockStatus requestedLockState,
QObject *parent)
: AbstractNetworkJob(account, path, parent)
, _journal(journal)
, _requestedLockState(requestedLockState)
{
}
void LockFileJob::start()
{
qCInfo(lcLockFileJob()) << "start" << path() << _requestedLockState;
QNetworkRequest request;
request.setRawHeader("X-User-Lock", "1");
QByteArray verb;
switch(_requestedLockState)
{
case SyncFileItem::LockStatus::LockedItem:
verb = "LOCK";
break;
case SyncFileItem::LockStatus::UnlockedItem:
verb = "UNLOCK";
break;
}
sendRequest(verb, makeDavUrl(path()), request);
AbstractNetworkJob::start();
}
bool LockFileJob::finished()
{
if (reply()->error() != QNetworkReply::NoError) {
qCInfo(lcLockFileJob()) << "finished with error" << reply()->error() << reply()->errorString();
const auto httpErrorCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (httpErrorCode == LOCKED_HTTP_ERROR_CODE) {
const auto record = handleReply();
if (static_cast<SyncFileItem::LockOwnerType>(record._lockstate._lockOwnerType) == SyncFileItem::LockOwnerType::UserLock) {
Q_EMIT finishedWithError(httpErrorCode, {}, record._lockstate._lockOwnerDisplayName);
} else {
Q_EMIT finishedWithError(httpErrorCode, {}, record._lockstate._lockEditorApp);
}
} else if (httpErrorCode == PRECONDITION_FAILED_ERROR_CODE) {
const auto record = handleReply();
if (_requestedLockState == SyncFileItem::LockStatus::UnlockedItem && !record._lockstate._locked) {
Q_EMIT finishedWithoutError();
} else {
Q_EMIT finishedWithError(httpErrorCode, reply()->errorString(), {});
}
} else {
Q_EMIT finishedWithError(httpErrorCode, reply()->errorString(), {});
}
} else {
qCInfo(lcLockFileJob()) << "success" << path();
handleReply();
Q_EMIT finishedWithoutError();
}
return true;
}
void LockFileJob::setFileRecordLocked(SyncJournalFileRecord &record) const
{
record._lockstate._locked = (_lockStatus == SyncFileItem::LockStatus::LockedItem);
record._lockstate._lockOwnerType = static_cast<int>(_lockOwnerType);
record._lockstate._lockOwnerDisplayName = _userDisplayName;
record._lockstate._lockOwnerId = _userId;
record._lockstate._lockEditorApp = _editorName;
record._lockstate._lockTime = _lockTime;
record._lockstate._lockTimeout = _lockTimeout;
}
void LockFileJob::resetState()
{
_lockStatus = SyncFileItem::LockStatus::UnlockedItem;
_lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
_userDisplayName.clear();
_editorName.clear();
_userId.clear();
_lockTime = 0;
_lockTimeout = 0;
}
SyncJournalFileRecord LockFileJob::handleReply()
{
const auto xml = reply()->readAll();
QXmlStreamReader reader(xml);
resetState();
while (!reader.atEnd()) {
const auto type = reader.readNext();
const auto name = reader.name().toString();
switch (type) {
case QXmlStreamReader::TokenType::NoToken:
case QXmlStreamReader::TokenType::Invalid:
case QXmlStreamReader::TokenType::DTD:
case QXmlStreamReader::TokenType::EntityReference:
case QXmlStreamReader::TokenType::ProcessingInstruction:
case QXmlStreamReader::TokenType::Comment:
case QXmlStreamReader::TokenType::StartDocument:
case QXmlStreamReader::TokenType::Characters:
case QXmlStreamReader::TokenType::EndDocument:
case QXmlStreamReader::TokenType::EndElement:
break;
case QXmlStreamReader::TokenType::StartElement:
decodeStartElement(name, reader);
break;
}
}
SyncJournalFileRecord record;
if (_lockStatus == SyncFileItem::LockStatus::LockedItem) {
if (_lockOwnerType == SyncFileItem::LockOwnerType::UserLock && _userDisplayName.isEmpty()) {
return record;
}
if (_lockOwnerType == SyncFileItem::LockOwnerType::AppLock && _editorName.isEmpty()) {
return record;
}
if (_userId.isEmpty()) {
return record;
}
if (_lockTime <= 0) {
return record;
}
if (_lockTimeout <= 0) {
return record;
}
}
const auto relativePath = path().mid(1);
if (_journal->getFileRecord(relativePath, &record) && record.isValid()) {
setFileRecordLocked(record);
if (_lockOwnerType != SyncFileItem::LockOwnerType::UserLock ||
_userId != account()->davUser()) {
FileSystem::setFileReadOnly(relativePath, true);
}
_journal->setFileRecord(record);
_journal->commit("lock file job");
}
return record;
}
void LockFileJob::decodeStartElement(const QString &name,
QXmlStreamReader &reader)
{
if (name == QStringLiteral("lock")) {
const auto valueText = reader.readElementText();
if (!valueText.isEmpty()) {
bool isValid = false;
const auto convertedValue = valueText.toInt(&isValid);
if (isValid) {
_lockStatus = static_cast<SyncFileItem::LockStatus>(convertedValue);
}
}
} else if (name == QStringLiteral("lock-owner-type")) {
const auto valueText = reader.readElementText();
bool isValid = false;
const auto convertedValue = valueText.toInt(&isValid);
if (isValid) {
_lockOwnerType = static_cast<SyncFileItem::LockOwnerType>(convertedValue);
}
} else if (name == QStringLiteral("lock-owner-displayname")) {
_userDisplayName = reader.readElementText();
} else if (name == QStringLiteral("lock-owner")) {
_userId = reader.readElementText();
} else if (name == QStringLiteral("lock-time")) {
const auto valueText = reader.readElementText();
bool isValid = false;
const auto convertedValue = valueText.toLongLong(&isValid);
if (isValid) {
_lockTime = convertedValue;
}
} else if (name == QStringLiteral("lock-timeout")) {
const auto valueText = reader.readElementText();
bool isValid = false;
const auto convertedValue = valueText.toLongLong(&isValid);
if (isValid) {
_lockTimeout = convertedValue;
}
} else if (name == QStringLiteral("lock-owner-editor")) {
_editorName = reader.readElementText();
}
}
}

View file

@ -0,0 +1,61 @@
#ifndef LOCKFILEJOBS_H
#define LOCKFILEJOBS_H
#include "abstractnetworkjob.h"
#include "syncfileitem.h"
class QXmlStreamReader;
namespace OCC {
class SyncJournalDb;
class OWNCLOUDSYNC_EXPORT LockFileJob : public AbstractNetworkJob
{
Q_OBJECT
public:
static constexpr auto LOCKED_HTTP_ERROR_CODE = 423;
static constexpr auto PRECONDITION_FAILED_ERROR_CODE = 412;
explicit LockFileJob(const AccountPtr account,
SyncJournalDb* const journal,
const QString &path,
const SyncFileItem::LockStatus requestedLockState,
QObject *parent = nullptr);
void start() override;
signals:
void finishedWithError(int httpErrorCode,
const QString &errorString,
const QString &lockOwnerName);
void finishedWithoutError();
private:
bool finished() override;
void setFileRecordLocked(SyncJournalFileRecord &record) const;
SyncJournalFileRecord handleReply();
void resetState();
void decodeStartElement(const QString &name,
QXmlStreamReader &reader);
SyncJournalDb* _journal = nullptr;
SyncFileItem::LockStatus _requestedLockState = SyncFileItem::LockStatus::LockedItem;
SyncFileItem::LockStatus _lockStatus = SyncFileItem::LockStatus::UnlockedItem;
SyncFileItem::LockOwnerType _lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
QString _userDisplayName;
QString _editorName;
QString _userId;
qint64 _lockTime = 0;
qint64 _lockTimeout = 0;
};
}
#endif // LOCKFILEJOBS_H

View file

@ -1211,6 +1211,12 @@ void PropagateDownloadFile::downloadFinished()
return;
}
qCInfo(lcPropagateDownload()) << propagator()->account()->davUser() << propagator()->account()->davDisplayName() << propagator()->account()->displayName();
if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || _item->_lockOwnerId != propagator()->account()->davUser())) {
qCInfo(lcPropagateDownload()) << "file is locked: making it read only";
FileSystem::setFileReadOnly(fn, true);
}
FileSystem::setFileHidden(fn, false);
// Maybe we downloaded a newer version of the file than we thought we would...

View file

@ -45,6 +45,13 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
rec._checksumHeader = _checksumHeader;
rec._e2eMangledName = _encryptedFileName.toUtf8();
rec._isE2eEncrypted = _isEncrypted;
rec._lockstate._locked = _locked == LockStatus::LockedItem;
rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName;
rec._lockstate._lockOwnerId = _lockOwnerId;
rec._lockstate._lockOwnerType = static_cast<qint64>(_lockOwnerType);
rec._lockstate._lockEditorApp = _lockEditorApp;
rec._lockstate._lockTime = _lockTime;
rec._lockstate._lockTimeout = _lockTimeout;
// Update the inode if possible
rec._inode = _inode;
@ -75,6 +82,13 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
item->_checksumHeader = rec._checksumHeader;
item->_encryptedFileName = rec.e2eMangledName();
item->_isEncrypted = rec._isE2eEncrypted;
item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem;
item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName;
item->_lockOwnerId = rec._lockstate._lockOwnerId;
item->_lockOwnerType = static_cast<LockOwnerType>(rec._lockstate._lockOwnerType);
item->_lockEditorApp = rec._lockstate._lockEditorApp;
item->_lockTime = rec._lockstate._lockTime;
item->_lockTimeout = rec._lockstate._lockTimeout;
return item;
}

View file

@ -93,6 +93,21 @@ public:
};
Q_ENUM(Status)
enum class LockStatus {
UnlockedItem = 0,
LockedItem = 1,
};
Q_ENUM(LockStatus)
enum class LockOwnerType : int{
UserLock = 0,
AppLock = 1,
TokenLock = 2,
};
Q_ENUM(LockOwnerType)
SyncJournalFileRecord toSyncJournalFileRecordWithInode(const QString &localFileName) const;
/** Creates a basic SyncFileItem from a DB record
@ -278,6 +293,14 @@ public:
QString _directDownloadUrl;
QString _directDownloadCookies;
LockStatus _locked = LockStatus::UnlockedItem;
QString _lockOwnerId;
QString _lockOwnerDisplayName;
LockOwnerType _lockOwnerType = LockOwnerType::UserLock;
QString _lockEditorApp;
qint64 _lockTime = 0;
qint64 _lockTimeout = 0;
};
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

View file

@ -64,6 +64,7 @@ nextcloud_add_test(UnifiedSearchListmodel)
nextcloud_add_test(ActivityListModel)
nextcloud_add_test(ActivityData)
nextcloud_add_test(TalkReply)
nextcloud_add_test(LockFile)
if( UNIX AND NOT APPLE )
nextcloud_add_test(InotifyWatcher)

View file

@ -298,11 +298,13 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces
// Don't care about the request and just return a full propfind
const QString davUri { QStringLiteral("DAV:") };
const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
const QString ncUri { QStringLiteral("http://nextcloud.org/ns") };
QBuffer buffer { &payload };
buffer.open(QIODevice::WriteOnly);
QXmlStreamWriter xml(&buffer);
xml.writeNamespace(davUri, QStringLiteral("d"));
xml.writeNamespace(ocUri, QStringLiteral("oc"));
xml.writeNamespace(ncUri, QStringLiteral("nc"));
xml.writeStartDocument();
xml.writeStartElement(davUri, QStringLiteral("multistatus"));
auto writeFileResponse = [&](const FileInfo &fileInfo) {
@ -998,6 +1000,8 @@ QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, cons
if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
reply = new FakePutMultiFileReply { info, op, newRequest, contentType, outgoingData->readAll(), this };
}
} else if (verb == QLatin1String("LOCK") || verb == QLatin1String("UNLOCK")) {
reply = new FakeFileLockReply{info, op, newRequest, this};
} else {
qDebug() << verb << outgoingData;
Q_UNREACHABLE();
@ -1249,3 +1253,50 @@ FakeJsonErrorReply::FakeJsonErrorReply(QNetworkAccessManager::Operation op,
: FakeErrorReply{ op, request, parent, httpErrorCode, reply.toJson() }
{
}
FakeFileLockReply::FakeFileLockReply(FileInfo &remoteRootFileInfo,
QNetworkAccessManager::Operation op,
const QNetworkRequest &request,
QObject *parent)
: FakePropfindReply(remoteRootFileInfo, op, request, parent)
{
const auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
QString fileName = getFilePathFromUrl(request.url());
Q_ASSERT(!fileName.isNull()); // for root, it should be empty
FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
if (!fileInfo) {
QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
return;
}
const QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
// Don't care about the request and just return a full propfind
const QString davUri { QStringLiteral("DAV:") };
const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
const QString ncUri { QStringLiteral("http://nextcloud.org/ns") };
payload.clear();
QBuffer buffer { &payload };
buffer.open(QIODevice::WriteOnly);
QXmlStreamWriter xml(&buffer);
xml.writeNamespace(davUri, QStringLiteral("d"));
xml.writeNamespace(ocUri, QStringLiteral("oc"));
xml.writeNamespace(ncUri, QStringLiteral("nc"));
xml.writeStartDocument();
xml.writeStartElement(davUri, QStringLiteral("prop"));
xml.writeTextElement(ncUri, QStringLiteral("lock"), verb == QStringLiteral("LOCK") ? "1" : "0");
xml.writeTextElement(ncUri, QStringLiteral("lock-owner-type"), QString::number(0));
xml.writeTextElement(ncUri, QStringLiteral("lock-owner"), QStringLiteral("admin"));
xml.writeTextElement(ncUri, QStringLiteral("lock-owner-displayname"), QStringLiteral("John Doe"));
xml.writeTextElement(ncUri, QStringLiteral("lock-owner-editor"), {});
xml.writeTextElement(ncUri, QStringLiteral("lock-time"), QString::number(1234560));
xml.writeTextElement(ncUri, QStringLiteral("lock-timeout"), QString::number(1800));
xml.writeEndElement(); // prop
xml.writeEndDocument();
}

View file

@ -402,6 +402,16 @@ public:
qint64 readData(char *, qint64) override { return 0; }
};
class FakeFileLockReply : public FakePropfindReply
{
Q_OBJECT
public:
FakeFileLockReply(FileInfo &remoteRootFileInfo,
QNetworkAccessManager::Operation op,
const QNetworkRequest &request,
QObject *parent);
};
// A delayed reply
template <class OriginalReply>
class DelayedReply : public OriginalReply

View file

@ -257,6 +257,20 @@ private slots:
QCOMPARE(bulkuploadAvailable, true);
}
void testFilesLockAvailable_filesLockAvailable_returnTrue()
{
QVariantMap filesMap;
filesMap["locking"] = "1.0";
QVariantMap capabilitiesMap;
capabilitiesMap["files"] = filesMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto filesLockAvailable = capabilities.filesLockAvailable();
QCOMPARE(filesLockAvailable, true);
}
};
QTEST_GUILESS_MAIN(TestCapabilities)

View file

@ -594,6 +594,53 @@ private slots:
auto expectedState = fakeFolder.currentLocalState();
QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
}
void testDiscoverLockChanges()
{
FakeFolder fakeFolder{FileInfo{}};
fakeFolder.syncEngine().account()->setCapabilities({{"activity", QVariantMap{{"apiv2", QVariantList{"filters", "filters-api", "previews", "rich-strings"}}}},
{"bruteforce", QVariantMap{{"delay", 0}}},
{"core", QVariantMap{{"pollinterval", 60}, {"webdav-root", "remote.php/webdav"}}},
{"dav", QVariantMap{{"chunking", "1.0"}}},
{"files", QVariantMap{{"bigfilechunking", true}, {"blacklisted_files", QVariantList{".htaccess"}},
{"comments", true},
{"directEditing", QVariantMap{{"etag", "c748e8fc588b54fc5af38c4481a19d20"}, {"url", "https://nextcloud.local/ocs/v2.php/apps/files/api/v1/directEditing"}}},
{"locking", "1.0"}}}});
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const QString fooFileRootFolder("foo");
const QString barFileRootFolder("bar");
const QString fooFileSubFolder("subfolder/foo");
const QString barFileSubFolder("subfolder/bar");
const QString fooFileAaaSubFolder("aaa/subfolder/foo");
const QString barFileAaaSubFolder("aaa/subfolder/bar");
fakeFolder.remoteModifier().insert(fooFileRootFolder);
fakeFolder.remoteModifier().insert(barFileRootFolder);
fakeFolder.remoteModifier().find("bar")->extraDavProperties = "<nc:lock>1</nc:lock>"
"<nc:lock-owner-type>0</nc:lock-owner-type>"
"<nc:lock-owner>user1</nc:lock-owner>"
"<nc:lock-owner-displayname>user1</nc:lock-owner-displayname>"
"<nc:lock-owner-editor>user1</nc:lock-owner-editor>"
"<nc:lock-time>1648046707</nc:lock-time>";
fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
fakeFolder.remoteModifier().insert(fooFileSubFolder);
fakeFolder.remoteModifier().insert(barFileSubFolder);
fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
fakeFolder.remoteModifier().insert(fooFileAaaSubFolder);
fakeFolder.remoteModifier().insert(barFileAaaSubFolder);
QVERIFY(fakeFolder.syncOnce());
fakeFolder.remoteModifier().find("bar")->extraDavProperties = "<nc:lock>0</nc:lock>";
fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem);
QVERIFY(fakeFolder.syncOnce());
}
};
QTEST_GUILESS_MAIN(TestLocalDiscovery)

488
test/testlockfile.cpp Normal file
View file

@ -0,0 +1,488 @@
#include "lockfilejobs.h"
#include "account.h"
#include "accountstate.h"
#include "common/syncjournaldb.h"
#include "common/syncjournalfilerecord.h"
#include "syncenginetestutils.h"
#include <QTest>
#include <QSignalSpy>
class TestLockFile : public QObject
{
Q_OBJECT
public:
TestLockFile() = default;
private slots:
void initTestCase()
{
}
void testLockFile_lockFile_lockSuccess()
{
const auto testFileName = QStringLiteral("file.txt");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
QVERIFY(lockFileSuccessSpy.wait());
QCOMPARE(lockFileErrorSpy.count(), 0);
}
void testLockFile_lockFile_lockError()
{
const auto testFileName = QStringLiteral("file.txt");
static constexpr auto LockedHttpErrorCode = 423;
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock/>\n"
" <nc:lock-owner-type>0</nc:lock-owner-type>\n"
" <nc:lock-owner>john</nc:lock-owner>\n"
" <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
}
return reply;
});
QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
QVERIFY(lockFileErrorSpy.wait());
QCOMPARE(lockFileSuccessSpy.count(), 0);
}
void testLockFile_fileLockStatus_queryLockStatus()
{
const auto testFileName = QStringLiteral("file.txt");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
QVERIFY(lockFileSuccessSpy.wait());
QCOMPARE(lockFileErrorSpy.count(), 0);
auto lockStatus = fakeFolder.account()->fileLockStatus(&fakeFolder.syncJournal(), testFileName);
QCOMPARE(lockStatus, OCC::SyncFileItem::LockStatus::LockedItem);
}
void testLockFile_fileCanBeUnlocked_canUnlock()
{
const auto testFileName = QStringLiteral("file.txt");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
QVERIFY(lockFileSuccessSpy.wait());
QCOMPARE(lockFileErrorSpy.count(), 0);
auto lockStatus = fakeFolder.account()->fileCanBeUnlocked(&fakeFolder.syncJournal(), testFileName);
QCOMPARE(lockStatus, true);
}
void testLockFile_lockFile_jobSuccess()
{
const auto testFileName = QStringLiteral("file.txt");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobSuccess.wait());
QCOMPARE(jobFailure.count(), 0);
auto fileRecord = OCC::SyncJournalFileRecord{};
QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
QCOMPARE(fileRecord._lockstate._locked, true);
QCOMPARE(fileRecord._lockstate._lockEditorApp, QString{});
QCOMPARE(fileRecord._lockstate._lockOwnerDisplayName, QStringLiteral("John Doe"));
QCOMPARE(fileRecord._lockstate._lockOwnerId, QStringLiteral("admin"));
QCOMPARE(fileRecord._lockstate._lockOwnerType, static_cast<qint64>(OCC::SyncFileItem::LockOwnerType::UserLock));
QCOMPARE(fileRecord._lockstate._lockTime, 1234560);
QCOMPARE(fileRecord._lockstate._lockTimeout, 1800);
QVERIFY(fakeFolder.syncOnce());
}
void testLockFile_lockFile_unlockFile_jobSuccess()
{
const auto testFileName = QStringLiteral("file.txt");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
lockFileJob->start();
QVERIFY(lockFileJobSuccess.wait());
QCOMPARE(lockFileJobFailure.count(), 0);
QVERIFY(fakeFolder.syncOnce());
auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
unlockFileJob->start();
QVERIFY(unlockFileJobSuccess.wait());
QCOMPARE(unlockFileJobFailure.count(), 0);
auto fileRecord = OCC::SyncJournalFileRecord{};
QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
QCOMPARE(fileRecord._lockstate._locked, false);
QVERIFY(fakeFolder.syncOnce());
}
void testLockFile_lockFile_alreadyLockedByUser()
{
static constexpr auto LockedHttpErrorCode = 423;
static constexpr auto PreconditionFailedHttpErrorCode = 412;
const auto testFileName = QStringLiteral("file.txt");
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock>1</nc:lock>\n"
" <nc:lock-owner-type>0</nc:lock-owner-type>\n"
" <nc:lock-owner>john</nc:lock-owner>\n"
" <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
} else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
}
return reply;
});
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobFailure.wait());
QCOMPARE(jobSuccess.count(), 0);
}
void testLockFile_lockFile_alreadyLockedByApp()
{
static constexpr auto LockedHttpErrorCode = 423;
static constexpr auto PreconditionFailedHttpErrorCode = 412;
const auto testFileName = QStringLiteral("file.txt");
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock>1</nc:lock>\n"
" <nc:lock-owner-type>1</nc:lock-owner-type>\n"
" <nc:lock-owner>john</nc:lock-owner>\n"
" <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
} else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
}
return reply;
});
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobFailure.wait());
QCOMPARE(jobSuccess.count(), 0);
}
void testLockFile_unlockFile_alreadyUnlocked()
{
static constexpr auto LockedHttpErrorCode = 423;
static constexpr auto PreconditionFailedHttpErrorCode = 412;
const auto testFileName = QStringLiteral("file.txt");
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock/>\n"
" <nc:lock-owner-type>0</nc:lock-owner-type>\n"
" <nc:lock-owner>john</nc:lock-owner>\n"
" <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
} else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
}
return reply;
});
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobSuccess.wait());
QCOMPARE(jobFailure.count(), 0);
}
void testLockFile_unlockFile_lockedBySomeoneElse()
{
static constexpr auto LockedHttpErrorCode = 423;
const auto testFileName = QStringLiteral("file.txt");
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock>1</nc:lock>\n"
" <nc:lock-owner-type>0</nc:lock-owner-type>\n"
" <nc:lock-owner>alice</nc:lock-owner>\n"
" <nc:lock-owner-displayname>Alice Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
}
return reply;
});
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobFailure.wait());
QCOMPARE(jobSuccess.count(), 0);
}
void testLockFile_lockFile_jobError()
{
const auto testFileName = QStringLiteral("file.txt");
static constexpr auto InternalServerErrorHttpErrorCode = 500;
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
reply = new FakeErrorReply(op, request, nullptr, InternalServerErrorHttpErrorCode, {});
}
return reply;
});
fakeFolder.localModifier().insert(QStringLiteral("file.txt"));
QVERIFY(fakeFolder.syncOnce());
auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
lockFileJob->start();
QVERIFY(lockFileJobFailure.wait());
QCOMPARE(lockFileJobSuccess.count(), 0);
QVERIFY(fakeFolder.syncOnce());
auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
unlockFileJob->start();
QVERIFY(unlockFileJobFailure.wait());
QCOMPARE(unlockFileJobSuccess.count(), 0);
QVERIFY(fakeFolder.syncOnce());
}
void testLockFile_lockFile_preconditionFailedError()
{
static constexpr auto PreconditionFailedHttpErrorCode = 412;
const auto testFileName = QStringLiteral("file.txt");
const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
"<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
" <nc:lock>1</nc:lock>\n"
" <nc:lock-owner-type>0</nc:lock-owner-type>\n"
" <nc:lock-owner>alice</nc:lock-owner>\n"
" <nc:lock-owner-displayname>Alice Doe</nc:lock-owner-displayname>\n"
" <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
" <nc:lock-time>1650619678</nc:lock-time>\n"
" <nc:lock-timeout>300</nc:lock-timeout>\n"
" <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
"</d:prop>\n");
FakeFolder fakeFolder{FileInfo{}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
QNetworkReply *reply = nullptr;
if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
}
return reply;
});
fakeFolder.localModifier().insert(testFileName);
QVERIFY(fakeFolder.syncOnce());
auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
job->start();
QVERIFY(jobFailure.wait());
QCOMPARE(jobSuccess.count(), 0);
}
};
QTEST_GUILESS_MAIN(TestLockFile)
#include "testlockfile.moc"