Skip to content

Commit c821ea4

Browse files
allexzandermgallien
authored andcommitted
Implement BulkPropagateDownloadJob for virtual files.
Signed-off-by: alex-z <blackslayer4@gmail.com>
1 parent 6e845c6 commit c821ea4

File tree

5 files changed

+415
-1
lines changed

5 files changed

+415
-1
lines changed

src/libsync/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ set(libsync_SRCS
8787
propagateuploadng.cpp
8888
bulkpropagatorjob.h
8989
bulkpropagatorjob.cpp
90+
bulkpropagatordownloadjob.h
91+
bulkpropagatordownloadjob.cpp
9092
putmultifilejob.h
9193
putmultifilejob.cpp
9294
propagateremotedelete.h
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
* Copyright (C) 2024 by Oleksandr Zolotov <alex@nextcloud.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but
10+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* for more details.
13+
*/
14+
15+
#include "bulkpropagatordownloadjob.h"
16+
17+
#include "owncloudpropagator_p.h"
18+
#include "syncfileitem.h"
19+
#include "syncengine.h"
20+
#include "common/syncjournaldb.h"
21+
#include "common/syncjournalfilerecord.h"
22+
#include "propagatorjobs.h"
23+
#include "filesystem.h"
24+
#include "account.h"
25+
#include "networkjobs.h"
26+
#include "propagatedownloadencrypted.h"
27+
28+
#include <QDir>
29+
30+
namespace OCC {
31+
32+
Q_LOGGING_CATEGORY(lcBulkPropagatorDownloadJob, "nextcloud.sync.propagator.bulkdownload", QtInfoMsg)
33+
34+
BulkPropagatorDownloadJob::BulkPropagatorDownloadJob(OwncloudPropagator *propagator,
35+
PropagateDirectory *parentDirJob,
36+
const std::vector<SyncFileItemPtr> &items)
37+
: PropagatorJob(propagator)
38+
, _filesToDownload(items)
39+
, _parentDirJob(parentDirJob)
40+
{
41+
}
42+
43+
namespace
44+
{
45+
static QString makeRecallFileName(const QString &fn)
46+
{
47+
QString recallFileName(fn);
48+
// Add _recall-XXXX before the extension.
49+
int dotLocation = recallFileName.lastIndexOf('.');
50+
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
51+
if (dotLocation <= recallFileName.lastIndexOf('/') + 1) {
52+
dotLocation = recallFileName.size();
53+
}
54+
55+
QString timeString = QDateTime::currentDateTimeUtc().toString("yyyyMMdd-hhmmss");
56+
recallFileName.insert(dotLocation, "_.sys.admin#recall#-" + timeString);
57+
58+
return recallFileName;
59+
}
60+
61+
void handleRecallFile(const QString &filePath, const QString &folderPath, SyncJournalDb &journal)
62+
{
63+
qCDebug(lcBulkPropagatorDownloadJob) << "handleRecallFile: " << filePath;
64+
65+
FileSystem::setFileHidden(filePath, true);
66+
67+
QFile file(filePath);
68+
if (!file.open(QIODevice::ReadOnly)) {
69+
qCWarning(lcBulkPropagatorDownloadJob) << "Could not open recall file" << file.errorString();
70+
return;
71+
}
72+
QFileInfo existingFile(filePath);
73+
QDir baseDir = existingFile.dir();
74+
75+
while (!file.atEnd()) {
76+
QByteArray line = file.readLine();
77+
line.chop(1); // remove trailing \n
78+
79+
QString recalledFile = QDir::cleanPath(baseDir.filePath(line));
80+
if (!recalledFile.startsWith(folderPath) || !recalledFile.startsWith(baseDir.path())) {
81+
qCWarning(lcBulkPropagatorDownloadJob) << "Ignoring recall of " << recalledFile;
82+
continue;
83+
}
84+
85+
// Path of the recalled file in the local folder
86+
QString localRecalledFile = recalledFile.mid(folderPath.size());
87+
88+
SyncJournalFileRecord record;
89+
if (!journal.getFileRecord(localRecalledFile, &record) || !record.isValid()) {
90+
qCWarning(lcBulkPropagatorDownloadJob) << "No db entry for recall of" << localRecalledFile;
91+
continue;
92+
}
93+
94+
qCInfo(lcBulkPropagatorDownloadJob) << "Recalling" << localRecalledFile << "Checksum:" << record._checksumHeader;
95+
96+
QString targetPath = makeRecallFileName(recalledFile);
97+
98+
qCDebug(lcBulkPropagatorDownloadJob) << "Copy recall file: " << recalledFile << " -> " << targetPath;
99+
// Remove the target first, QFile::copy will not overwrite it.
100+
FileSystem::remove(targetPath);
101+
QFile::copy(recalledFile, targetPath);
102+
}
103+
}
104+
}
105+
106+
void BulkPropagatorDownloadJob::addDownloadItem(const SyncFileItemPtr &item)
107+
{
108+
Q_ASSERT(item->isDirectory() || item->_type == ItemTypeVirtualFileDehydration || item->_type == ItemTypeVirtualFile);
109+
if (item->isDirectory() || (item->_type != ItemTypeVirtualFileDehydration && item->_type != ItemTypeVirtualFile)) {
110+
qCDebug(lcBulkPropagatorDownloadJob) << "Failed to process bulk download for a non-virtual file" << item->_originalFile;
111+
return;
112+
}
113+
_filesToDownload.push_back(item);
114+
}
115+
116+
bool BulkPropagatorDownloadJob::scheduleSelfOrChild()
117+
{
118+
if (_filesToDownload.empty()) {
119+
return false;
120+
}
121+
122+
_state = Running;
123+
124+
for (const auto &fileToDownload : _filesToDownload) {
125+
qCDebug(lcBulkPropagatorDownloadJob) << "Scheduling bulk propagator job:" << this << "and starting download of item"
126+
<< "with file:" << fileToDownload->_file << "with size:" << fileToDownload->_size;
127+
_filesDownloading.push_back(fileToDownload);
128+
start(fileToDownload);
129+
}
130+
131+
_filesToDownload.clear();
132+
133+
checkPropagationIsDone();
134+
135+
return true;
136+
}
137+
138+
PropagatorJob::JobParallelism BulkPropagatorDownloadJob::parallelism() const
139+
{
140+
return PropagatorJob::JobParallelism::FullParallelism;
141+
}
142+
143+
void BulkPropagatorDownloadJob::startAfterIsEncryptedIsChecked(const SyncFileItemPtr &item)
144+
{
145+
const auto &vfs = propagator()->syncOptions()._vfs;
146+
Q_ASSERT(vfs && vfs->mode() == Vfs::WindowsCfApi);
147+
Q_ASSERT(item->_type == ItemTypeVirtualFileDehydration || item->_type == ItemTypeVirtualFile);
148+
149+
if (propagator()->localFileNameClash(item->_file)) {
150+
_parentDirJob->appendTask(item);
151+
finalizeOneFile(item);
152+
return;
153+
}
154+
155+
// For virtual files just dehydrate or create the placeholder and be done
156+
if (item->_type == ItemTypeVirtualFileDehydration) {
157+
const auto fsPath = propagator()->fullLocalPath(item->_file);
158+
if (!FileSystem::verifyFileUnchanged(fsPath, item->_previousSize, item->_previousModtime)) {
159+
propagator()->_anotherSyncNeeded = true;
160+
item->_errorString = tr("File has changed since discovery");
161+
abortWithError(item, SyncFileItem::SoftError, tr("File has changed since discovery"));
162+
return;
163+
}
164+
qCDebug(lcBulkPropagatorDownloadJob) << "dehydrating file" << item->_file;
165+
const auto r = vfs->dehydratePlaceholder(*item);
166+
if (!r) {
167+
qCCritical(lcBulkPropagatorDownloadJob) << "Could not dehydrate a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error();
168+
abortWithError(item, SyncFileItem::NormalError, r.error());
169+
return;
170+
}
171+
if (!propagator()->_journal->deleteFileRecord(item->_originalFile)) {
172+
qCWarning(lcBulkPropagatorDownloadJob) << "could not delete file from local DB" << item->_originalFile;
173+
abortWithError(item, SyncFileItem::NormalError, tr("Could not delete file record %1 from local DB").arg(item->_originalFile));
174+
return;
175+
}
176+
} else if (item->_type == ItemTypeVirtualFile) {
177+
qCDebug(lcBulkPropagatorDownloadJob) << "creating virtual file" << item->_file;
178+
const auto r = vfs->createPlaceholder(*item);
179+
if (!r) {
180+
qCCritical(lcBulkPropagatorDownloadJob) << "Could not create a placholder for a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error();
181+
abortWithError(item, SyncFileItem::NormalError, r.error());
182+
return;
183+
}
184+
} else {
185+
// we should never get here, as BulkPropagatorDownloadJob must only ever be instantiated and only contain virtual files
186+
qCCritical(lcBulkPropagatorDownloadJob) << "File" << QDir::toNativeSeparators(item->_file) << "can not be downloaded because it is non virtual!";
187+
abortWithError(item, SyncFileItem::NormalError, tr("File %1 can not be downloaded because it is non virtual!").arg(QDir::toNativeSeparators(item->_file)));
188+
return;
189+
}
190+
191+
if (!updateMetadata(item)) {
192+
return;
193+
}
194+
195+
if (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) {
196+
// make sure ReadOnly flag is preserved for placeholder, similarly to regular files
197+
FileSystem::setFileReadOnly(propagator()->fullLocalPath(item->_file), true);
198+
}
199+
finalizeOneFile(item);
200+
}
201+
202+
void BulkPropagatorDownloadJob::finalizeOneFile(const SyncFileItemPtr &file)
203+
{
204+
const auto foundIt = std::find_if(std::cbegin(_filesDownloading), std::cend(_filesDownloading), [&file](const auto &fileDownloading) {
205+
return fileDownloading == file;
206+
});
207+
if (foundIt != std::cend(_filesDownloading)) {
208+
emit propagator()->itemCompleted(file, ErrorCategory::GenericError);
209+
_filesDownloading.erase(foundIt);
210+
}
211+
checkPropagationIsDone();
212+
}
213+
214+
void BulkPropagatorDownloadJob::checkPropagationIsDone()
215+
{
216+
if (_filesToDownload.empty() && _filesDownloading.empty()) {
217+
qCInfo(lcBulkPropagatorDownloadJob) << "finished with status" << SyncFileItem::Status::Success;
218+
emit finished(SyncFileItem::Status::Success);
219+
propagator()->scheduleNextJob();
220+
}
221+
}
222+
223+
void BulkPropagatorDownloadJob::start(const SyncFileItemPtr &item)
224+
{
225+
if (propagator()->_abortRequested) {
226+
return;
227+
}
228+
229+
qCDebug(lcBulkPropagatorDownloadJob) << item->_file << propagator()->_activeJobList.count();
230+
231+
const auto path = item->_file;
232+
const auto slashPosition = path.lastIndexOf('/');
233+
const auto parentPath = slashPosition >= 0 ? path.left(slashPosition) : QString();
234+
235+
SyncJournalFileRecord parentRec;
236+
if (!propagator()->_journal->getFileRecord(parentPath, &parentRec)) {
237+
qCWarning(lcBulkPropagatorDownloadJob) << "could not get file from local DB" << parentPath;
238+
abortWithError(item, SyncFileItem::NormalError, tr("could not get file %1 from local DB").arg(parentPath));
239+
return;
240+
}
241+
242+
if (!propagator()->account()->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || !parentRec.isE2eEncrypted()) {
243+
startAfterIsEncryptedIsChecked(item);
244+
} else {
245+
_downloadEncryptedHelper = new PropagateDownloadEncrypted(propagator(), parentPath, item, this);
246+
connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::fileMetadataFound, [this, &item] {
247+
startAfterIsEncryptedIsChecked(item);
248+
});
249+
connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::failed, [this, &item] {
250+
abortWithError(
251+
item,
252+
SyncFileItem::NormalError,
253+
tr("File %1 cannot be downloaded because encryption information is missing.").arg(QDir::toNativeSeparators(item->_file)));
254+
});
255+
_downloadEncryptedHelper->start();
256+
}
257+
}
258+
259+
bool BulkPropagatorDownloadJob::updateMetadata(const SyncFileItemPtr &item)
260+
{
261+
const auto fn = propagator()->fullLocalPath(item->_file);
262+
const auto result = propagator()->updateMetadata(*item);
263+
if (!result) {
264+
abortWithError(item, SyncFileItem::FatalError, tr("Error updating metadata: %1").arg(result.error()));
265+
return false;
266+
} else if (*result == Vfs::ConvertToPlaceholderResult::Locked) {
267+
abortWithError(item, SyncFileItem::SoftError, tr("The file %1 is currently in use").arg(item->_file));
268+
return false;
269+
}
270+
271+
propagator()->_journal->commit("download file start2");
272+
273+
// handle the special recall file
274+
if (!item->_remotePerm.hasPermission(RemotePermissions::IsShared)
275+
&& (item->_file == QLatin1String(".sys.admin#recall#") || item->_file.endsWith(QLatin1String("/.sys.admin#recall#")))) {
276+
handleRecallFile(fn, propagator()->localPath(), *propagator()->_journal);
277+
}
278+
279+
const auto isLockOwnedByCurrentUser = item->_lockOwnerId == propagator()->account()->davUser();
280+
281+
const auto isUserLockOwnedByCurrentUser = (item->_lockOwnerType == SyncFileItem::LockOwnerType::UserLock && isLockOwnedByCurrentUser);
282+
const auto isTokenLockOwnedByCurrentUser = (item->_lockOwnerType == SyncFileItem::LockOwnerType::TokenLock && isLockOwnedByCurrentUser);
283+
284+
if (item->_locked == SyncFileItem::LockStatus::LockedItem && !isUserLockOwnedByCurrentUser && !isTokenLockOwnedByCurrentUser) {
285+
qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is locked: making it read only";
286+
FileSystem::setFileReadOnly(fn, true);
287+
} else {
288+
qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is not locked: making it" << ((!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite))
289+
? "read only"
290+
: "read write");
291+
FileSystem::setFileReadOnlyWeak(fn, (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)));
292+
}
293+
return true;
294+
}
295+
296+
void BulkPropagatorDownloadJob::done(const SyncFileItem::Status status)
297+
{
298+
emit finished(status);
299+
}
300+
301+
void BulkPropagatorDownloadJob::abortWithError(SyncFileItemPtr item, SyncFileItem::Status status, const QString &error)
302+
{
303+
qCInfo(lcBulkPropagatorDownloadJob) << "finished with status" << status << error;
304+
abort(AbortType::Synchronous);
305+
if (item) {
306+
item->_errorString = error;
307+
item->_status = status;
308+
emit propagator()->itemCompleted(item, ErrorCategory::GenericError);
309+
}
310+
done(status);
311+
}
312+
313+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (C) 2024 by Oleksandr Zolotov <alex@nextcloud.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but
10+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* for more details.
13+
*/
14+
15+
#pragma once
16+
17+
#include "owncloudpropagator.h"
18+
#include "abstractnetworkjob.h"
19+
20+
#include <QLoggingCategory>
21+
#include <QVector>
22+
23+
namespace OCC {
24+
25+
class PropagateDownloadEncrypted;
26+
27+
Q_DECLARE_LOGGING_CATEGORY(lcBulkPropagatorDownloadJob)
28+
29+
class BulkPropagatorDownloadJob : public PropagatorJob
30+
{
31+
Q_OBJECT
32+
33+
public:
34+
explicit BulkPropagatorDownloadJob(OwncloudPropagator *propagator, PropagateDirectory *parentDirJob, const std::vector<SyncFileItemPtr> &items = {});
35+
36+
bool scheduleSelfOrChild() override;
37+
38+
[[nodiscard]] JobParallelism parallelism() const override;
39+
40+
public slots:
41+
void addDownloadItem(const SyncFileItemPtr &item);
42+
void start(const SyncFileItemPtr &item);
43+
44+
private slots:
45+
void startAfterIsEncryptedIsChecked(const SyncFileItemPtr &item);
46+
47+
void finalizeOneFile(const SyncFileItemPtr &file);
48+
49+
void done( const SyncFileItem::Status status);
50+
51+
void abortWithError(SyncFileItemPtr item, SyncFileItem::Status status, const QString &error);
52+
53+
private:
54+
bool updateMetadata(const SyncFileItemPtr &item);
55+
void checkPropagationIsDone();
56+
57+
std::vector<SyncFileItemPtr> _filesToDownload;
58+
std::vector<SyncFileItemPtr> _filesDownloading;
59+
60+
PropagateDownloadEncrypted *_downloadEncryptedHelper = nullptr;
61+
62+
PropagateDirectory *_parentDirJob = nullptr;
63+
};
64+
65+
}

0 commit comments

Comments
 (0)