#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"
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)
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);
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())
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;
}
<< "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 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();
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();
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
QByteArray _checksumHeader;
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
+ SyncJournalFileLockInfo _lockstate;
};
bool OCSYNC_EXPORT
{
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
<< " | 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()
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
{
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);
}
} 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")) {
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
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...
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;
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;
}
};
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
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)
// 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) {
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)