Skip to content

Commit

Permalink
On folder move execute only one UPDATE query for all nested items.
Browse files Browse the repository at this point in the history
Signed-off-by: alex-z <[email protected]>
  • Loading branch information
allexzander committed Nov 13, 2023
1 parent d0cd017 commit badbf13
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/common/preparedsqlquerymanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class OCSYNC_EXPORT PreparedSqlQueryManager
GetE2EeLockedFolderQuery,
GetE2EeLockedFoldersQuery,
DeleteE2EeLockedFolderQuery,
MoveFilesInPathQuery,

PreparedQueryCount
};
Expand Down
34 changes: 34 additions & 0 deletions src/common/syncjournaldb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,18 @@ bool SyncJournalDb::checkConnect()
end - text, 0));
}, nullptr, nullptr);

sqlite3_create_function(_db.sqliteDb(), "path_hash", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
[] (sqlite3_context *ctx,int, sqlite3_value **argv) {
const auto text = reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
sqlite3_result_int64(ctx, c_jhash64(reinterpret_cast<const uint8_t*>(text), strlen(text), 0));
}, nullptr, nullptr);

sqlite3_create_function(_db.sqliteDb(), "path_length", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
[] (sqlite3_context *ctx,int, sqlite3_value **argv) {
const auto text = reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
sqlite3_result_int64(ctx, strlen(text));
}, nullptr, nullptr);

/* Because insert is so slow, we do everything in a transaction, and only need one call to commit */
startTransaction();

Expand Down Expand Up @@ -1030,6 +1042,28 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
return {};
}

bool SyncJournalDb::updateParentForAllChildren(const QByteArray &oldParentPath, const QByteArray &newParentPath)
{
qCInfo(lcDb) << "Moving files from path" << oldParentPath << "to path" << newParentPath;

if (!checkConnect()) {
qCWarning(lcDb) << "Failed to connect database.";
return false;
}

const auto query = _queryManager.get(PreparedSqlQueryManager::MoveFilesInPathQuery,
QByteArrayLiteral("UPDATE metadata"
" SET path = REPLACE(path, ?1, ?2), phash = path_hash(REPLACE(path, ?1, ?2)), pathlen = path_length(REPLACE(path, ?1, ?2))"
" WHERE " IS_PREFIX_PATH_OF("?1", "path")),
_db);
if (!query) {
return false;
}
query->bindValue(1, oldParentPath);
query->bindValue(2, newParentPath);
return query->exec();
}

void SyncJournalDb::keyValueStoreSet(const QString &key, QVariant value)
{
QMutexLocker locker(&_mutex);
Expand Down
1 change: 1 addition & 0 deletions src/common/syncjournaldb.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject
[[nodiscard]] bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
[[nodiscard]] bool listFilesInPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
[[nodiscard]] Result<void, QString> setFileRecord(const SyncJournalFileRecord &record);
[[nodiscard]] bool updateParentForAllChildren(const QByteArray &oldParentPath, const QByteArray &newParentPath);

void keyValueStoreSet(const QString &key, QVariant value);
[[nodiscard]] qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);
Expand Down
36 changes: 30 additions & 6 deletions src/libsync/owncloudpropagator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,33 @@ void OwncloudPropagator::adjustDeletedFoldersWithNewChildren(SyncFileItemVector
}
}

void OwncloudPropagator::cleanupLocallyMovedFoldersFromNestedItems(SyncFileItemVector &items)
{
QString previousFolderPath;
const auto eraseBeginIt = std::remove_if(std::begin(items), std::end(items), [&previousFolderPath](const SyncFileItemPtr &item) {
// we assume items is always sorted such that parent folder go before children
if (item->_instruction != CSYNC_INSTRUCTION_RENAME || item->_direction != SyncFileItem::Up) {
previousFolderPath.clear();
return false;
}

if (item->isDirectory() && (previousFolderPath.isEmpty() || !item->_originalFile.startsWith(previousFolderPath))) {
// if it is a directory and there was no previous directory set, do it now, same for a directory which is not a child of previous directory, assume
// starting a new hierarchy
previousFolderPath = item->_originalFile;
return false;
}

if (previousFolderPath.isEmpty()) {
return false;
}

// remove only children of previousFolderPath, not previousFolderPath itself such that parent always remains in the vector
return item->_originalFile.startsWith(previousFolderPath);
});
items.erase(eraseBeginIt, std::end(items));
}

qint64 OwncloudPropagator::smallFileSize()
{
const qint64 smallFileSize = 100 * 1024; //default to 1 MB. Not dynamic right now.
Expand Down Expand Up @@ -518,15 +545,12 @@ void OwncloudPropagator::start(SyncFileItemVector &&items)
items.end());
}

QStringList files;

for (const auto &item : items) {
files.push_back(item->_file);
}

// process each item that is new and is a directory and make sure every parent in its tree has the instruction NEW instead of REMOVE
adjustDeletedFoldersWithNewChildren(items);

// when a folder is moved on the local device we only need to perform one MOVE on the server and then just update database, so we only keep unique moves (topmost moved folder items)
cleanupLocallyMovedFoldersFromNestedItems(items);

resetDelayedUploadTasks();
_rootJob.reset(new PropagateRootDirectory(this));
QStack<QPair<QString /* directory name */, PropagateDirectory * /* job */>> directories;
Expand Down
1 change: 1 addition & 0 deletions src/libsync/owncloudpropagator.h
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ private slots:
void resetDelayedUploadTasks();

static void adjustDeletedFoldersWithNewChildren(SyncFileItemVector &items);
static void cleanupLocallyMovedFoldersFromNestedItems(SyncFileItemVector &items);

AccountPtr _account;
QScopedPointer<PropagateRootDirectory> _rootJob;
Expand Down
31 changes: 31 additions & 0 deletions src/libsync/propagateremotemove.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,35 @@ void PropagateRemoteMove::slotMoveJobFinished()
return;
}

if (_item->isDirectory()) {
if (!propagator()->_journal->updateParentForAllChildren(_item->_originalFile.toUtf8(), _item->_renameTarget.toUtf8())) {
done(SyncFileItem::FatalError, tr("Failed to move folder: %1").arg(_item->_file), ErrorCategory::GenericError);
return;
}

if (!propagator()->_journal->getFilesBelowPath(_item->_renameTarget.toUtf8(), [&](const SyncJournalFileRecord &rec) {
// not sure if this is needed, inode seems to never change for move/rename
auto newItem = SyncFileItem::fromSyncJournalFileRecord(rec);
newItem->_originalFile = QString(newItem->_file).replace(_item->_renameTarget, _item->_originalFile);
newItem->_renameTarget = newItem->_file;
const auto fsPath = propagator()->fullLocalPath(newItem->_renameTarget);
quint64 inode = rec._inode;
if (!FileSystem::getInode(fsPath, &inode)) {
qCWarning(lcPropagateRemoteMove) << "Could not get inode for moved file" << fsPath;
return;
}
if (inode != rec._inode) {
auto newRec = rec;
newRec._inode = inode;
if (!propagator()->_journal->setFileRecord(newRec)) {
qCWarning(lcPropagateRemoteMove) << "Could not update inode for moved file" << newRec.path();
return;
}
}
})) {
qCWarning(lcPropagateRemoteMove) << "Could not update inode for moved files in" << _item->_renameTarget;
}
}
finalize();
}

Expand All @@ -249,6 +278,7 @@ void PropagateRemoteMove::finalize()
return;
}
auto &vfs = propagator()->syncOptions()._vfs;
// TODO: vfs->pinState(_item->_originalFile); does not make sense as item is already gone from original location, do we need this?
auto pinState = vfs->pinState(_item->_originalFile);

const auto targetFile = propagator()->fullLocalPath(_item->_renameTarget);
Expand All @@ -260,6 +290,7 @@ void PropagateRemoteMove::finalize()
done(SyncFileItem::NormalError, tr("Could not delete file record %1 from local DB").arg(_item->_originalFile), ErrorCategory::GenericError);
return;
}
// TODO: vfs->setPinState(_item->_originalFile, PinState::Inherited) will always fail as item is already gone from original location, do we need this?
if (!vfs->setPinState(_item->_originalFile, PinState::Inherited)) {
qCWarning(lcPropagateRemoteMove) << "Could not set pin state of" << _item->_originalFile << "to inherited";
}
Expand Down

0 comments on commit badbf13

Please sign in to comment.