diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index b824df53ab..a1fedb873d 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -9,6 +9,7 @@ #include "accountfwd.h" #include "capabilities.h" #include "clientsideencryptionjobs.h" +#include "common/utility.h" #include "configfile.h" #include "cookiejar.h" #include "creds/abstractcredentials.h" @@ -1174,61 +1175,64 @@ void Account::setAskUserForMnemonic(const bool ask) emit askUserForMnemonicChanged(); } -void Account::listRemoteFolder(QPromise *promise, const QString &path, SyncJournalDb *journalForFolder) +void Account::listRemoteFolder(QPromise *promise, const QString &remoteSyncRootPath, const QString &subPath, SyncJournalDb *journalForFolder) { - qCInfo(lcAccount()) << "ls col job requested for" << path; + qCInfo(lcAccount()) << "ls col job requested for" << subPath; if (!credentials()->ready()) { - qCWarning(lcAccount()) << "credentials are not ready" << path; + qCWarning(lcAccount()) << "credentials are not ready" << subPath; promise->finish(); return; } - auto listFolderJob = new OCC::LsColJob{sharedFromThis(), path}; + auto listFolderJob = new OCC::LsColJob{sharedFromThis(), subPath}; const auto props = LsColJob::defaultProperties(LsColJob::FolderType::ChildFolder, sharedFromThis()); listFolderJob->setProperties(props); - QObject::connect(listFolderJob, &OCC::LsColJob::networkError, this, [promise, path] (QNetworkReply *reply) { + QObject::connect(listFolderJob, &OCC::LsColJob::networkError, this, [promise, subPath] (QNetworkReply *reply) { if (reply) { - qCWarning(lcAccount()) << "ls col job" << path << "error" << reply->errorString(); + qCWarning(lcAccount()) << "ls col job" << subPath << "error" << reply->errorString(); } - qCWarning(lcAccount()) << "ls col job" << path << "error without a reply"; + qCWarning(lcAccount()) << "ls col job" << subPath << "error without a reply"; promise->finish(); }); - QObject::connect(listFolderJob, &OCC::LsColJob::finishedWithError, this, [promise, path] (QNetworkReply *reply) { + QObject::connect(listFolderJob, &OCC::LsColJob::finishedWithError, this, [promise, subPath] (QNetworkReply *reply) { if (reply) { - qCWarning(lcAccount()) << "ls col job" << path << "error" << reply->errorString(); + qCWarning(lcAccount()) << "ls col job" << subPath << "error" << reply->errorString(); } - qCWarning(lcAccount()) << "ls col job" << path << "error without a reply"; + qCWarning(lcAccount()) << "ls col job" << subPath << "error without a reply"; promise->finish(); }); - QObject::connect(listFolderJob, &OCC::LsColJob::finishedWithoutError, this, [promise, path] () { - qCInfo(lcAccount()) << "ls col job" << path << "finished"; + QObject::connect(listFolderJob, &OCC::LsColJob::finishedWithoutError, this, [promise, subPath] () { + qCInfo(lcAccount()) << "ls col job" << subPath << "finished"; promise->finish(); }); - QObject::connect(listFolderJob, &OCC::LsColJob::directoryListingIterated, this, [promise, path, journalForFolder, this](const QString &name, const QMap &properties) { - // `name` is e.g. "/remote.php/dav/files/admin" or "/remote.php/dav/files/admin/SomeFolder"; whereas `path` is e.g. "" or "SomeFolder/" - // in case these two are equal we are currently iterating the entry for the current directory - const auto serverPath = name.mid(this->davPath().size()); - const auto isRootCollection = serverPath.isEmpty() && path.isEmpty(); - const auto isCurrentCollection = isRootCollection || serverPath == Utility::noTrailingSlashPath(path); - if (isCurrentCollection) { + listFolderJob->setProperty("ignoredFirst", false); + const auto baseRemotePath = Utility::trailingSlashPath(Utility::noLeadingSlashPath(remoteSyncRootPath)); + auto syncRootPath = subPath; + if (subPath.startsWith(baseRemotePath)) { + syncRootPath = syncRootPath.mid(baseRemotePath.size()); + } + + QObject::connect(listFolderJob, &OCC::LsColJob::directoryListingIterated, this, [promise, remoteSyncRootPath, syncRootPath, journalForFolder, this](const QString &completeDavPath, const QMap &properties) { + if (!sender()->property("ignoredFirst").toBool()) { qCDebug(lcAccount()) << "skip first item"; + sender()->setProperty("ignoredFirst", true); return; } - qCInfo(lcAccount()) << "ls col job" << path << "new file" << name << properties.count(); + qCInfo(lcAccount()) << "ls col job" << syncRootPath << "new file" << completeDavPath << properties.count(); - const auto slash = name.lastIndexOf('/'); - const auto itemFileName = name.mid(slash + 1); - const auto absoluteItemPathName = (path.isEmpty() ? itemFileName : path + "/" + itemFileName); + const auto slash = completeDavPath.lastIndexOf('/'); + const auto itemFileName = completeDavPath.mid(slash + 1); + const auto absoluteItemPathName = syncRootPath.isEmpty() ? itemFileName : Utility::noTrailingSlashPath(syncRootPath) + '/' + itemFileName; auto currentItemDbRecord = SyncJournalFileRecord{}; if (journalForFolder->getFileRecord(absoluteItemPathName, ¤tItemDbRecord) && currentItemDbRecord.isValid()) { diff --git a/src/libsync/account.h b/src/libsync/account.h index 36d17a7b60..0d09e723e5 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -425,7 +425,8 @@ public slots: void setAskUserForMnemonic(const bool ask); void listRemoteFolder(QPromise *promise, - const QString &path, + const QString &remoteSyncRootPath, + const QString &subPath, SyncJournalDb *journalForFolder); signals: diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.cpp b/src/libsync/vfs/cfapi/cfapiwrapper.cpp index b7a5b1aab0..2e87850682 100644 --- a/src/libsync/vfs/cfapi/cfapiwrapper.cpp +++ b/src/libsync/vfs/cfapi/cfapiwrapper.cpp @@ -553,7 +553,8 @@ void CALLBACK cfApiFetchPlaceHolders(const CF_CALLBACK_INFO *callbackInfo, const sendTransferError(); return; } - const auto serverPath = QString{vfs->params().remotePath + pathString.mid(rootPath.length() + 1)}.mid(1); + const auto remoteSyncRootPath = vfs->params().remotePath; // with leading slash + const auto serverPath = QString{remoteSyncRootPath + pathString.mid(rootPath.length() + 1)}.mid(1); qCDebug(lcCfApiWrapper) << "fetch placeholder:" << path << serverPath << requestId; @@ -584,7 +585,7 @@ void CALLBACK cfApiFetchPlaceHolders(const CF_CALLBACK_INFO *callbackInfo, const qCInfo(lcCfApiWrapper()) << "ls prop started"; }); - QMetaObject::invokeMethod(vfs->params().account.data(), &OCC::Account::listRemoteFolder, &lsPropPromise, serverPath, vfs->params().journal); + QMetaObject::invokeMethod(vfs->params().account.data(), &OCC::Account::listRemoteFolder, &lsPropPromise, remoteSyncRootPath, serverPath, vfs->params().journal); qCInfo(lcCfApiWrapper()) << "ls prop requested" << path << serverPath; diff --git a/test/testaccount.cpp b/test/testaccount.cpp index 698e2296d1..e76b158775 100644 --- a/test/testaccount.cpp +++ b/test/testaccount.cpp @@ -12,13 +12,10 @@ #include #include -#include "common/utility.h" -#include "folderman.h" #include "account.h" #include "accountstate.h" -#include "configfile.h" -#include "testhelper.h" #include "logger.h" +#include "syncenginetestutils.h" using namespace OCC; @@ -100,7 +97,64 @@ private slots: setLimitSettings(LimitSetting::LegacyGlobalLimit); verifyLimitSettings(LimitSetting::NoLimit); } + + void testAccount_listRemoteFolder_data() + { + QTest::addColumn("remotePath"); + QTest::addColumn("subPath"); + QTest::addColumn("expectedPaths"); + + QTest::newRow("root = /; requesting ''") << "" << "" << QStringList{ "A", "B", "C", "S" }; + QTest::newRow("root = /; requesting A/") << "" << "A/" << QStringList{ "A/a1", "A/a2", "A/sub1" }; + QTest::newRow("root = /; requesting A/sub1/") << "" << "A/sub1/" << QStringList{ "A/sub1/sub2" }; + QTest::newRow("root = /; requesting A/sub1/sub2") << "" << "A/sub1/sub2/" << QStringList{}; + QTest::newRow("root = /A; requesting A/") << "/A" << "A/" << QStringList{ "a1", "a2", "sub1" }; + QTest::newRow("root = /A; requesting A/sub1") << "/A" << "A/sub1/" << QStringList{ "sub1/sub2" }; + QTest::newRow("root = /A; requesting A/sub1/sub2") << "/A" << "A/sub1/sub2/" << QStringList{}; + } + + void testAccount_listRemoteFolder() + { + QFETCH(QString, remotePath); + QFETCH(QString, subPath); + QFETCH(QStringList, expectedPaths); + + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.remoteModifier().mkdir("A/sub1"); + fakeFolder.remoteModifier().mkdir("A/sub1/sub2"); + const auto account = fakeFolder.account(); + + QEventLoop localEventLoop; + auto lsPropPromise = QPromise{}; + auto lsPropFuture = lsPropPromise.future(); + auto lsPropFutureWatcher = QFutureWatcher{}; + lsPropFutureWatcher.setFuture(lsPropFuture); + + QList receivedPaths; + + QObject::connect(&lsPropFutureWatcher, &QFutureWatcher::finished, + &localEventLoop, [&localEventLoop] () { + qInfo() << "ls prop finished"; + localEventLoop.quit(); + }); + + QObject::connect(&lsPropFutureWatcher, &QFutureWatcher::resultReadyAt, + &localEventLoop, [&receivedPaths, &lsPropFutureWatcher] (int resultIndex) { + qInfo() << "ls prop result at index" << resultIndex; + const auto &newResultEntries = lsPropFutureWatcher.resultAt(resultIndex); + receivedPaths.append(newResultEntries.fullPath); + }); + + fakeFolder.syncJournal().clearFileTable(); // pretend we only need to fetch placeholders + + account->listRemoteFolder(&lsPropPromise, remotePath, subPath, &fakeFolder.syncJournal()); + localEventLoop.exec(); + + receivedPaths.sort(); + expectedPaths.sort(); + QCOMPARE_EQ(receivedPaths, expectedPaths); + } }; -QTEST_APPLESS_MAIN(TestAccount) +QTEST_GUILESS_MAIN(TestAccount) #include "testaccount.moc"