зеркало из https://github.com/nextcloud/desktop.git
SyncEngine: Fix renaming of folder when file are changed (#5195)
Two bugs: - The change filed are not considered as move, they are re-downloaded but the old file was not removed from the database. The change in owncloudpropagator.cpp takes care of removing the old entries. - Next sync would then remove the file in the server in the old folder This was not a problem until we start reusing the sync engine, and that the _renamedFolders map is not cleared. We were before deleting a non-existing file. But now we delete the actual file. Also improve the tests to be able to do move on the server. This include support for file id. Issue #5192
This commit is contained in:
Родитель
ea9d17b41d
Коммит
85b8ab178e
|
@ -668,6 +668,12 @@ void PropagateDirectory::finalize()
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
if (!_item->isEmpty() && _hasError == SyncFileItem::NoStatus) {
|
if (!_item->isEmpty() && _hasError == SyncFileItem::NoStatus) {
|
||||||
if( !_item->_renameTarget.isEmpty() ) {
|
if( !_item->_renameTarget.isEmpty() ) {
|
||||||
|
if(_item->_instruction == CSYNC_INSTRUCTION_RENAME
|
||||||
|
&& _item->_originalFile != _item->_renameTarget) {
|
||||||
|
// Remove the stale entries from the database.
|
||||||
|
_propagator->_journal->deleteFileRecord(_item->_originalFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
_item->_file = _item->_renameTarget;
|
_item->_file = _item->_renameTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -868,6 +868,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
|
||||||
bool walkOk = true;
|
bool walkOk = true;
|
||||||
_seenFiles.clear();
|
_seenFiles.clear();
|
||||||
_temporarilyUnavailablePaths.clear();
|
_temporarilyUnavailablePaths.clear();
|
||||||
|
_renamedFolders.clear();
|
||||||
|
|
||||||
if( csync_walk_local_tree(_csync_ctx, &treewalkLocal, 0) < 0 ) {
|
if( csync_walk_local_tree(_csync_ctx, &treewalkLocal, 0) < 0 ) {
|
||||||
qDebug() << "Error in local treewalk.";
|
qDebug() << "Error in local treewalk.";
|
||||||
|
|
|
@ -14,13 +14,17 @@
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QMap>
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
|
static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
|
||||||
|
|
||||||
static QString generateEtag() {
|
inline QString generateEtag() {
|
||||||
return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
|
return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
|
||||||
}
|
}
|
||||||
|
inline QByteArray generateFileId() {
|
||||||
|
return QByteArray::number(qrand(), 16);
|
||||||
|
}
|
||||||
|
|
||||||
class PathComponents : public QStringList {
|
class PathComponents : public QStringList {
|
||||||
public:
|
public:
|
||||||
|
@ -44,6 +48,7 @@ public:
|
||||||
virtual void setContents(const QString &relativePath, char contentChar) = 0;
|
virtual void setContents(const QString &relativePath, char contentChar) = 0;
|
||||||
virtual void appendByte(const QString &relativePath) = 0;
|
virtual void appendByte(const QString &relativePath) = 0;
|
||||||
virtual void mkdir(const QString &relativePath) = 0;
|
virtual void mkdir(const QString &relativePath) = 0;
|
||||||
|
virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DiskFileModifier : public FileModifier
|
class DiskFileModifier : public FileModifier
|
||||||
|
@ -85,6 +90,9 @@ public:
|
||||||
void mkdir(const QString &relativePath) override {
|
void mkdir(const QString &relativePath) override {
|
||||||
_rootDir.mkpath(relativePath);
|
_rootDir.mkpath(relativePath);
|
||||||
}
|
}
|
||||||
|
void rename(const QString &, const QString &) override {
|
||||||
|
Q_ASSERT(!"not implemented");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileInfo : public FileModifier
|
class FileInfo : public FileModifier
|
||||||
|
@ -125,6 +133,7 @@ public:
|
||||||
for (const auto &source : children) {
|
for (const auto &source : children) {
|
||||||
auto &dest = this->children[source.name] = source;
|
auto &dest = this->children[source.name] = source;
|
||||||
dest.parentPath = p;
|
dest.parentPath = p;
|
||||||
|
dest.fixupParentPathRecursively();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +165,21 @@ public:
|
||||||
createDir(relativePath);
|
createDir(relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void rename(const QString &oldPath, const QString &newPath) override {
|
||||||
|
const PathComponents newPathComponents{newPath};
|
||||||
|
FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
|
||||||
|
Q_ASSERT(dir);
|
||||||
|
Q_ASSERT(dir->isDir);
|
||||||
|
const PathComponents pathComponents{oldPath};
|
||||||
|
FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
|
||||||
|
Q_ASSERT(parent);
|
||||||
|
FileInfo fi = parent->children.take(pathComponents.fileName());
|
||||||
|
fi.parentPath = dir->path();
|
||||||
|
fi.name = newPathComponents.fileName();
|
||||||
|
fi.fixupParentPathRecursively();
|
||||||
|
dir->children.insert(newPathComponents.fileName(), std::move(fi));
|
||||||
|
}
|
||||||
|
|
||||||
FileInfo *find(const PathComponents &pathComponents, const bool invalidateEtags = false) {
|
FileInfo *find(const PathComponents &pathComponents, const bool invalidateEtags = false) {
|
||||||
if (pathComponents.isEmpty()) {
|
if (pathComponents.isEmpty()) {
|
||||||
if (invalidateEtags)
|
if (invalidateEtags)
|
||||||
|
@ -217,6 +241,7 @@ public:
|
||||||
bool isShared = false;
|
bool isShared = false;
|
||||||
QDateTime lastModified = QDateTime::currentDateTime().addDays(-7);
|
QDateTime lastModified = QDateTime::currentDateTime().addDays(-7);
|
||||||
QString etag = generateEtag();
|
QString etag = generateEtag();
|
||||||
|
QByteArray fileId = generateFileId();
|
||||||
qint64 size = 0;
|
qint64 size = 0;
|
||||||
char contentChar = 'W';
|
char contentChar = 'W';
|
||||||
|
|
||||||
|
@ -228,6 +253,19 @@ private:
|
||||||
FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) {
|
FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) {
|
||||||
return find(pathComponents, true);
|
return find(pathComponents, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fixupParentPathRecursively() {
|
||||||
|
auto p = path();
|
||||||
|
for (auto it = children.begin(); it != children.end(); ++it) {
|
||||||
|
Q_ASSERT(it.key() == it->name);
|
||||||
|
it->parentPath = p;
|
||||||
|
it->fixupParentPathRecursively();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
friend inline QDebug operator<<(QDebug dbg, const FileInfo& fi) {
|
||||||
|
return dbg << "{ " << fi.path() << ": " << fi.children;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class FakePropfindReply : public QNetworkReply
|
class FakePropfindReply : public QNetworkReply
|
||||||
|
@ -273,6 +311,7 @@ public:
|
||||||
xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
|
xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
|
||||||
xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag);
|
xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag);
|
||||||
xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
|
xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
|
||||||
|
xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId);
|
||||||
xml.writeEndElement(); // prop
|
xml.writeEndElement(); // prop
|
||||||
xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
|
xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
|
||||||
xml.writeEndElement(); // propstat
|
xml.writeEndElement(); // propstat
|
||||||
|
@ -282,6 +321,7 @@ public:
|
||||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||||
const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
|
const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
|
||||||
|
Q_ASSERT(fileInfo);
|
||||||
|
|
||||||
writeFileResponse(*fileInfo);
|
writeFileResponse(*fileInfo);
|
||||||
foreach(const FileInfo &childFileInfo, fileInfo->children)
|
foreach(const FileInfo &childFileInfo, fileInfo->children)
|
||||||
|
@ -380,7 +420,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_INVOKABLE void respond() {
|
Q_INVOKABLE void respond() {
|
||||||
// FIXME: setRawHeader("OC-FileId", fileInfo->???);
|
setRawHeader("OC-FileId", fileInfo->fileId);
|
||||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
|
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
|
||||||
emit metaDataChanged();
|
emit metaDataChanged();
|
||||||
emit finished();
|
emit finished();
|
||||||
|
@ -443,6 +483,7 @@ public:
|
||||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
|
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
|
||||||
setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
|
setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
|
||||||
setRawHeader("ETag", fileInfo->etag.toLatin1());
|
setRawHeader("ETag", fileInfo->etag.toLatin1());
|
||||||
|
setRawHeader("OC-FileId", fileInfo->fileId);
|
||||||
emit metaDataChanged();
|
emit metaDataChanged();
|
||||||
if (bytesAvailable())
|
if (bytesAvailable())
|
||||||
emit readyRead();
|
emit readyRead();
|
||||||
|
|
|
@ -119,6 +119,33 @@ private slots:
|
||||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml"));
|
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml"));
|
||||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testRemoteChangeInMovedFolder() {
|
||||||
|
// issue #5192
|
||||||
|
FakeFolder fakeFolder{FileInfo{ QString(), {
|
||||||
|
FileInfo { QStringLiteral("folder"), {
|
||||||
|
FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } },
|
||||||
|
QStringLiteral("folderB")
|
||||||
|
}
|
||||||
|
}}}};
|
||||||
|
|
||||||
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||||
|
|
||||||
|
// Edit a file in a moved directory.
|
||||||
|
fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
|
||||||
|
fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
|
||||||
|
fakeFolder.syncOnce();
|
||||||
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||||
|
auto oldState = fakeFolder.currentLocalState();
|
||||||
|
QVERIFY(oldState.find(PathComponents("folder/folderB/folderA/file.txt")));
|
||||||
|
QVERIFY(!oldState.find(PathComponents("folder/folderA/file.txt")));
|
||||||
|
|
||||||
|
// This sync should not remove the file
|
||||||
|
fakeFolder.syncOnce();
|
||||||
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||||
|
QCOMPARE(fakeFolder.currentLocalState(), oldState);
|
||||||
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestSyncEngine)
|
QTEST_GUILESS_MAIN(TestSyncEngine)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче