Skip to content

Commit

Permalink
fetch and store in sync database information abot lock state of files
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
mgallien committed Apr 13, 2022
1 parent 901e2b6 commit ee0bb4a
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 54 deletions.
91 changes: 40 additions & 51 deletions src/common/syncjournaldb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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);");
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -937,8 +920,9 @@ 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, 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();
Expand All @@ -962,6 +946,11 @@ 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._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();
Expand Down
5 changes: 5 additions & 0 deletions src/common/syncjournalfilerecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/libsync/discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
{
Expand Down
39 changes: 39 additions & 0 deletions src/libsync/discoveryphase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -445,6 +452,38 @@ 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.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;
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/libsync/discoveryphase.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/libsync/lockfilejobs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/libsync/propagatedownload.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/libsync/syncfileitem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
13 changes: 13 additions & 0 deletions src/libsync/syncfileitem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions test/syncenginetestutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions test/testlocaldiscovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<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)
Expand Down

0 comments on commit ee0bb4a

Please sign in to comment.