mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-22 13:05:51 +03:00
Merge pull request #4420 from nextcloud/feature/files_lock
Feature/files lock
This commit is contained in:
commit
eeb0d20a7f
30 changed files with 1353 additions and 135 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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
|
||||
|
|
223
src/libsync/lockfilejobs.cpp
Normal file
223
src/libsync/lockfilejobs.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
61
src/libsync/lockfilejobs.h
Normal file
61
src/libsync/lockfilejobs.h
Normal 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
|
|
@ -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...
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
488
test/testlockfile.cpp
Normal 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"
|
Loading…
Reference in a new issue