diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 0b66a6cc08c34..4c43c5f7df1b2 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -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, lockType, lockOwner, lockOwnerEditor, lockTime " \ " FROM metadata" \ " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" @@ -66,6 +67,11 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que rec._checksumHeader = query.baValue(9); rec._e2eMangledName = query.baValue(10); rec._isE2eEncrypted = query.intValue(11) > 0; + rec._locked = query.intValue(12) > 0; + rec._lockOwner = query.baValue(13); + rec._lockOwnerType = query.int64Value(14); + rec._lockEditorAppId = query.int64Value(15); + rec._lockTime = query.int64Value(16); } static QByteArray defaultJournalMode(const QString &dbPath) @@ -658,6 +664,20 @@ bool SyncJournalDb::updateMetadataTableStructure() return false; } + const auto addColumn = [this, &columns, &re] (const QString &columnName, const QString &dataType) { + auto latin1ColumnName = columnName.toLatin1(); + if (columns.indexOf(latin1ColumnName) == -1) { + SqlQuery query(_db); + 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; + } + commitInternal(QStringLiteral("update database structure: add %1 column").arg(columnName)); + } + }; + if (columns.indexOf("fileid") == -1) { SqlQuery query(_db); query.prepare("ALTER TABLE metadata ADD COLUMN fileid VARCHAR(128);"); @@ -722,54 +742,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 +783,12 @@ bool SyncJournalDb::updateMetadataTableStructure() commitInternal(QStringLiteral("update database structure: add e2eMangledName index")); } + addColumn(QStringLiteral("lock"), QStringLiteral("INTEGER")); + addColumn(QStringLiteral("lockType"), QStringLiteral("INTEGER")); + addColumn(QStringLiteral("lockOwner"), QStringLiteral("TEXT")); + addColumn(QStringLiteral("lockOwnerEditor"), QStringLiteral("INTEGER")); + addColumn(QStringLiteral("lockTime"), QStringLiteral("INTEGER")); + return re; } @@ -937,8 +920,9 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & 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);"), + "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, " + "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwner, lockOwnerEditor, lockTime) " + "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23);"), _db); if (!query) { return query->error(); @@ -962,6 +946,11 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(16, contentChecksumTypeId); query->bindValue(17, record._e2eMangledName); query->bindValue(18, record._isE2eEncrypted); + query->bindValue(19, record._locked ? 1 : 0); + query->bindValue(20, record._lockOwnerType); + query->bindValue(21, record._lockOwner); + query->bindValue(22, record._lockEditorAppId); + query->bindValue(23, record._lockTime); if (!query->exec()) { return query->error(); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index b47f98e5b5d73..0d00ce6a22ef5 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -70,6 +70,11 @@ class OCSYNC_EXPORT SyncJournalFileRecord QByteArray _checksumHeader; QByteArray _e2eMangledName; bool _isE2eEncrypted = false; + bool _locked = false; + QByteArray _lockOwner; + qint64 _lockOwnerType = 0; + qint64 _lockEditorAppId = 0; + qint64 _lockTime = 0; }; bool OCSYNC_EXPORT diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 63b75a05fa25d..3fdd74c20dfe4 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -374,7 +374,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: " << (serverEntry.isValid() ? (serverEntry.locked == SyncFileItem::LockStatus::LockedItem ? "locked" : "not locked") : (dbEntry._locked ? "locked" : "not locked")); if (localEntry.isValid() && !serverEntry.isValid() @@ -483,6 +484,12 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( Q_ASSERT(serverEntry.e2eMangledName.startsWith(rootPath)); return serverEntry.e2eMangledName.mid(rootPath.length()); }(); + item->_locked = serverEntry.locked; + item->_lockOwner = serverEntry.lockOwner; + item->_lockOwnerType = serverEntry.lockOwnerType; + item->_lockEditorAppId = serverEntry.lockEditorAppId; + item->_lockTime = serverEntry.lockTime; + qCInfo(lcDisco()) << item->_locked << item->_lockOwner << item->_lockOwnerType << item->_lockEditorAppId << item->_lockTime; // Check for missing server data { diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index d71dca4335cf5..798e2719a7db7 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -378,6 +378,13 @@ 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-type" + << "http://nextcloud.org/ns:lock-owner-editor" + << "http://nextcloud.org/ns:lock-time"; + } lsColJob->setProperties(props); @@ -445,6 +452,38 @@ static void propertyMapToRemoteInfo(const QMap &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.lockOwner = value.toUtf8(); + } + if (property == "lock-owner-type") { + auto ok = false; + const auto intConvertedValue = value.toULongLong(&ok); + if (ok) { + result.lockOwnerType = intConvertedValue; + } else { + result.lockOwnerType = 0; + } + } + if (property == "lock-owner-editor") { + auto ok = false; + const auto intConvertedValue = value.toULongLong(&ok); + if (ok) { + result.lockEditorAppId = intConvertedValue; + } else { + result.lockEditorAppId = 0; + } + } + if (property == "lock-time") { + auto ok = false; + const auto intConvertedValue = value.toULongLong(&ok); + if (ok) { + result.lockTime = intConvertedValue; + } else { + result.lockTime = 0; + } } } diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index cd210a644639c..427dcca57e51b 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -65,6 +65,12 @@ struct RemoteInfo QString directDownloadUrl; QString directDownloadCookies; + + SyncFileItem::LockStatus locked = SyncFileItem::LockStatus::UnlockedItem; + QByteArray lockOwner; + qint64 lockOwnerType = 0; + qint64 lockEditorAppId = 0; + qint64 lockTime = 0; }; struct LocalInfo diff --git a/src/libsync/lockfilejobs.cpp b/src/libsync/lockfilejobs.cpp index 1666318d4db0f..cda9d9208a755 100644 --- a/src/libsync/lockfilejobs.cpp +++ b/src/libsync/lockfilejobs.cpp @@ -33,10 +33,10 @@ void LockFileJob::start() QByteArray verb; switch(_requestedLockState) { - case SyncFileItem::LockedItem: + case SyncFileItem::LockStatus::LockedItem: verb = "LOCK"; break; - case SyncFileItem::UnlockedItem: + case SyncFileItem::LockStatus::UnlockedItem: verb = "UNLOCK"; break; } diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index bc2922070038d..3153b5128c3fc 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -1143,6 +1143,10 @@ void PropagateDownloadFile::downloadFinished() qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime; } + if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != 0 || _item->_lockOwner != propagator()->account()->davUser())) { + FileSystem::setFileReadOnly(_tmpFile.fileName(), true); + } + bool previousFileExists = FileSystem::fileExists(fn); if (previousFileExists) { // Preserve the existing file permissions. diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index cc7afe0f0e811..8947491c09436 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -45,6 +45,11 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); rec._isE2eEncrypted = _isEncrypted; + rec._locked = _locked == LockStatus::LockedItem; + rec._lockOwner = _lockOwner; + rec._lockOwnerType = _lockOwnerType; + rec._lockEditorAppId = _lockEditorAppId; + rec._lockTime = _lockTime; // Update the inode if possible rec._inode = _inode; @@ -75,6 +80,11 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_checksumHeader = rec._checksumHeader; item->_encryptedFileName = rec.e2eMangledName(); item->_isEncrypted = rec._isE2eEncrypted; + item->_locked = rec._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem; + item->_lockOwner = rec._lockOwner; + item->_lockOwnerType = rec._lockOwnerType; + item->_lockEditorAppId = rec._lockEditorAppId; + item->_lockTime = rec._lockTime; return item; } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index a38b4e94fc2c7..a9be56bf9d17f 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -93,6 +93,13 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem }; Q_ENUM(Status) + enum class LockStatus { + LockedItem, + UnlockedItem, + }; + + Q_ENUM(LockStatus) + SyncJournalFileRecord toSyncJournalFileRecordWithInode(const QString &localFileName) const; /** Creates a basic SyncFileItem from a DB record @@ -278,6 +285,12 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem QString _directDownloadUrl; QString _directDownloadCookies; + + LockStatus _locked = LockStatus::UnlockedItem; + QByteArray _lockOwner; + qint64 _lockOwnerType = 0; + qint64 _lockEditorAppId = 0; + qint64 _lockTime = 0; }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 1d813e3ff3a0c..86873ca8ff5a5 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -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) { diff --git a/test/testlocaldiscovery.cpp b/test/testlocaldiscovery.cpp index d84f95345ffbb..4772ed0fbd4c2 100644 --- a/test/testlocaldiscovery.cpp +++ b/test/testlocaldiscovery.cpp @@ -594,6 +594,58 @@ private slots: auto expectedState = fakeFolder.currentLocalState(); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); } + + void testDiscoverLockChanges() + { + constexpr auto FUTURE_MTIME = 0xFFFFFFFF; + constexpr auto CURRENT_MTIME = 1646057277; + +// Logger::instance()->setLogDebug(true); + + 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 = "1" + "0" + "user1" + "user1" + "user1" + "164804670720020"; + + 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 = "0"; + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem); + QVERIFY(fakeFolder.syncOnce()); + } }; QTEST_GUILESS_MAIN(TestLocalDiscovery)