mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-22 13:05:51 +03:00
Merge pull request #5175 from nextcloud/feature/edit-file-locally-restart-sync
Feature/edit file locally restart sync
This commit is contained in:
commit
f6ac52bcd7
21 changed files with 603 additions and 119 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
25
src/libsync/helpers.cpp
Normal 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
25
src/libsync/helpers.h
Normal 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
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue