Merge pull request #5175 from nextcloud/feature/edit-file-locally-restart-sync

Feature/edit file locally restart sync
This commit is contained in:
allexzander 2022-12-06 10:02:29 +01:00 committed by GitHub
commit f6ac52bcd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 603 additions and 119 deletions

View file

@ -118,32 +118,14 @@ void EditLocallyJob::remoteTokenCheckResultReceived(const int statusCode)
return; return;
} }
proceedWithSetup(); findAfolderAndConstructPaths();
} }
void EditLocallyJob::proceedWithSetup() void EditLocallyJob::proceedWithSetup()
{ {
if (!_tokenVerified) { if (!_tokenVerified) {
qCWarning(lcEditLocallyJob) << "Could not proceed with setup as token is not verified."; qCWarning(lcEditLocallyJob) << "Could not proceed with setup as token is not verified.";
return; showError(tr("Could not validate the request to open a file from server."), tr("Please try again."));
}
const auto foundFiles = FolderMan::instance()->findFileInLocalFolders(_relPath, _accountState->account());
if (foundFiles.isEmpty()) {
if (isRelPathExcluded(_relPath)) {
showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), _relPath);
} else {
showError(tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), _relPath);
}
return;
}
_localFilePath = foundFiles.first();
_folderForFile = FolderMan::instance()->folderForPath(_localFilePath);
if (!_folderForFile) {
showError(tr("Could not find a folder to sync."), _relPath);
return; return;
} }
@ -155,15 +137,180 @@ void EditLocallyJob::proceedWithSetup()
_fileName = relPathSplit.last(); _fileName = relPathSplit.last();
_folderForFile = findFolderForFile(_relPath, _userId);
if (!_folderForFile) {
showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), _relPath);
return;
}
if (_relPathParent != QStringLiteral("/") && (!_fileParentItem || _fileParentItem->isEmpty())) {
showError(tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), _relPath);
return;
}
_localFilePath = _folderForFile->path() + _relativePathToRemoteRoot;
Systray::instance()->destroyEditFileLocallyLoadingDialog(); Systray::instance()->destroyEditFileLocallyLoadingDialog();
Q_EMIT setupFinished(); Q_EMIT setupFinished();
} }
void EditLocallyJob::findAfolderAndConstructPaths()
{
_folderForFile = findFolderForFile(_relPath, _userId);
if (!_folderForFile) {
showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), _relPath);
return;
}
_relativePathToRemoteRoot = getRelativePathToRemoteRootForFile();
if (_relativePathToRemoteRoot.isEmpty()) {
qCWarning(lcEditLocallyJob) << "_relativePathToRemoteRoot is empty for" << _relPath;
showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), _relPath);
return;
}
_relPathParent = getRelativePathParent();
if (_relPathParent.isEmpty()) {
showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), _relPath);
return;
}
if (_relPathParent == QStringLiteral("/")) {
proceedWithSetup();
return;
}
fetchRemoteFileParentInfo();
}
QString EditLocallyJob::prefixSlashToPath(const QString &path) QString EditLocallyJob::prefixSlashToPath(const QString &path)
{ {
return path.startsWith('/') ? path : QChar::fromLatin1('/') + path; return path.startsWith('/') ? path : QChar::fromLatin1('/') + path;
} }
void EditLocallyJob::fetchRemoteFileParentInfo()
{
Q_ASSERT(_relPathParent != QStringLiteral("/"));
if (_relPathParent == QStringLiteral("/")) {
qCWarning(lcEditLocallyJob) << "LsColJob must only be used for nested folders.";
return;
}
const auto job = new LsColJob(_accountState->account(), QDir::cleanPath(_folderForFile->remotePathTrailingSlash() + _relPathParent), this);
const QList<QByteArray> props{QByteArrayLiteral("resourcetype"),
QByteArrayLiteral("getlastmodified"),
QByteArrayLiteral("getetag"),
QByteArrayLiteral("http://owncloud.org/ns:size"),
QByteArrayLiteral("http://owncloud.org/ns:id"),
QByteArrayLiteral("http://owncloud.org/ns:permissions"),
QByteArrayLiteral("http://owncloud.org/ns:checksums")};
job->setProperties(props);
connect(job, &LsColJob::directoryListingIterated, this, &EditLocallyJob::slotDirectoryListingIterated);
connect(job, &LsColJob::finishedWithoutError, this, &EditLocallyJob::proceedWithSetup);
connect(job, &LsColJob::finishedWithError, this, &EditLocallyJob::slotLsColJobFinishedWithError);
job->start();
}
bool EditLocallyJob::checkIfFileParentSyncIsNeeded()
{
if (_relPathParent == QLatin1String("/")) {
return true;
}
Q_ASSERT(_fileParentItem && !_fileParentItem->isEmpty());
if (!_fileParentItem || _fileParentItem->isEmpty()) {
return true;
}
SyncJournalFileRecord rec;
if (!_folderForFile->journalDb()->getFileRecord(_fileParentItem->_file, &rec) || !rec.isValid()) {
// we don't have this folder locally, so let's sync it
_fileParentItem->_direction = SyncFileItem::Down;
_fileParentItem->_instruction = CSYNC_INSTRUCTION_NEW;
} else if (rec._etag != _fileParentItem->_etag && rec._modtime != _fileParentItem->_modtime) {
// we just need to update metadata as the folder is already present locally
_fileParentItem->_direction = rec._modtime < _fileParentItem->_modtime ? SyncFileItem::Down : SyncFileItem::Up;
_fileParentItem->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
} else {
_fileParentItem->_direction = SyncFileItem::Down;
_fileParentItem->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
SyncJournalFileRecord recFile;
if (_folderForFile->journalDb()->getFileRecord(_relativePathToRemoteRoot, &recFile) && recFile.isValid()) {
return false;
}
}
return true;
}
void EditLocallyJob::startSyncBeforeOpening()
{
eraseBlacklistRecordForItem();
if (!checkIfFileParentSyncIsNeeded()) {
openFile();
return;
}
// connect to a SyncEngine::itemDiscovered so we can complete the job as soon as the file in question is discovered
QObject::connect(&_folderForFile->syncEngine(), &SyncEngine::itemDiscovered, this, &EditLocallyJob::slotItemDiscovered);
_folderForFile->syncEngine().setSingleItemDiscoveryOptions({_relPathParent == QStringLiteral("/") ? QString{} : _relPathParent, _relativePathToRemoteRoot, _fileParentItem});
FolderMan::instance()->forceSyncForFolder(_folderForFile);
}
void EditLocallyJob::eraseBlacklistRecordForItem()
{
if (!_folderForFile || !_fileParentItem) {
qCWarning(lcEditLocallyJob) << "_folderForFile or _fileParentItem is invalid!";
return;
}
Q_ASSERT(!_folderForFile->isSyncRunning());
if (_folderForFile->isSyncRunning()) {
qCWarning(lcEditLocallyJob) << "_folderForFile is syncing";
return;
}
if (_folderForFile->journalDb()->errorBlacklistEntry(_fileParentItem->_file).isValid()) {
_folderForFile->journalDb()->wipeErrorBlacklistEntry(_fileParentItem->_file);
}
}
const QString EditLocallyJob::getRelativePathToRemoteRootForFile() const
{
Q_ASSERT(_folderForFile);
if (!_folderForFile) {
return {};
}
if (_folderForFile->remotePathTrailingSlash().size() == 1) {
return _relPath;
} else {
const auto remoteFolderPathWithTrailingSlash = _folderForFile->remotePathTrailingSlash();
const auto remoteFolderPathWithoutLeadingSlash =
remoteFolderPathWithTrailingSlash.startsWith(QLatin1Char('/')) ? remoteFolderPathWithTrailingSlash.mid(1) : remoteFolderPathWithTrailingSlash;
return _relPath.startsWith(remoteFolderPathWithoutLeadingSlash) ? _relPath.mid(remoteFolderPathWithoutLeadingSlash.size()) : _relPath;
}
}
const QString EditLocallyJob::getRelativePathParent() const
{
Q_ASSERT(!_relativePathToRemoteRoot.isEmpty());
if (_relativePathToRemoteRoot.isEmpty()) {
return {};
}
auto relativePathToRemoteRootSplit = _relativePathToRemoteRoot.split(QLatin1Char('/'));
if (relativePathToRemoteRootSplit.size() > 1) {
relativePathToRemoteRootSplit.removeLast();
return relativePathToRemoteRootSplit.join(QLatin1Char('/'));
}
return QStringLiteral("/");
}
bool EditLocallyJob::isTokenValid(const QString &token) bool EditLocallyJob::isTokenValid(const QString &token)
{ {
if (token.isEmpty()) { if (token.isEmpty()) {
@ -201,24 +348,49 @@ bool EditLocallyJob::isRelPathValid(const QString &relPath)
return true; return true;
} }
bool EditLocallyJob::isRelPathExcluded(const QString &relPath) OCC::Folder *EditLocallyJob::findFolderForFile(const QString &relPath, const QString &userId)
{ {
if (relPath.isEmpty()) { if (relPath.isEmpty()) {
return false; return nullptr;
} }
const auto folderMap = FolderMan::instance()->map(); const auto folderMap = FolderMan::instance()->map();
for (const auto &folder : folderMap) {
bool result = false; const auto relPathSplit = relPath.split(QLatin1Char('/'));
const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result);
for (const auto &excludedPath : excludedThroughSelectiveSync) { // a file is on the first level of remote root, so, we just need a proper folder that points to a remote root
if (relPath.startsWith(excludedPath)) { if (relPathSplit.size() == 1) {
return true; const auto foundIt = std::find_if(std::begin(folderMap), std::end(folderMap), [&userId](const OCC::Folder *folder) {
} return folder->remotePath() == QStringLiteral("/") && folder->accountState()->account()->userIdAtHostWithPort() == userId;
} });
return foundIt != std::end(folderMap) ? foundIt.value() : nullptr;
} }
return false; const auto relPathWithSlash = relPath.startsWith(QStringLiteral("/")) ? relPath : QStringLiteral("/") + relPath;
for (const auto &folder : folderMap) {
// make sure we properly handle folders with non-root(nested) remote paths
if ((folder->remotePath() != QStringLiteral("/") && !relPathWithSlash.startsWith(folder->remotePath()))
|| folder->accountState()->account()->userIdAtHostWithPort() != userId) {
continue;
}
auto result = false;
const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result);
auto isExcluded = false;
for (const auto &excludedPath : excludedThroughSelectiveSync) {
if (relPath.startsWith(excludedPath)) {
isExcluded = true;
break;
}
}
if (isExcluded) {
continue;
}
return folder;
}
return nullptr;
} }
void EditLocallyJob::showError(const QString &message, const QString &informativeText) void EditLocallyJob::showError(const QString &message, const QString &informativeText)
@ -271,32 +443,95 @@ void EditLocallyJob::startEditLocally()
Systray::instance()->createEditFileLocallyLoadingDialog(_fileName); Systray::instance()->createEditFileLocallyLoadingDialog(_fileName);
_folderForFile->startSync(); if (_folderForFile->isSyncRunning()) {
const auto syncFinishedConnection = connect(_folderForFile, &Folder::syncFinished, // in case sync is already running - terminate it and start a new one
this, &EditLocallyJob::folderSyncFinished); _syncTerminatedConnection = connect(_folderForFile, &Folder::syncFinished, this, [this]() {
disconnect(_syncTerminatedConnection);
_syncTerminatedConnection = {};
startSyncBeforeOpening();
});
_folderForFile->slotTerminateSync();
EditLocallyManager::instance()->folderSyncFinishedConnections.insert(_localFilePath, return;
syncFinishedConnection); }
startSyncBeforeOpening();
} }
void EditLocallyJob::folderSyncFinished(const OCC::SyncResult &result) void EditLocallyJob::slotItemCompleted(const OCC::SyncFileItemPtr &item)
{ {
Q_UNUSED(result) Q_ASSERT(item && !item->isEmpty());
disconnectSyncFinished(); if (!item || item->isEmpty()) {
openFile(); qCWarning(lcEditLocallyJob) << "invalid item";
}
if (item->_file == _relativePathToRemoteRoot) {
disconnect(&_folderForFile->syncEngine(), &SyncEngine::itemCompleted, this, &EditLocallyJob::slotItemCompleted);
disconnect(&_folderForFile->syncEngine(), &SyncEngine::itemDiscovered, this, &EditLocallyJob::slotItemDiscovered);
openFile();
}
} }
void EditLocallyJob::disconnectSyncFinished() const void EditLocallyJob::slotLsColJobFinishedWithError(QNetworkReply *reply)
{ {
if(_localFilePath.isEmpty()) { const auto contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
const auto invalidContentType = !contentType.contains(QStringLiteral("application/xml; charset=utf-8"))
&& !contentType.contains(QStringLiteral("application/xml; charset=\"utf-8\"")) && !contentType.contains(QStringLiteral("text/xml; charset=utf-8"))
&& !contentType.contains(QStringLiteral("text/xml; charset=\"utf-8\""));
const auto httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qCWarning(lcEditLocallyJob) << "LSCOL job error" << reply->errorString() << httpCode << reply->error();
const auto message = reply->error() == QNetworkReply::NoError && invalidContentType
? tr("Server error: PROPFIND reply is not XML formatted!") : reply->errorString();
qCWarning(lcEditLocallyJob) << "Could not proceed with setup as file PROPFIND job has failed." << httpCode << message;
showError(tr("Could not find a remote file info for local editing. Make sure its path is valid."), _relPath);
}
void EditLocallyJob::slotDirectoryListingIterated(const QString &name, const QMap<QString, QString> &properties)
{
Q_ASSERT(_relPathParent != QStringLiteral("/"));
if (_relPathParent == QStringLiteral("/")) {
qCWarning(lcEditLocallyJob) << "LsColJob must only be used for nested folders.";
return; return;
} }
const auto manager = EditLocallyManager::instance(); const auto job = qobject_cast<LsColJob*>(sender());
Q_ASSERT(job);
if (!job) {
qCWarning(lcEditLocallyJob) << "Must call slotDirectoryListingIterated from a signal.";
return;
}
if (const auto existingConnection = manager->folderSyncFinishedConnections.value(_localFilePath)) { if (name.endsWith(_relPathParent)) {
disconnect(existingConnection); // let's remove remote dav path and remote root from the beginning of the name
manager->folderSyncFinishedConnections.remove(_localFilePath); const auto nameWithoutDavPath = name.mid(_accountState->account()->davPath().size());
const auto remoteFolderPathWithTrailingSlash = _folderForFile->remotePathTrailingSlash();
const auto remoteFolderPathWithoutLeadingSlash = remoteFolderPathWithTrailingSlash.startsWith(QLatin1Char('/'))
? remoteFolderPathWithTrailingSlash.mid(1) : remoteFolderPathWithTrailingSlash;
const auto cleanName = nameWithoutDavPath.startsWith(remoteFolderPathWithoutLeadingSlash)
? nameWithoutDavPath.mid(remoteFolderPathWithoutLeadingSlash.size()) : nameWithoutDavPath;
disconnect(job, &LsColJob::directoryListingIterated, this, &EditLocallyJob::slotDirectoryListingIterated);
_fileParentItem = SyncFileItem::fromProperties(cleanName, properties);
}
}
void EditLocallyJob::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
{
Q_ASSERT(item && !item->isEmpty());
if (!item || item->isEmpty()) {
qCWarning(lcEditLocallyJob) << "invalid item";
}
if (item->_file == _relativePathToRemoteRoot) {
disconnect(&_folderForFile->syncEngine(), &SyncEngine::itemDiscovered, this, &EditLocallyJob::slotItemDiscovered);
if (item->_instruction == CSYNC_INSTRUCTION_NONE) {
// return early if the file is already in sync
slotItemCompleted(item);
return;
}
// or connect to the SyncEngine::itemCompleted and wait till the file gets sycned
QObject::connect(&_folderForFile->syncEngine(), &SyncEngine::itemCompleted, this, &EditLocallyJob::slotItemCompleted);
} }
} }

View file

@ -17,6 +17,7 @@
#include <QObject> #include <QObject>
#include "accountstate.h" #include "accountstate.h"
#include "syncfileitem.h"
namespace OCC { namespace OCC {
@ -38,7 +39,7 @@ public:
[[nodiscard]] static bool isTokenValid(const QString &token); [[nodiscard]] static bool isTokenValid(const QString &token);
[[nodiscard]] static bool isRelPathValid(const QString &relPath); [[nodiscard]] static bool isRelPathValid(const QString &relPath);
[[nodiscard]] static bool isRelPathExcluded(const QString &relPath); [[nodiscard]] static OCC::Folder *findFolderForFile(const QString &relPath, const QString &userId);
[[nodiscard]] static QString prefixSlashToPath(const QString &path); [[nodiscard]] static QString prefixSlashToPath(const QString &path);
signals: signals:
@ -51,31 +52,47 @@ public slots:
void startEditLocally(); void startEditLocally();
private slots: private slots:
void fetchRemoteFileParentInfo();
void startSyncBeforeOpening();
void eraseBlacklistRecordForItem();
void startTokenRemoteCheck(); void startTokenRemoteCheck();
void proceedWithSetup(); void proceedWithSetup();
void findAfolderAndConstructPaths();
void showError(const QString &message, const QString &informativeText); void showError(const QString &message, const QString &informativeText);
void showErrorNotification(const QString &message, const QString &informativeText) const; void showErrorNotification(const QString &message, const QString &informativeText) const;
void showErrorMessageBox(const QString &message, const QString &informativeText) const; void showErrorMessageBox(const QString &message, const QString &informativeText) const;
void remoteTokenCheckResultReceived(const int statusCode); void remoteTokenCheckResultReceived(const int statusCode);
void folderSyncFinished(const OCC::SyncResult &result); void slotItemDiscovered(const OCC::SyncFileItemPtr &item);
void slotItemCompleted(const OCC::SyncFileItemPtr &item);
void slotLsColJobFinishedWithError(QNetworkReply *reply);
void slotDirectoryListingIterated(const QString &name, const QMap<QString, QString> &properties);
void disconnectSyncFinished() const;
void openFile(); void openFile();
private: private:
[[nodiscard]] bool checkIfFileParentSyncIsNeeded(); // returns true if sync will be needed, false otherwise
[[nodiscard]] const QString getRelativePathToRemoteRootForFile() const; // returns either '/' or a (relative path - Folder::remotePath()) for folders pointing to a non-root remote path e.g. '/subfolder' instead of '/'
[[nodiscard]] const QString getRelativePathParent() const;
bool _tokenVerified = false; bool _tokenVerified = false;
AccountStatePtr _accountState; AccountStatePtr _accountState;
QString _userId; QString _userId;
QString _relPath; QString _relPath; // full remote path for a file (as on the server)
QString _relativePathToRemoteRoot; // (relative path - Folder::remotePath()) for folders pointing to a non-root remote path e.g. '/subfolder' instead of '/'
QString _relPathParent; // a folder where the file resides ('/' if it is in the first level of a remote root, or e.g. a '/subfolder/a/b/c if it resides in a nested folder)
QString _token; QString _token;
SyncFileItemPtr _fileParentItem;
QString _fileName; QString _fileName;
QString _localFilePath; QString _localFilePath;
Folder *_folderForFile = nullptr; Folder *_folderForFile = nullptr;
std::unique_ptr<SimpleApiJob> _checkTokenJob; std::unique_ptr<SimpleApiJob> _checkTokenJob;
QMetaObject::Connection _syncTerminatedConnection = {};
}; };
} }

View file

@ -71,6 +71,9 @@ void EditLocallyManager::createJob(const QString &userId,
const QString &relPath, const QString &relPath,
const QString &token) const QString &token)
{ {
if (_jobs.contains(token)) {
return;
}
const EditLocallyJobPtr job(new EditLocallyJob(userId, relPath, token)); const EditLocallyJobPtr job(new EditLocallyJob(userId, relPath, token));
// We need to make sure the job sticks around until it is finished // We need to make sure the job sticks around until it is finished
_jobs.insert(token, job); _jobs.insert(token, job);

View file

@ -28,8 +28,6 @@ class EditLocallyManager : public QObject
public: public:
[[nodiscard]] static EditLocallyManager *instance(); [[nodiscard]] static EditLocallyManager *instance();
QHash<QString, QMetaObject::Connection> folderSyncFinishedConnections;
public slots: public slots:
void editLocally(const QUrl &url); void editLocally(const QUrl &url);

View file

@ -831,8 +831,11 @@ bool Folder::reloadExcludes()
void Folder::startSync(const QStringList &pathList) void Folder::startSync(const QStringList &pathList)
{ {
Q_UNUSED(pathList) const auto singleItemDiscoveryOptions = _engine->singleItemDiscoveryOptions();
Q_ASSERT(!singleItemDiscoveryOptions.discoveryDirItem || singleItemDiscoveryOptions.discoveryDirItem->isDirectory());
if (singleItemDiscoveryOptions.discoveryDirItem && !singleItemDiscoveryOptions.discoveryDirItem->isDirectory()) {
qCCritical(lcFolder) << "startSync only accepts directory SyncFileItem, not a file.";
}
if (isBusy()) { if (isBusy()) {
qCCritical(lcFolder) << "ERROR csync is still running and new sync requested."; qCCritical(lcFolder) << "ERROR csync is still running and new sync requested.";
return; return;
@ -868,7 +871,13 @@ void Folder::startSync(const QStringList &pathList)
bool periodicFullLocalDiscoveryNow = bool periodicFullLocalDiscoveryNow =
fullLocalDiscoveryInterval.count() >= 0 // negative means we don't require periodic full runs fullLocalDiscoveryInterval.count() >= 0 // negative means we don't require periodic full runs
&& _timeSinceLastFullLocalDiscovery.hasExpired(fullLocalDiscoveryInterval.count()); && _timeSinceLastFullLocalDiscovery.hasExpired(fullLocalDiscoveryInterval.count());
if (_folderWatcher && _folderWatcher->isReliable()
if (!singleItemDiscoveryOptions.filePathRelative.isEmpty()
&& singleItemDiscoveryOptions.discoveryDirItem && !singleItemDiscoveryOptions.discoveryDirItem->isEmpty()) {
qCInfo(lcFolder) << "Going to sync just one file";
_engine->setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, {singleItemDiscoveryOptions.discoveryPath});
_localDiscoveryTracker->startSyncPartialDiscovery();
} else if (_folderWatcher && _folderWatcher->isReliable()
&& hasDoneFullLocalDiscovery && hasDoneFullLocalDiscovery
&& !periodicFullLocalDiscoveryNow) { && !periodicFullLocalDiscoveryNow) {
qCInfo(lcFolder) << "Allowing local discovery to read from the database"; qCInfo(lcFolder) << "Allowing local discovery to read from the database";

View file

@ -34,6 +34,7 @@ set(libsync_SRCS
encryptfolderjob.cpp encryptfolderjob.cpp
filesystem.h filesystem.h
filesystem.cpp filesystem.cpp
helpers.cpp
httplogger.h httplogger.h
httplogger.cpp httplogger.cpp
logger.h logger.h

View file

@ -64,7 +64,6 @@ constexpr int checksumRecalculateRequestServerVersionMinSupportedMajor = 24;
} }
namespace OCC { namespace OCC {
Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg) Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg)
const char app_password[] = "_app-password"; const char app_password[] = "_app-password";
@ -162,6 +161,25 @@ QString Account::displayName() const
return dn; return dn;
} }
QString Account::userIdAtHostWithPort() const
{
const auto credentialsUserSplit = credentials() ? credentials()->user().split(QLatin1Char('@')) : QStringList{};
if (credentialsUserSplit.isEmpty()) {
return {};
}
const auto userName = credentialsUserSplit.first();
QString dn = QStringLiteral("%1@%2").arg(userName, _url.host());
const auto port = url().port();
if (port > 0 && port != 80 && port != 443) {
dn.append(QLatin1Char(':'));
dn.append(QString::number(port));
}
return dn;
}
QString Account::davDisplayName() const QString Account::davDisplayName() const
{ {
return _displayName; return _displayName;

View file

@ -115,6 +115,9 @@ public:
/// The name of the account as shown in the toolbar /// The name of the account as shown in the toolbar
[[nodiscard]] QString displayName() const; [[nodiscard]] QString displayName() const;
/// User id in a form 'user@example.de, optionally port is added (if it is not 80 or 443)
[[nodiscard]] QString userIdAtHostWithPort() const;
/// The name of the account that is displayed as nicely as possible, /// The name of the account that is displayed as nicely as possible,
/// e.g. the actual name of the user (John Doe). If this cannot be /// e.g. the actual name of the user (John Doe). If this cannot be
/// provided, defaults to davUser (e.g. johndoe) /// provided, defaults to davUser (e.g. johndoe)

View file

@ -59,6 +59,17 @@ ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileIt
computePinState(parent->_pinState); computePinState(parent->_pinState);
} }
ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent)
: QObject(parent)
, _dirItem(dirItem)
, _lastSyncTimestamp(lastSyncTimestamp)
, _queryLocal(queryLocal)
, _discoveryData(data)
, _currentFolder(path)
{
computePinState(basePinState);
}
void ProcessDirectoryJob::start() void ProcessDirectoryJob::start()
{ {
qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal; qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal;
@ -162,6 +173,11 @@ void ProcessDirectoryJob::process()
PathTuple path; PathTuple path;
path = _currentFolder.addName(e.nameOverride.isEmpty() ? f.first : e.nameOverride); path = _currentFolder.addName(e.nameOverride.isEmpty() ? f.first : e.nameOverride);
if (!_discoveryData->_listExclusiveFiles.isEmpty() && !_discoveryData->_listExclusiveFiles.contains(path._server)) {
qCInfo(lcDisco) << "Skipping a file:" << path._server << "as it is not listed in the _listExclusiveFiles";
continue;
}
if (isVfsWithSuffix()) { if (isVfsWithSuffix()) {
// Without suffix vfs the paths would be good. But since the dbEntry and localEntry // Without suffix vfs the paths would be good. But since the dbEntry and localEntry
// can have different names from f.first when suffix vfs is on, make sure the // can have different names from f.first when suffix vfs is on, make sure the
@ -213,6 +229,7 @@ void ProcessDirectoryJob::process()
processFile(std::move(path), e.localEntry, e.serverEntry, e.dbEntry); processFile(std::move(path), e.localEntry, e.serverEntry, e.dbEntry);
} }
_discoveryData->_listExclusiveFiles.clear();
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
} }

View file

@ -49,8 +49,45 @@ class ProcessDirectoryJob : public QObject
{ {
Q_OBJECT Q_OBJECT
struct PathTuple;
public: public:
/** Structure representing a path during discovery. A same path may have different value locally
* or on the server in case of renames.
*
* These strings never start or ends with slashes. They are all relative to the folder's root.
* Usually they are all the same and are even shared instance of the same QString.
*
* _server and _local paths will differ if there are renames, example:
* remote renamed A/ to B/ and local renamed A/X to A/Y then
* target: B/Y/file
* original: A/X/file
* local: A/Y/file
* server: B/X/file
*/
struct PathTuple {
QString _original; // Path as in the DB (before the sync)
QString _target; // Path that will be the result after the sync (and will be in the DB)
QString _server; // Path on the server (before the sync)
QString _local; // Path locally (before the sync)
static QString pathAppend(const QString &base, const QString &name)
{
return base.isEmpty() ? name : base + QLatin1Char('/') + name;
}
[[nodiscard]] PathTuple addName(const QString &name) const
{
PathTuple result;
result._original = pathAppend(_original, name);
auto buildString = [&](const QString &other) {
// Optimize by trying to keep all string implicitly shared if they are the same (common case)
return other == _original ? result._original : pathAppend(other, name);
};
result._target = buildString(_target);
result._server = buildString(_server);
result._local = buildString(_local);
return result;
}
};
enum QueryMode { enum QueryMode {
NormalQuery, NormalQuery,
ParentDontExist, // Do not query this folder because it does not exist ParentDontExist, // Do not query this folder because it does not exist
@ -71,6 +108,9 @@ public:
QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp,
ProcessDirectoryJob *parent); ProcessDirectoryJob *parent);
explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem,
QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent);
void start(); void start();
/** Start up to nbJobs, return the number of job started; emit finished() when done */ /** Start up to nbJobs, return the number of job started; emit finished() when done */
int processSubJobs(int nbJobs); int processSubJobs(int nbJobs);
@ -96,44 +136,6 @@ private:
LocalInfo localEntry; LocalInfo localEntry;
}; };
/** Structure representing a path during discovery. A same path may have different value locally
* or on the server in case of renames.
*
* These strings never start or ends with slashes. They are all relative to the folder's root.
* Usually they are all the same and are even shared instance of the same QString.
*
* _server and _local paths will differ if there are renames, example:
* remote renamed A/ to B/ and local renamed A/X to A/Y then
* target: B/Y/file
* original: A/X/file
* local: A/Y/file
* server: B/X/file
*/
struct PathTuple
{
QString _original; // Path as in the DB (before the sync)
QString _target; // Path that will be the result after the sync (and will be in the DB)
QString _server; // Path on the server (before the sync)
QString _local; // Path locally (before the sync)
static QString pathAppend(const QString &base, const QString &name)
{
return base.isEmpty() ? name : base + QLatin1Char('/') + name;
}
[[nodiscard]] PathTuple addName(const QString &name) const
{
PathTuple result;
result._original = pathAppend(_original, name);
auto buildString = [&](const QString &other) {
// Optimize by trying to keep all string implicitly shared if they are the same (common case)
return other == _original ? result._original : pathAppend(other, name);
};
result._target = buildString(_target);
result._server = buildString(_server);
result._local = buildString(_local);
return result;
}
};
/** Iterate over entries inside the directory (non-recursively). /** Iterate over entries inside the directory (non-recursively).
* *
* Called once _serverEntries and _localEntries are filled * Called once _serverEntries and _localEntries are filled

View file

@ -14,6 +14,7 @@
#include "discoveryphase.h" #include "discoveryphase.h"
#include "discovery.h" #include "discovery.h"
#include "helpers.h"
#include "account.h" #include "account.h"
#include "clientsideencryptionjobs.h" #include "clientsideencryptionjobs.h"

View file

@ -294,6 +294,8 @@ public:
QHash<QString, long long> _filesNeedingScheduledSync; QHash<QString, long long> _filesNeedingScheduledSync;
QVector<QString> _filesUnscheduleSync; QVector<QString> _filesUnscheduleSync;
QStringList _listExclusiveFiles;
signals: signals:
void fatalError(const QString &errorString); void fatalError(const QString &errorString);
void itemDiscovered(const OCC::SyncFileItemPtr &item); void itemDiscovered(const OCC::SyncFileItemPtr &item);

25
src/libsync/helpers.cpp Normal file
View file

@ -0,0 +1,25 @@
#include "helpers.h"
namespace OCC
{
QByteArray parseEtag(const char *header)
{
if (!header) {
return {};
}
QByteArray result = header;
// Weak E-Tags can appear when gzip compression is on, see #3946
if (result.startsWith("W/")) {
result = result.mid(2);
}
// https://github.com/owncloud/client/issues/1195
result.replace("-gzip", "");
if (result.length() >= 2 && result.startsWith('"') && result.endsWith('"')) {
result = result.mid(1, result.length() - 2);
}
return result;
}
} // namespace OCC

25
src/libsync/helpers.h Normal file
View file

@ -0,0 +1,25 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include "owncloudlib.h"
#include <QByteArray>
namespace OCC
{
/** Strips quotes and gzip annotations */
OWNCLOUDSYNC_EXPORT QByteArray parseEtag(const char *header);
} // namespace OCC

View file

@ -38,6 +38,7 @@
#include "networkjobs.h" #include "networkjobs.h"
#include "account.h" #include "account.h"
#include "helpers.h"
#include "owncloudpropagator.h" #include "owncloudpropagator.h"
#include "clientsideencryption.h" #include "clientsideencryption.h"
@ -59,25 +60,6 @@ Q_LOGGING_CATEGORY(lcDetermineAuthTypeJob, "nextcloud.sync.networkjob.determinea
Q_LOGGING_CATEGORY(lcSimpleFileJob, "nextcloud.sync.networkjob.simplefilejob", QtInfoMsg) Q_LOGGING_CATEGORY(lcSimpleFileJob, "nextcloud.sync.networkjob.simplefilejob", QtInfoMsg)
const int notModifiedStatusCode = 304; const int notModifiedStatusCode = 304;
QByteArray parseEtag(const char *header)
{
if (!header)
return QByteArray();
QByteArray arr = header;
// Weak E-Tags can appear when gzip compression is on, see #3946
if (arr.startsWith("W/"))
arr = arr.mid(2);
// https://github.com/owncloud/client/issues/1195
arr.replace("-gzip", "");
if (arr.length() >= 2 && arr.startsWith('"') && arr.endsWith('"')) {
arr = arr.mid(1, arr.length() - 2);
}
return arr;
}
RequestEtagJob::RequestEtagJob(AccountPtr account, const QString &path, QObject *parent) RequestEtagJob::RequestEtagJob(AccountPtr account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent) : AbstractNetworkJob(account, path, parent)
{ {

View file

@ -30,9 +30,6 @@ class QJsonObject;
namespace OCC { namespace OCC {
/** Strips quotes and gzip annotations */
OWNCLOUDSYNC_EXPORT QByteArray parseEtag(const char *header);
struct HttpError struct HttpError
{ {
int code; // HTTP error code int code; // HTTP error code

View file

@ -16,6 +16,7 @@
#pragma once #pragma once
#include "owncloudpropagator.h" #include "owncloudpropagator.h"
#include "helpers.h"
#include "syncfileitem.h" #include "syncfileitem.h"
#include "networkjobs.h" #include "networkjobs.h"
#include "syncengine.h" #include "syncengine.h"

View file

@ -320,6 +320,8 @@ void SyncEngine::conflictRecordMaintenance()
void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
{ {
emit itemDiscovered(item);
if (Utility::isConflictFile(item->_file)) if (Utility::isConflictFile(item->_file))
_seenConflictFiles.insert(item->_file); _seenConflictFiles.insert(item->_file);
if (item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA && !item->isDirectory()) { if (item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA && !item->isDirectory()) {
@ -633,8 +635,54 @@ void SyncEngine::startSync()
connect(_discoveryPhase.data(), &DiscoveryPhase::silentlyExcluded, connect(_discoveryPhase.data(), &DiscoveryPhase::silentlyExcluded,
_syncFileStatusTracker.data(), &SyncFileStatusTracker::slotAddSilentlyExcluded); _syncFileStatusTracker.data(), &SyncFileStatusTracker::slotAddSilentlyExcluded);
auto discoveryJob = new ProcessDirectoryJob( ProcessDirectoryJob *discoveryJob = nullptr;
_discoveryPhase.data(), PinState::AlwaysLocal, _journal->keyValueStoreGetInt("last_sync", 0), _discoveryPhase.data());
if (!singleItemDiscoveryOptions().filePathRelative.isEmpty()) {
_discoveryPhase->_listExclusiveFiles.clear();
_discoveryPhase->_listExclusiveFiles.push_back(singleItemDiscoveryOptions().filePathRelative);
}
if (!singleItemDiscoveryOptions().discoveryPath.isEmpty() && singleItemDiscoveryOptions().discoveryDirItem) {
ProcessDirectoryJob::PathTuple path = {};
path._local = path._original = path._server = path._target = singleItemDiscoveryOptions().discoveryPath;
SyncJournalFileRecord rec;
const auto localQueryMode = _journal->getFileRecord(singleItemDiscoveryOptions().discoveryDirItem->_file, &rec) && rec.isValid()
? ProcessDirectoryJob::NormalQuery
: ProcessDirectoryJob::ParentDontExist;
const auto pinState = [this, &rec]() {
if (!_syncOptions._vfs || _syncOptions._vfs->mode() == Vfs::Off) {
return PinState::AlwaysLocal;
}
if (!rec.isValid()) {
return PinState::OnlineOnly;
}
const auto pinStateInDb = _journal->internalPinStates().rawForPath(singleItemDiscoveryOptions().discoveryDirItem->_file.toUtf8());
if (pinStateInDb) {
return *pinStateInDb;
}
return PinState::Unspecified;
}();
discoveryJob = new ProcessDirectoryJob(
_discoveryPhase.data(),
pinState,
path,
singleItemDiscoveryOptions().discoveryDirItem,
localQueryMode,
_journal->keyValueStoreGetInt("last_sync", 0),
_discoveryPhase.data()
);
} else {
discoveryJob = new ProcessDirectoryJob(
_discoveryPhase.data(),
PinState::AlwaysLocal,
_journal->keyValueStoreGetInt("last_sync", 0),
_discoveryPhase.data()
);
}
_discoveryPhase->startJob(discoveryJob); _discoveryPhase->startJob(discoveryJob);
connect(discoveryJob, &ProcessDirectoryJob::etag, this, &SyncEngine::slotRootEtagReceived); connect(discoveryJob, &ProcessDirectoryJob::etag, this, &SyncEngine::slotRootEtagReceived);
connect(_discoveryPhase.data(), &DiscoveryPhase::addErrorToGui, this, &SyncEngine::addErrorToGui); connect(_discoveryPhase.data(), &DiscoveryPhase::addErrorToGui, this, &SyncEngine::addErrorToGui);
@ -874,6 +922,8 @@ void SyncEngine::slotPropagationFinished(bool success)
void SyncEngine::finalize(bool success) void SyncEngine::finalize(bool success)
{ {
setSingleItemDiscoveryOptions({});
qCInfo(lcEngine) << "Sync run took " << _stopWatch.addLapTime(QLatin1String("Sync Finished")) << "ms"; qCInfo(lcEngine) << "Sync run took " << _stopWatch.addLapTime(QLatin1String("Sync Finished")) << "ms";
_stopWatch.stop(); _stopWatch.stop();
@ -1003,6 +1053,16 @@ void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QS
} }
} }
void SyncEngine::setSingleItemDiscoveryOptions(const SingleItemDiscoveryOptions &singleItemDiscoveryOptions)
{
_singleItemDiscoveryOptions = singleItemDiscoveryOptions;
}
const SyncEngine::SingleItemDiscoveryOptions &SyncEngine::singleItemDiscoveryOptions() const
{
return _singleItemDiscoveryOptions;
}
bool SyncEngine::shouldDiscoverLocally(const QString &path) const bool SyncEngine::shouldDiscoverLocally(const QString &path) const
{ {
if (_localDiscoveryStyle == LocalDiscoveryStyle::FilesystemOnly) if (_localDiscoveryStyle == LocalDiscoveryStyle::FilesystemOnly)

View file

@ -57,6 +57,12 @@ class OWNCLOUDSYNC_EXPORT SyncEngine : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
struct SingleItemDiscoveryOptions {
QString discoveryPath;
QString filePathRelative;
SyncFileItemPtr discoveryDirItem;
};
SyncEngine(AccountPtr account, SyncEngine(AccountPtr account,
const QString &localPath, const QString &localPath,
const SyncOptions &syncOptions, const SyncOptions &syncOptions,
@ -143,6 +149,9 @@ public slots:
*/ */
void setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle style, std::set<QString> paths = {}); void setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle style, std::set<QString> paths = {});
void setSingleItemDiscoveryOptions(const SingleItemDiscoveryOptions &singleItemDiscoveryOptions);
[[nodiscard]] const SyncEngine::SingleItemDiscoveryOptions &singleItemDiscoveryOptions() const;
void addAcceptedInvalidFileName(const QString& filePath); void addAcceptedInvalidFileName(const QString& filePath);
signals: signals:
@ -157,6 +166,8 @@ signals:
void transmissionProgress(const OCC::ProgressInfo &progress); void transmissionProgress(const OCC::ProgressInfo &progress);
void itemDiscovered(const SyncFileItemPtr &);
/// We've produced a new sync error of a type. /// We've produced a new sync error of a type.
void syncError(const QString &message, OCC::ErrorCategory category = OCC::ErrorCategory::Normal); void syncError(const QString &message, OCC::ErrorCategory category = OCC::ErrorCategory::Normal);
@ -375,6 +386,8 @@ private:
// A vector of all the (unique) scheduled sync timers // A vector of all the (unique) scheduled sync timers
QVector<QSharedPointer<ScheduledSyncTimer>> _scheduledSyncTimers; QVector<QSharedPointer<ScheduledSyncTimer>> _scheduledSyncTimers;
SingleItemDiscoveryOptions _singleItemDiscoveryOptions;
}; };
} }

View file

@ -13,8 +13,10 @@
*/ */
#include "syncfileitem.h" #include "syncfileitem.h"
#include "common/checksums.h"
#include "common/syncjournalfilerecord.h" #include "common/syncjournalfilerecord.h"
#include "common/utility.h" #include "common/utility.h"
#include "helpers.h"
#include "filesystem.h" #include "filesystem.h"
#include <QLoggingCategory> #include <QLoggingCategory>
@ -98,4 +100,73 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
return item; return item;
} }
SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap<QString, QString> &properties)
{
SyncFileItemPtr item(new SyncFileItem);
item->_file = filePath;
item->_originalFile = filePath;
const auto isDirectory = properties.value(QStringLiteral("resourcetype")).contains(QStringLiteral("collection"));
item->_type = isDirectory ? ItemTypeDirectory : ItemTypeFile;
item->_size = isDirectory ? 0 : properties.value(QStringLiteral("size")).toInt();
item->_fileId = properties.value(QStringLiteral("id")).toUtf8();
if (properties.contains(QStringLiteral("permissions"))) {
item->_remotePerm = RemotePermissions::fromServerString(properties.value("permissions"));
}
if (!properties.value(QStringLiteral("share-types")).isEmpty()) {
item->_remotePerm.setPermission(RemotePermissions::IsShared);
}
item->_isShared = item->_remotePerm.hasPermission(RemotePermissions::IsShared);
item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
item->_isEncrypted = properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1");
item->_locked =
properties.value(QStringLiteral("lock")) == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem;
item->_lockOwnerDisplayName = properties.value(QStringLiteral("lock-owner-displayname"));
item->_lockOwnerId = properties.value(QStringLiteral("lock-owner"));
item->_lockEditorApp = properties.value(QStringLiteral("lock-owner-editor"));
{
auto ok = false;
const auto intConvertedValue = properties.value(QStringLiteral("lock-owner-type")).toULongLong(&ok);
item->_lockOwnerType = ok ? static_cast<SyncFileItem::LockOwnerType>(intConvertedValue) : SyncFileItem::LockOwnerType::UserLock;
}
{
auto ok = false;
const auto intConvertedValue = properties.value(QStringLiteral("lock-time")).toULongLong(&ok);
item->_lockTime = ok ? intConvertedValue : 0;
}
{
auto ok = false;
const auto intConvertedValue = properties.value(QStringLiteral("lock-timeout")).toULongLong(&ok);
item->_lockTimeout = ok ? intConvertedValue : 0;
}
const auto date = QDateTime::fromString(properties.value(QStringLiteral("getlastmodified")), Qt::RFC2822Date);
Q_ASSERT(date.isValid());
if (date.toSecsSinceEpoch() > 0) {
item->_modtime = date.toSecsSinceEpoch();
}
if (properties.contains(QStringLiteral("getetag"))) {
item->_etag = parseEtag(properties.value(QStringLiteral("getetag")).toUtf8());
}
if (properties.contains(QStringLiteral("checksums"))) {
item->_checksumHeader = findBestChecksum(properties.value("checksums").toUtf8());
}
// direction and instruction are decided later
item->_direction = SyncFileItem::None;
item->_instruction = CSYNC_INSTRUCTION_NONE;
return item;
}
} }

View file

@ -124,6 +124,10 @@ public:
*/ */
static SyncFileItemPtr fromSyncJournalFileRecord(const SyncJournalFileRecord &rec); static SyncFileItemPtr fromSyncJournalFileRecord(const SyncJournalFileRecord &rec);
/** Creates a basic SyncFileItem from remote properties
*/
[[nodiscard]] static SyncFileItemPtr fromProperties(const QString &filePath, const QMap<QString, QString> &properties);
SyncFileItem() SyncFileItem()
: _type(ItemTypeSkip) : _type(ItemTypeSkip)