fetch and store in sync database information abot lock state of files
authorMatthieu Gallien <matthieu.gallien@nextcloud.com>
Fri, 8 Apr 2022 08:23:50 +0000 (10:23 +0200)
committerMatthieu Gallien <matthieu_gallien@yahoo.fr>
Mon, 2 May 2022 11:52:05 +0000 (13:52 +0200)
fetch lock properties from server

decode them and store them in sync database

test to ensure we do properly handle those properties

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
src/common/syncjournaldb.cpp
src/common/syncjournalfilerecord.h
src/libsync/discovery.cpp
src/libsync/discoveryphase.cpp
src/libsync/discoveryphase.h
src/libsync/propagatedownload.cpp
src/libsync/syncfileitem.cpp
src/libsync/syncfileitem.h
test/syncenginetestutils.cpp
test/testlocaldiscovery.cpp

index ca5ae1faaeabad14fa48211bfda13ba69c6bb532..b5e4f97ca1dc77ae4edf27e3058f47458e838aaa 100644 (file)
@@ -48,7 +48,8 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
 
 #define GET_FILE_RECORD_QUERY \
         "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
-        "  ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted " \
+        "  ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
+        "  lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \
         " FROM metadata" \
         "  LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
 
@@ -66,6 +67,13 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
     rec._checksumHeader = query.baValue(9);
     rec._e2eMangledName = query.baValue(10);
     rec._isE2eEncrypted = query.intValue(11) > 0;
+    rec._lockstate._locked = query.intValue(12) > 0;
+    rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
+    rec._lockstate._lockOwnerId = query.stringValue(14);
+    rec._lockstate._lockOwnerType = query.int64Value(15);
+    rec._lockstate._lockEditorApp = query.stringValue(16);
+    rec._lockstate._lockTime = query.int64Value(17);
+    rec._lockstate._lockTimeout = query.int64Value(18);
 }
 
 static QByteArray defaultJournalMode(const QString &dbPath)
@@ -658,39 +666,31 @@ bool SyncJournalDb::updateMetadataTableStructure()
         return false;
     }
 
-    if (columns.indexOf("fileid") == -1) {
-        SqlQuery query(_db);
-        query.prepare("ALTER TABLE metadata ADD COLUMN fileid VARCHAR(128);");
-        if (!query.exec()) {
-            sqlFail(QStringLiteral("updateMetadataTableStructure: Add column fileid"), query);
-            re = false;
-        }
+    const auto addColumn = [this, &columns, &re] (const QString &columnName, const QString &dataType, const bool withIndex = false) {
+        const auto latin1ColumnName = columnName.toLatin1();
+        if (columns.indexOf(latin1ColumnName) == -1) {
+            SqlQuery query(_db);
+            const auto request = QStringLiteral("ALTER TABLE metadata ADD COLUMN %1 %2;").arg(columnName).arg(dataType);
+            query.prepare(request.toLatin1());
+            if (!query.exec()) {
+                sqlFail(QStringLiteral("updateMetadataTableStructure: add %1 column").arg(columnName), query);
+                re = false;
+            }
 
-        query.prepare("CREATE INDEX metadata_file_id ON metadata(fileid);");
-        if (!query.exec()) {
-            sqlFail(QStringLiteral("updateMetadataTableStructure: create index fileid"), query);
-            re = false;
-        }
-        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;
+            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 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,7 +884,10 @@ 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()) {
@@ -943,8 +911,10 @@ Result<void, QString> 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, 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();
@@ -968,6 +938,13 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
     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();
index b47f98e5b5d7348ab3dde2fff652d18c0203bad6..9f3e0b5187d484b082e56d229571129c4dca022e 100644 (file)
@@ -31,6 +31,16 @@ namespace OCC {
 
 class SyncFileItem;
 
+struct SyncJournalFileLockInfo {
+    bool _locked = false;
+    QString _lockOwnerDisplayName;
+    QString _lockOwnerId;
+    qint64 _lockOwnerType = 0;
+    QString _lockEditorApp;
+    qint64 _lockTime = 0;
+    qint64 _lockTimeout = 0;
+};
+
 /**
  * @brief The SyncJournalFileRecord class
  * @ingroup libsync
@@ -70,6 +80,7 @@ public:
     QByteArray _checksumHeader;
     QByteArray _e2eMangledName;
     bool _isE2eEncrypted = false;
+    SyncJournalFileLockInfo _lockstate;
 };
 
 bool OCSYNC_EXPORT
index 63b75a05fa25dd65221b845dbd1df45c43cc76d2..7527d06d53a469aeb2b1183c19bff2cbff491f87 100644 (file)
@@ -363,6 +363,8 @@ void ProcessDirectoryJob::processFile(PathTuple path,
 {
     const char *hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false";
     const char *hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false";
+    const auto serverFileIsLocked = serverEntry.locked == SyncFileItem::LockStatus::LockedItem ? "locked" : "not locked";
+    const auto localFileIsLocked = dbEntry._lockstate._locked ? "locked" : "not locked";
     qCInfo(lcDisco).nospace() << "Processing " << path._original
                               << " | valid: " << dbEntry.isValid() << "/" << hasLocal << "/" << hasServer
                               << " | mtime: " << dbEntry._modtime << "/" << localEntry.modtime << "/" << serverEntry.modtime
@@ -374,7 +376,8 @@ void ProcessDirectoryJob::processFile(PathTuple path,
                               << " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/"
                               << " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile)
                               << " | e2ee: " << dbEntry._isE2eEncrypted << "/" << serverEntry.isE2eEncrypted
-                              << " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName;
+                              << " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName
+                              << " | file lock: " << localFileIsLocked << "//" << serverFileIsLocked;
 
     if (localEntry.isValid()
         && !serverEntry.isValid()
@@ -483,6 +486,14 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
         Q_ASSERT(serverEntry.e2eMangledName.startsWith(rootPath));
         return serverEntry.e2eMangledName.mid(rootPath.length());
     }();
+    item->_locked = serverEntry.locked;
+    item->_lockOwnerDisplayName = serverEntry.lockOwnerDisplayName;
+    item->_lockOwnerId = serverEntry.lockOwnerId;
+    item->_lockOwnerType = serverEntry.lockOwnerType;
+    item->_lockEditorApp = serverEntry.lockEditorApp;
+    item->_lockTime = serverEntry.lockTime;
+    item->_lockTimeout = serverEntry.lockTimeout;
+    qCInfo(lcDisco()) << item->_locked << item->_lockOwnerDisplayName << item->_lockOwnerId << item->_lockOwnerType << item->_lockEditorApp << item->_lockTime << item->_lockTimeout;
 
     // Check for missing server data
     {
index d71dca4335cf57f4940b7e195e1dc5e2d0c54dc9..67b91ad872c79480fc93204e1d49b915b11bd67c 100644 (file)
@@ -378,6 +378,15 @@ void DiscoverySingleDirectoryJob::start()
     if (_account->capabilities().clientSideEncryptionAvailable()) {
         props << "http://nextcloud.org/ns:is-encrypted";
     }
+    if (_account->capabilities().filesLockAvailable()) {
+        props << "http://nextcloud.org/ns:lock"
+              << "http://nextcloud.org/ns:lock-owner-displayname"
+              << "http://nextcloud.org/ns:lock-owner"
+              << "http://nextcloud.org/ns:lock-owner-type"
+              << "http://nextcloud.org/ns:lock-owner-editor"
+              << "http://nextcloud.org/ns:lock-time"
+              << "http://nextcloud.org/ns:lock-timeout";
+    }
 
     lsColJob->setProperties(props);
 
@@ -445,7 +454,46 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInf
             }
         } else if (property == "is-encrypted" && value == QStringLiteral("1")) {
             result.isE2eEncrypted = true;
+        } else if (property == "lock") {
+            result.locked = (value == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem);
+        }
+        if (property == "lock-owner-displayname") {
+            result.lockOwnerDisplayName = value;
+        }
+        if (property == "lock-owner") {
+            result.lockOwnerId = value;
+        }
+        if (property == "lock-owner-type") {
+            auto ok = false;
+            const auto intConvertedValue = value.toULongLong(&ok);
+            if (ok) {
+                result.lockOwnerType = static_cast<SyncFileItem::LockOwnerType>(intConvertedValue);
+            } else {
+                result.lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
+            }
+        }
+        if (property == "lock-owner-editor") {
+            result.lockEditorApp = value;
         }
+        if (property == "lock-time") {
+            auto ok = false;
+            const auto intConvertedValue = value.toULongLong(&ok);
+            if (ok) {
+                result.lockTime = intConvertedValue;
+            } else {
+                result.lockTime = 0;
+            }
+        }
+        if (property == "lock-timeout") {
+            auto ok = false;
+            const auto intConvertedValue = value.toULongLong(&ok);
+            if (ok) {
+                result.lockTimeout = intConvertedValue;
+            } else {
+                result.lockTimeout = 0;
+            }
+        }
+
     }
 
     if (result.isDirectory && map.contains("size")) {
index cd210a644639c0cc828d2a07fc0e53d9b3ec2a87..3f5240dd1de316d11591f31d205cf1a7a02ffe52 100644 (file)
@@ -65,6 +65,14 @@ struct RemoteInfo
 
     QString directDownloadUrl;
     QString directDownloadCookies;
+
+    SyncFileItem::LockStatus locked = SyncFileItem::LockStatus::UnlockedItem;
+    QString lockOwnerDisplayName;
+    QString lockOwnerId;
+    SyncFileItem::LockOwnerType lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
+    QString lockEditorApp;
+    qint64 lockTime = 0;
+    qint64 lockTimeout = 0;
 };
 
 struct LocalInfo
index bc2922070038d78ecf267c2813537436a79bf478..57e5572deb82b66daca57b94312788ad45561373 100644 (file)
@@ -1211,6 +1211,12 @@ void PropagateDownloadFile::downloadFinished()
         return;
     }
 
+    qCInfo(lcPropagateDownload()) << propagator()->account()->davUser() << propagator()->account()->davDisplayName() << propagator()->account()->displayName();
+    if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || _item->_lockOwnerId != propagator()->account()->davUser())) {
+        qCInfo(lcPropagateDownload()) << "file is locked: making it read only";
+        FileSystem::setFileReadOnly(fn, true);
+    }
+
     FileSystem::setFileHidden(fn, false);
 
     // Maybe we downloaded a newer version of the file than we thought we would...
index cc7afe0f0e8114d6e97d7e42bae189cf980e5b3d..07202d41f210db5c7d4966fb30fd38606fa07b94 100644 (file)
@@ -45,6 +45,13 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
     rec._checksumHeader = _checksumHeader;
     rec._e2eMangledName = _encryptedFileName.toUtf8();
     rec._isE2eEncrypted = _isEncrypted;
+    rec._lockstate._locked = _locked == LockStatus::LockedItem;
+    rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName;
+    rec._lockstate._lockOwnerId = _lockOwnerId;
+    rec._lockstate._lockOwnerType = static_cast<qint64>(_lockOwnerType);
+    rec._lockstate._lockEditorApp = _lockEditorApp;
+    rec._lockstate._lockTime = _lockTime;
+    rec._lockstate._lockTimeout = _lockTimeout;
 
     // Update the inode if possible
     rec._inode = _inode;
@@ -75,6 +82,13 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
     item->_checksumHeader = rec._checksumHeader;
     item->_encryptedFileName = rec.e2eMangledName();
     item->_isEncrypted = rec._isE2eEncrypted;
+    item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem;
+    item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName;
+    item->_lockOwnerId = rec._lockstate._lockOwnerId;
+    item->_lockOwnerType = static_cast<LockOwnerType>(rec._lockstate._lockOwnerType);
+    item->_lockEditorApp = rec._lockstate._lockEditorApp;
+    item->_lockTime = rec._lockstate._lockTime;
+    item->_lockTimeout = rec._lockstate._lockTimeout;
     return item;
 }
 
index a38b4e94fc2c7663fbc647e43a15bd8bfc502e15..9dc6fa4f522d06991c57acc89ae39413d4f6efe6 100644 (file)
@@ -93,6 +93,21 @@ public:
     };
     Q_ENUM(Status)
 
+    enum class LockStatus {
+        UnlockedItem = 0,
+        LockedItem = 1,
+    };
+
+    Q_ENUM(LockStatus)
+
+    enum class LockOwnerType : int{
+        UserLock = 0,
+        AppLock = 1,
+        TokenLock = 2,
+    };
+
+    Q_ENUM(LockOwnerType)
+
     SyncJournalFileRecord toSyncJournalFileRecordWithInode(const QString &localFileName) const;
 
     /** Creates a basic SyncFileItem from a DB record
@@ -278,6 +293,14 @@ public:
 
     QString _directDownloadUrl;
     QString _directDownloadCookies;
+
+    LockStatus _locked = LockStatus::UnlockedItem;
+    QString _lockOwnerId;
+    QString _lockOwnerDisplayName;
+    LockOwnerType _lockOwnerType = LockOwnerType::UserLock;
+    QString _lockEditorApp;
+    qint64 _lockTime = 0;
+    qint64 _lockTimeout = 0;
 };
 
 inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)
index 1d813e3ff3a0c1b60f035d17ad3462cacfae8b55..86873ca8ff5a5a548980bd7af5319a990d0719cd 100644 (file)
@@ -298,11 +298,13 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces
     // Don't care about the request and just return a full propfind
     const QString davUri { QStringLiteral("DAV:") };
     const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
+    const QString ncUri { QStringLiteral("http://nextcloud.org/ns") };
     QBuffer buffer { &payload };
     buffer.open(QIODevice::WriteOnly);
     QXmlStreamWriter xml(&buffer);
     xml.writeNamespace(davUri, QStringLiteral("d"));
     xml.writeNamespace(ocUri, QStringLiteral("oc"));
+    xml.writeNamespace(ncUri, QStringLiteral("nc"));
     xml.writeStartDocument();
     xml.writeStartElement(davUri, QStringLiteral("multistatus"));
     auto writeFileResponse = [&](const FileInfo &fileInfo) {
index d84f95345ffbb6987aaaf7e39bea4744408a1dd1..c3719d0fc0117060e620d9c4d8d6b39dab1ee5f0 100644 (file)
@@ -594,6 +594,55 @@ private slots:
         auto expectedState = fakeFolder.currentLocalState();
         QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
     }
+
+    void testDiscoverLockChanges()
+    {
+//        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 = "<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><oc:size>20020</oc:size>";
+
+        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)