Skip to content

Commit 5124562

Browse files
authored
Merge pull request #8502 from nextcloud/feature/notify-file-ids
feat: support `notify_file_id` push notifications
2 parents 6382cb7 + 5667ea2 commit 5124562

22 files changed

+408
-14
lines changed

src/common/preparedsqlquerymanager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class OCSYNC_EXPORT PreparedSqlQueryManager
9898
ListAllTopLevelE2eeFoldersStatusLessThanQuery,
9999
FolderUpdateInvalidEncryptionStatus,
100100
FileUpdateInvalidEncryptionStatus,
101+
HasFileIdQuery,
101102

102103
PreparedQueryCount
103104
};

src/common/syncjournaldb.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
#include <QCryptographicHash>
88
#include <QFile>
9+
#include <QJsonArray>
10+
#include <QJsonDocument>
911
#include <QLoggingCategory>
1012
#include <QStringList>
1113
#include <QElapsedTimer>
@@ -33,6 +35,8 @@
3335
static constexpr auto MAJOR_VERSION_3 = 3;
3436
static constexpr auto MINOR_VERSION_16 = 16;
3537

38+
using namespace Qt::StringLiterals;
39+
3640
namespace OCC {
3741

3842
Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
@@ -1769,6 +1773,58 @@ bool SyncJournalDb::updateLocalMetadata(const QString &filename,
17691773
return true;
17701774
}
17711775

1776+
bool SyncJournalDb::hasFileIds(const QList<qint64> &fileIds)
1777+
{
1778+
if (fileIds.isEmpty()) {
1779+
// no need to check the db if no file id matches
1780+
return false;
1781+
}
1782+
1783+
QMutexLocker locker(&_mutex);
1784+
1785+
if (!checkConnect()) {
1786+
return false;
1787+
}
1788+
1789+
// quick workaround for looking up pure numeric file IDs: cast it to integer
1790+
//
1791+
// using `IN` with a list of IDs does not allow for a prepared query: the execution plan
1792+
// would be different depending on the amount of elements as each element is added to a
1793+
// temporary table one-by-one. with `json_each()`, all that changes is one string
1794+
// parameter which is perfect for creating a prepared query :)
1795+
const auto query = _queryManager.get(
1796+
PreparedSqlQueryManager::HasFileIdQuery,
1797+
"SELECT 1 FROM metadata, json_each(?1) file_ids WHERE CAST(metadata.fileid AS INTEGER) = CAST(file_ids.value AS INTEGER) LIMIT 1;"_ba,
1798+
_db
1799+
);
1800+
if (!query) {
1801+
qCWarning(lcDb) << "database error:" << query->error();
1802+
return false;
1803+
}
1804+
1805+
// we have a prepared query, so let's build up a JSON array of file IDs to check for.
1806+
// Strings are used here to avoid any surprises during serialisation: JSON only really
1807+
// has a double type, and who knows when the representation changes to the +e* variant.
1808+
QJsonArray fileIdStrings = {};
1809+
for (const auto &fileId : fileIds) {
1810+
fileIdStrings.append(QString::number(fileId));
1811+
}
1812+
const auto fileIdsParameter = QJsonDocument(fileIdStrings).toJson(QJsonDocument::Compact);
1813+
query->bindValue(1, fileIdsParameter);
1814+
1815+
if (!query->exec()) {
1816+
qCWarning(lcDb) << "file id query failed:" << query->error();
1817+
return false;
1818+
}
1819+
1820+
if (query->next().hasData && query->intValue(0) == 1) {
1821+
// at least one file ID from the passed list is present
1822+
return true;
1823+
}
1824+
1825+
return false;
1826+
}
1827+
17721828
Optional<SyncJournalDb::HasHydratedDehydrated> SyncJournalDb::hasHydratedOrDehydratedFiles(const QByteArray &filename)
17731829
{
17741830
QMutexLocker locker(&_mutex);

src/common/syncjournaldb.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject
7676
[[nodiscard]] bool updateLocalMetadata(const QString &filename,
7777
qint64 modtime, qint64 size, quint64 inode, const SyncJournalFileLockInfo &lockInfo);
7878

79+
[[nodiscard]] bool hasFileIds(const QList<qint64> &fileIds);
80+
7981
/// Return value for hasHydratedOrDehydratedFiles()
8082
struct HasHydratedDehydrated
8183
{

src/gui/folder.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Folder::Folder(const FolderDefinition &definition,
9292

9393
connect(_accountState.data(), &AccountState::isConnectedChanged, this, &Folder::canSyncChanged);
9494
connect(_engine.data(), &SyncEngine::rootEtag, this, &Folder::etagRetrievedFromSyncEngine);
95+
connect(_engine.data(), &SyncEngine::rootFileIdReceived, this, &Folder::rootFileIdReceivedFromSyncEngine);
9596

9697
connect(_engine.data(), &SyncEngine::started, this, &Folder::slotSyncStarted, Qt::QueuedConnection);
9798
connect(_engine.data(), &SyncEngine::finished, this, &Folder::slotSyncFinished, Qt::QueuedConnection);
@@ -396,6 +397,12 @@ void Folder::etagRetrievedFromSyncEngine(const QByteArray &etag, const QDateTime
396397
_lastEtag = etag;
397398
}
398399

400+
void Folder::rootFileIdReceivedFromSyncEngine(const qint64 fileId)
401+
{
402+
qCDebug(lcFolder).nospace() << "retrieved root fileId=" << fileId;
403+
_rootFileId = fileId;
404+
}
405+
399406
void Folder::showSyncResultPopup()
400407
{
401408
if (_syncResult.firstItemNew()) {
@@ -1002,6 +1009,11 @@ void Folder::migrateBlackListPath(const QString &legacyPath)
10021009
}
10031010
}
10041011

1012+
bool Folder::hasFileIds(const QList<qint64>& fileIds) const
1013+
{
1014+
return fileIds.contains(_rootFileId) || journalDb()->hasFileIds(fileIds);
1015+
}
1016+
10051017
QString Folder::filePath(const QString& fileName)
10061018
{
10071019
const auto folderDir = QDir(_canonicalLocalPath);

src/gui/folder.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ class Folder : public QObject
316316
void blacklistPath(const QString &path);
317317
void migrateBlackListPath(const QString &legacyPath);
318318

319+
/// whether the current folder contains any of the passed fileIds
320+
[[nodiscard]] bool hasFileIds(const QList<qint64>& fileIds) const;
321+
319322
signals:
320323
void syncStateChange();
321324
void syncStarted();
@@ -429,6 +432,8 @@ private slots:
429432
void etagRetrieved(const QByteArray &, const QDateTime &tp);
430433
void etagRetrievedFromSyncEngine(const QByteArray &, const QDateTime &time);
431434

435+
void rootFileIdReceivedFromSyncEngine(qint64 fileId);
436+
432437
void slotEmitFinishedDelayed();
433438

434439
void slotNewBigFolderDiscovered(const QString &, bool isExternal);
@@ -585,6 +590,9 @@ private slots:
585590
QMetaObject::Connection _officeFileLockReleaseUnlockFailure;
586591
QMetaObject::Connection _fileLockSuccess;
587592
QMetaObject::Connection _fileLockFailure;
593+
594+
/// The remote file ID of the current folder.
595+
qint64 _rootFileId = 0;
588596
};
589597
}
590598

src/gui/folderman.cpp

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,15 +2084,35 @@ void FolderMan::slotSetupPushNotifications(const Folder::Map &folderMap)
20842084

20852085
void FolderMan::slotProcessFilesPushNotification(Account *account)
20862086
{
2087-
qCInfo(lcFolderMan) << "Got files push notification for account" << account;
2087+
qCDebug(lcFolderMan) << "received notify_file push notification account=" << account->displayName();
20882088

20892089
for (auto folder : std::as_const(_folderMap)) {
20902090
// Just run on the folders that belong to this account
20912091
if (folder->accountState()->account() != account) {
20922092
continue;
20932093
}
20942094

2095-
qCInfo(lcFolderMan) << "Schedule folder" << folder << "for sync";
2095+
qCInfo(lcFolderMan).nospace() << "scheduling sync folder=" << folder->alias() << " account=" << account->displayName() << " reason=notify_file";
2096+
scheduleFolder(folder);
2097+
}
2098+
}
2099+
2100+
void FolderMan::slotProcessFileIdsPushNotification(Account *account, const QList<qint64> &fileIds)
2101+
{
2102+
qCDebug(lcFolderMan).nospace() << "received notify_file_id push notification account=" << account->displayName() << " fileIds=" << fileIds;
2103+
2104+
for (auto folder : std::as_const(_folderMap)) {
2105+
// Just run on the folders that belong to this account
2106+
if (folder->accountState()->account() != account) {
2107+
continue;
2108+
}
2109+
2110+
if (!folder->hasFileIds(fileIds)) {
2111+
qCDebug(lcFolderMan).nospace() << "no matching file ids, ignoring folder=" << folder->alias() << " account=" << account->displayName();
2112+
continue;
2113+
}
2114+
2115+
qCInfo(lcFolderMan).nospace() << "scheduling sync folder=" << folder->alias() << " account=" << account->displayName() << " reason=notify_file_id";
20962116
scheduleFolder(folder);
20972117
}
20982118
}
@@ -2104,6 +2124,7 @@ void FolderMan::slotConnectToPushNotifications(const AccountPtr &account)
21042124
if (pushNotificationsFilesReady(account)) {
21052125
qCInfo(lcFolderMan) << "Push notifications ready";
21062126
connect(pushNotifications, &PushNotifications::filesChanged, this, &FolderMan::slotProcessFilesPushNotification, Qt::UniqueConnection);
2127+
connect(pushNotifications, &PushNotifications::fileIdsChanged, this, &FolderMan::slotProcessFileIdsPushNotification, Qt::UniqueConnection);
21072128
}
21082129
}
21092130

src/gui/folderman.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ private slots:
330330

331331
void slotSetupPushNotifications(const OCC::Folder::Map &);
332332
void slotProcessFilesPushNotification(OCC::Account *account);
333+
void slotProcessFileIdsPushNotification(OCC::Account *account, const QList<qint64> &fileIds);
333334
void slotConnectToPushNotifications(const OCC::AccountPtr &account);
334335

335336
void slotLeaveShare(const QString &localFile, const QByteArray &folderToken = {});

src/libsync/discovery.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2271,6 +2271,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
22712271
}
22722272

22732273
connect(serverJob, &DiscoverySingleDirectoryJob::etag, this, &ProcessDirectoryJob::etag);
2274+
connect(serverJob, &DiscoverySingleDirectoryJob::firstDirectoryFileId, this, &ProcessDirectoryJob::rootFileIdReceived);
22742275
connect(serverJob, &DiscoverySingleDirectoryJob::setfolderQuota, this, &ProcessDirectoryJob::setFolderQuota);
22752276
_discoveryData->_currentlyActiveJobs++;
22762277
_pendingAsyncJobs++;

src/libsync/discovery.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ class ProcessDirectoryJob : public QObject
308308
// The root etag of this directory was fetched
309309
void etag(const QByteArray &, const QDateTime &time);
310310
void updatedRootFolderQuota(const int64_t &bytesUsed, const int64_t &bytesAvailable);
311+
void rootFileIdReceived(qint64 fileId);
311312

312313
private slots:
313314
void setFolderQuota(const OCC::FolderQuota &folderQuota);

src/libsync/discoveryphase.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,19 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi
503503
}
504504
}
505505
if (map.contains("fileid"_L1)) {
506+
// this is from the "oc:fileid" property, this is the plain ID without any special format (e.g. "2")
506507
_localFileId = map.value("fileid"_L1).toUtf8();
508+
509+
bool ok = false;
510+
if (qint64 numericFileId = _localFileId.toLongLong(&ok, 10); ok) {
511+
qCDebug(lcDiscovery).nospace() << "received numericFileId=" << numericFileId;
512+
emit firstDirectoryFileId(numericFileId);
513+
} else {
514+
qCWarning(lcDiscovery).nospace() << "conversion to qint64 failed _localFileId=" << _localFileId;
515+
}
507516
}
508517
if (map.contains("id"_L1)) {
518+
// this is from the "oc:id" property, the format is e.g. "00000002oc123xyz987e"
509519
_fileId = map.value("id"_L1).toUtf8();
510520
}
511521
if (map.contains("is-encrypted"_L1) && map.value("is-encrypted"_L1) == "1"_L1) {

0 commit comments

Comments
 (0)