Merge pull request #8568 from nextcloud/feature/bulkCreateWindowsPlaceholdersPerFolders

Feature/bulk create windows placeholders per folders (V1)
This commit is contained in:
Matthieu Gallien 2025-08-29 14:41:15 +02:00 committed by GitHub
commit a1d263c7a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 270 additions and 193 deletions

View File

@ -14,6 +14,7 @@
#include <QScopedPointer>
#include <QSharedPointer>
#include <deque>
#include <memory>
using csync_file_stat_t = struct csync_file_stat_s;
@ -25,6 +26,7 @@ using AccountPtr = QSharedPointer<Account>;
class SyncJournalDb;
class VfsPrivate;
class SyncFileItem;
using SyncFileItemPtr = QSharedPointer<SyncFileItem>;
/** Collection of parameters for initializing a Vfs instance. */
struct OCSYNC_EXPORT VfsSetupParams
@ -186,21 +188,24 @@ public:
[[nodiscard]] virtual bool isPlaceHolderInSync(const QString &filePath) const = 0;
/// Create a new dehydrated placeholder. Called from PropagateDownload.
Q_REQUIRED_RESULT virtual Result<void, QString> createPlaceholder(const SyncFileItem &item) = 0;
[[nodiscard]] virtual Result<void, QString> createPlaceholder(const SyncFileItem &item) = 0;
/// Create a new dehydrated list of placeholders
[[nodiscard]] virtual Result<void, QString> createPlaceholders(const QList<SyncFileItemPtr> &items) = 0;
/** Convert a hydrated placeholder to a dehydrated one. Called from PropagateDownlaod.
*
* This is different from delete+create because preserving some file metadata
* (like pin states) may be essential for some vfs plugins.
*/
Q_REQUIRED_RESULT virtual Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) = 0;
[[nodiscard]] virtual Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) = 0;
/** Discovery hook: even unchanged files may need UPDATE_METADATA.
*
* For instance cfapi vfs wants local hydrated non-placeholder files to
* become hydrated placeholder files.
*/
Q_REQUIRED_RESULT virtual bool needsMetadataUpdate(const SyncFileItem &item) = 0;
[[nodiscard]] virtual bool needsMetadataUpdate(const SyncFileItem &item) = 0;
/** Convert a new file to a hydrated placeholder.
*
@ -215,13 +220,13 @@ public:
* new placeholder shall supersede, for rename-replace actions with new downloads,
* for example.
*/
Q_REQUIRED_RESULT virtual Result<Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &filename,
[[nodiscard]] virtual Result<Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &filename,
const SyncFileItem &item,
const QString &replacesFile = {},
UpdateMetadataTypes updateType = AllMetadata) = 0;
/// Determine whether the file at the given absolute path is a dehydrated placeholder.
Q_REQUIRED_RESULT virtual bool isDehydratedPlaceholder(const QString &filePath) = 0;
[[nodiscard]] virtual bool isDehydratedPlaceholder(const QString &filePath) = 0;
/** Similar to isDehydratedPlaceholder() but used from sync discovery.
*
@ -230,7 +235,7 @@ public:
*
* Returning true means that type was fully determined.
*/
Q_REQUIRED_RESULT virtual bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) = 0;
[[nodiscard]] virtual bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) = 0;
/** Sets the pin state for the item at a path.
*
@ -241,7 +246,7 @@ public:
*
* folderPath is relative to the sync folder. Can be "" for root folder.
*/
Q_REQUIRED_RESULT virtual bool setPinState(const QString &folderPath, PinState state) = 0;
[[nodiscard]] virtual bool setPinState(const QString &folderPath, PinState state) = 0;
/** Returns the pin state of an item at a path.
*
@ -252,7 +257,7 @@ public:
*
* Returns none on retrieval error.
*/
Q_REQUIRED_RESULT virtual Optional<PinState> pinState(const QString &folderPath) = 0;
[[nodiscard]] virtual Optional<PinState> pinState(const QString &folderPath) = 0;
/** Returns availability status of an item at a path.
*
@ -261,7 +266,7 @@ public:
*
* folderPath is relative to the sync folder. Can be "" for root folder.
*/
Q_REQUIRED_RESULT virtual AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) = 0;
[[nodiscard]] virtual AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) = 0;
public slots:
/** Update in-sync state based on SyncFileStatusTracker signal.
@ -323,7 +328,10 @@ public:
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updateMetadata(const SyncFileItem &, const QString &, const QString &) override { return {OCC::Vfs::ConvertToPlaceholderResult::Ok}; }
Result<Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderMarkInSync(const QString &filePath, const QByteArray &fileId) override {Q_UNUSED(filePath) Q_UNUSED(fileId) return {QString{}};}
[[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; }
Result<void, QString> createPlaceholder(const SyncFileItem &) override { return {}; }
Result<void, QString> createPlaceholders(const QList<SyncFileItemPtr> &) override { return {}; }
Result<void, QString> dehydratePlaceholder(const SyncFileItem &) override { return {}; }
Result<ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &, const SyncFileItem &, const QString &, const UpdateMetadataTypes) override { return ConvertToPlaceholderResult::Ok; }

View File

@ -120,7 +120,7 @@ public:
/*
* Get whether the share has a password set
*/
[[nodiscard]] Q_REQUIRED_RESULT bool isPasswordSet() const;
[[nodiscard]] bool isPasswordSet() const;
/*
* Is it a share with a user or group (local or remote)

View File

@ -81,12 +81,12 @@ public:
enum class FileDetailsPage { Activity, Sharing };
Q_ENUM(FileDetailsPage);
Q_REQUIRED_RESULT QString windowTitle() const;
Q_REQUIRED_RESULT bool useNormalWindow() const;
[[nodiscard]] QString windowTitle() const;
[[nodiscard]] bool useNormalWindow() const;
Q_REQUIRED_RESULT bool syncIsPaused() const;
Q_REQUIRED_RESULT bool anySyncFolders() const;
Q_REQUIRED_RESULT bool isOpen() const;
[[nodiscard]] bool syncIsPaused() const;
[[nodiscard]] bool anySyncFolders() const;
[[nodiscard]] bool isOpen() const;
[[nodiscard]] bool enableAddAccount() const;

View File

@ -67,30 +67,30 @@ public:
explicit UserStatusSelectorModel(const UserStatus &userStatus,
QObject *parent = nullptr);
Q_REQUIRED_RESULT int userIndex() const;
[[nodiscard]] int userIndex() const;
Q_REQUIRED_RESULT UserStatus::OnlineStatus onlineStatus() const;
[[nodiscard]] UserStatus::OnlineStatus onlineStatus() const;
void setOnlineStatus(UserStatus::OnlineStatus status);
Q_REQUIRED_RESULT QUrl onlineIcon() const;
Q_REQUIRED_RESULT QUrl awayIcon() const;
Q_REQUIRED_RESULT QUrl dndIcon() const;
Q_REQUIRED_RESULT QUrl busyIcon() const;
Q_REQUIRED_RESULT QUrl invisibleIcon() const;
[[nodiscard]] QUrl onlineIcon() const;
[[nodiscard]] QUrl awayIcon() const;
[[nodiscard]] QUrl dndIcon() const;
[[nodiscard]] QUrl busyIcon() const;
[[nodiscard]] QUrl invisibleIcon() const;
Q_REQUIRED_RESULT QString userStatusMessage() const;
[[nodiscard]] QString userStatusMessage() const;
void setUserStatusMessage(const QString &message);
Q_REQUIRED_RESULT QString userStatusEmoji() const;
[[nodiscard]] QString userStatusEmoji() const;
void setUserStatusEmoji(const QString &emoji);
[[nodiscard]] QVector<UserStatus> predefinedStatuses() const;
Q_REQUIRED_RESULT QVariantList clearStageTypes() const;
Q_REQUIRED_RESULT QString clearAtDisplayString() const;
[[nodiscard]] QVariantList clearStageTypes() const;
[[nodiscard]] QString clearAtDisplayString() const;
[[nodiscard]] Q_INVOKABLE QString clearAtReadable(const OCC::UserStatus &status) const;
Q_REQUIRED_RESULT QString errorMessage() const;
Q_REQUIRED_RESULT bool busyStatusSupported() const;
[[nodiscard]] QString errorMessage() const;
[[nodiscard]] bool busyStatusSupported() const;
public slots:
void setUserIndex(const int userIndex);
@ -117,10 +117,10 @@ private:
void onMessageCleared();
void onError(UserStatusConnector::Error error);
Q_REQUIRED_RESULT QString clearAtReadable(const Optional<ClearAt> &clearAt) const;
Q_REQUIRED_RESULT QString clearAtStageToString(ClearStageType stage) const;
Q_REQUIRED_RESULT QString timeDifferenceToString(int differenceSecs) const;
Q_REQUIRED_RESULT Optional<ClearAt> clearStageTypeToDateTime(ClearStageType type) const;
[[nodiscard]] QString clearAtReadable(const Optional<ClearAt> &clearAt) const;
[[nodiscard]] QString clearAtStageToString(ClearStageType stage) const;
[[nodiscard]] QString timeDifferenceToString(int differenceSecs) const;
[[nodiscard]] Optional<ClearAt> clearStageTypeToDateTime(ClearStageType type) const;
void setError(const QString &reason);
void clearError();

View File

@ -5,7 +5,6 @@
#include "bulkpropagatordownloadjob.h"
#include "owncloudpropagator_p.h"
#include "syncfileitem.h"
#include "syncengine.h"
#include "common/syncjournaldb.h"
@ -13,7 +12,6 @@
#include "propagatorjobs.h"
#include "filesystem.h"
#include "account.h"
#include "networkjobs.h"
#include "propagatedownloadencrypted.h"
#include <QDir>
@ -23,11 +21,10 @@ namespace OCC {
Q_LOGGING_CATEGORY(lcBulkPropagatorDownloadJob, "nextcloud.sync.propagator.bulkdownload", QtInfoMsg)
BulkPropagatorDownloadJob::BulkPropagatorDownloadJob(OwncloudPropagator *propagator,
PropagateDirectory *parentDirJob,
const std::vector<SyncFileItemPtr> &items)
: PropagatorJob(propagator)
, _filesToDownload(items)
, _parentDirJob(parentDirJob)
PropagateDirectory *parentDirJob)
: PropagatorJob{propagator}
, _filesToDownload{}
, _parentDirJob{parentDirJob}
{
}
@ -35,15 +32,15 @@ namespace
{
static QString makeRecallFileName(const QString &fn)
{
QString recallFileName(fn);
auto recallFileName(fn);
// Add _recall-XXXX before the extension.
int dotLocation = recallFileName.lastIndexOf('.');
auto dotLocation = recallFileName.lastIndexOf('.');
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
if (dotLocation <= recallFileName.lastIndexOf('/') + 1) {
dotLocation = recallFileName.size();
}
QString timeString = QDateTime::currentDateTimeUtc().toString("yyyyMMdd-hhmmss");
const auto &timeString = QDateTime::currentDateTimeUtc().toString("yyyyMMdd-hhmmss");
recallFileName.insert(dotLocation, "_.sys.admin#recall#-" + timeString);
return recallFileName;
@ -55,28 +52,28 @@ void handleRecallFile(const QString &filePath, const QString &folderPath, SyncJo
FileSystem::setFileHidden(filePath, true);
QFile file(filePath);
auto file = QFile{filePath};
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcBulkPropagatorDownloadJob) << "Could not open recall file" << file.errorString();
return;
}
QFileInfo existingFile(filePath);
QDir baseDir = existingFile.dir();
const auto existingFile = QFileInfo{filePath};
const auto &baseDir = existingFile.dir();
while (!file.atEnd()) {
QByteArray line = file.readLine();
auto line = file.readLine();
line.chop(1); // remove trailing \n
QString recalledFile = QDir::cleanPath(baseDir.filePath(line));
const auto &recalledFile = QDir::cleanPath(baseDir.filePath(line));
if (!recalledFile.startsWith(folderPath) || !recalledFile.startsWith(baseDir.path())) {
qCWarning(lcBulkPropagatorDownloadJob) << "Ignoring recall of " << recalledFile;
continue;
}
// Path of the recalled file in the local folder
QString localRecalledFile = recalledFile.mid(folderPath.size());
const auto &localRecalledFile = recalledFile.mid(folderPath.size());
SyncJournalFileRecord record;
auto record = SyncJournalFileRecord{};
if (!journal.getFileRecord(localRecalledFile, &record) || !record.isValid()) {
qCWarning(lcBulkPropagatorDownloadJob) << "No db entry for recall of" << localRecalledFile;
continue;
@ -84,7 +81,7 @@ void handleRecallFile(const QString &filePath, const QString &folderPath, SyncJo
qCInfo(lcBulkPropagatorDownloadJob) << "Recalling" << localRecalledFile << "Checksum:" << record._checksumHeader;
QString targetPath = makeRecallFileName(recalledFile);
const auto &targetPath = makeRecallFileName(recalledFile);
qCDebug(lcBulkPropagatorDownloadJob) << "Copy recall file: " << recalledFile << " -> " << targetPath;
// Remove the target first, QFile::copy will not overwrite it.
@ -112,18 +109,11 @@ bool BulkPropagatorDownloadJob::scheduleSelfOrChild()
_state = Running;
for (const auto &fileToDownload : _filesToDownload) {
qCDebug(lcBulkPropagatorDownloadJob) << "Scheduling bulk propagator job:" << this << "and starting download of item"
<< "with file:" << fileToDownload->_file << "with size:" << fileToDownload->_size;
_filesDownloading.push_back(fileToDownload);
start(fileToDownload);
}
start();
_filesToDownload.clear();
_state = Finished;
checkPropagationIsDone();
return true;
return false;
}
PropagatorJob::JobParallelism BulkPropagatorDownloadJob::parallelism() const
@ -131,125 +121,68 @@ PropagatorJob::JobParallelism BulkPropagatorDownloadJob::parallelism() const
return PropagatorJob::JobParallelism::FullParallelism;
}
void BulkPropagatorDownloadJob::startAfterIsEncryptedIsChecked(const SyncFileItemPtr &item)
{
const auto &vfs = propagator()->syncOptions()._vfs;
Q_ASSERT(vfs && vfs->mode() == Vfs::WindowsCfApi);
Q_ASSERT(item->_type == ItemTypeVirtualFileDehydration || item->_type == ItemTypeVirtualFile);
if (propagator()->localFileNameClash(item->_file)) {
_parentDirJob->appendTask(item);
finalizeOneFile(item);
return;
}
// For virtual files just dehydrate or create the placeholder and be done
if (item->_type == ItemTypeVirtualFileDehydration) {
const auto fsPath = propagator()->fullLocalPath(item->_file);
if (!FileSystem::verifyFileUnchanged(fsPath, item->_previousSize, item->_previousModtime)) {
propagator()->_anotherSyncNeeded = true;
item->_errorString = tr("File has changed since discovery");
abortWithError(item, SyncFileItem::SoftError, tr("File has changed since discovery"));
return;
}
qCDebug(lcBulkPropagatorDownloadJob) << "dehydrating file" << item->_file;
const auto r = vfs->dehydratePlaceholder(*item);
if (!r) {
qCCritical(lcBulkPropagatorDownloadJob) << "Could not dehydrate a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error();
abortWithError(item, SyncFileItem::NormalError, r.error());
return;
}
if (!propagator()->_journal->deleteFileRecord(item->_originalFile)) {
qCWarning(lcBulkPropagatorDownloadJob) << "could not delete file from local DB" << item->_originalFile;
abortWithError(item, SyncFileItem::NormalError, tr("Could not delete file record %1 from local DB").arg(item->_originalFile));
return;
}
} else if (item->_type == ItemTypeVirtualFile) {
qCDebug(lcBulkPropagatorDownloadJob) << "creating virtual file" << item->_file;
const auto r = vfs->createPlaceholder(*item);
if (!r) {
qCCritical(lcBulkPropagatorDownloadJob) << "Could not create a placholder for a file" << QDir::toNativeSeparators(item->_file) << ":" << r.error();
abortWithError(item, SyncFileItem::NormalError, r.error());
return;
}
} else {
// we should never get here, as BulkPropagatorDownloadJob must only ever be instantiated and only contain virtual files
qCCritical(lcBulkPropagatorDownloadJob) << "File" << QDir::toNativeSeparators(item->_file) << "can not be downloaded because it is non virtual!";
abortWithError(item, SyncFileItem::NormalError, tr("File %1 cannot be downloaded because it is non virtual!").arg(QDir::toNativeSeparators(item->_file)));
return;
}
if (!updateMetadata(item)) {
return;
}
if (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) {
// make sure ReadOnly flag is preserved for placeholder, similarly to regular files
FileSystem::setFileReadOnly(propagator()->fullLocalPath(item->_file), true);
}
finalizeOneFile(item);
}
void BulkPropagatorDownloadJob::finalizeOneFile(const SyncFileItemPtr &file)
{
const auto foundIt = std::find_if(std::cbegin(_filesDownloading), std::cend(_filesDownloading), [&file](const auto &fileDownloading) {
const auto foundIt = std::find_if(std::cbegin(_filesToDownload), std::cend(_filesToDownload), [&file](const auto &fileDownloading) {
return fileDownloading == file;
});
if (foundIt != std::cend(_filesDownloading)) {
if (foundIt != std::cend(_filesToDownload)) {
emit propagator()->itemCompleted(file, ErrorCategory::GenericError);
_filesDownloading.erase(foundIt);
}
checkPropagationIsDone();
}
void BulkPropagatorDownloadJob::checkPropagationIsDone()
{
if (_filesToDownload.empty() && _filesDownloading.empty()) {
qCInfo(lcBulkPropagatorDownloadJob) << "finished with status" << SyncFileItem::Status::Success;
emit finished(SyncFileItem::Status::Success);
propagator()->scheduleNextJob();
_filesToDownload.erase(foundIt);
}
}
void BulkPropagatorDownloadJob::start(const SyncFileItemPtr &item)
void BulkPropagatorDownloadJob::start()
{
if (propagator()->_abortRequested) {
abortWithError({}, SyncFileItem::NormalError, {});
return;
}
qCDebug(lcBulkPropagatorDownloadJob) << item->_file << propagator()->_activeJobList.count();
for (const auto &fileToDownload : std::as_const(_filesToDownload)) {
Q_ASSERT(fileToDownload->_type == ItemTypeVirtualFile);
const auto path = item->_file;
const auto slashPosition = path.lastIndexOf('/');
const auto parentPath = slashPosition >= 0 ? path.left(slashPosition) : QString();
if (propagator()->localFileNameClash(fileToDownload->_file)) {
finalizeOneFile(fileToDownload);
qCWarning(lcBulkPropagatorDownloadJob) << "File" << QDir::toNativeSeparators(fileToDownload->_file) << "can not be downloaded because of a local file name clash!";
abortWithError(fileToDownload, SyncFileItem::FileNameClash, tr("File %1 can not be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(fileToDownload->_file)));
return;
}
}
SyncJournalFileRecord parentRec;
if (!propagator()->_journal->getFileRecord(parentPath, &parentRec)) {
qCWarning(lcBulkPropagatorDownloadJob) << "could not get file from local DB" << parentPath;
abortWithError(item, SyncFileItem::NormalError, tr("Could not get file %1 from local DB").arg(parentPath));
const auto &vfs = propagator()->syncOptions()._vfs;
Q_ASSERT(vfs && vfs->mode() == Vfs::WindowsCfApi);
const auto r = vfs->createPlaceholders(_filesToDownload);
if (!r) {
qCCritical(lcBulkPropagatorDownloadJob) << "Could not create placholders:" << r.error();
for (const auto &fileToDownload : std::as_const(_filesToDownload)) {
finalizeOneFile(fileToDownload);
}
abortWithError({}, SyncFileItem::NormalError, r.error());
return;
}
if (!propagator()->account()->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || !parentRec.isE2eEncrypted()) {
startAfterIsEncryptedIsChecked(item);
} else {
_downloadEncryptedHelper = new PropagateDownloadEncrypted(propagator(), parentPath, item, this);
connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::fileMetadataFound, [this, &item] {
startAfterIsEncryptedIsChecked(item);
});
connect(_downloadEncryptedHelper, &PropagateDownloadEncrypted::failed, [this, &item] {
abortWithError(
item,
SyncFileItem::NormalError,
tr("File %1 cannot be downloaded because encryption information is missing.").arg(QDir::toNativeSeparators(item->_file)));
});
_downloadEncryptedHelper->start();
for (const auto &fileToDownload : std::as_const(_filesToDownload)) {
if (!updateMetadata(fileToDownload)) {
abortWithError(fileToDownload, SyncFileItem::NormalError, tr("Unable to update metadata of new file %1.", "error with update metadata of new Win VFS file").arg(fileToDownload->_file));
return;
}
if (!fileToDownload->_remotePerm.isNull() && !fileToDownload->_remotePerm.hasPermission(RemotePermissions::CanWrite)) {
// make sure ReadOnly flag is preserved for placeholder, similarly to regular files
FileSystem::setFileReadOnly(propagator()->fullLocalPath(fileToDownload->_file), true);
}
finalizeOneFile(fileToDownload);
}
done(SyncFileItem::Success);
}
bool BulkPropagatorDownloadJob::updateMetadata(const SyncFileItemPtr &item)
{
const auto fn = propagator()->fullLocalPath(item->_file);
const auto fullFileName = propagator()->fullLocalPath(item->_file);
const auto result = propagator()->updateMetadata(*item);
if (!result) {
abortWithError(item, SyncFileItem::FatalError, tr("Error updating metadata: %1").arg(result.error()));
@ -264,7 +197,7 @@ bool BulkPropagatorDownloadJob::updateMetadata(const SyncFileItemPtr &item)
// handle the special recall file
if (!item->_remotePerm.hasPermission(RemotePermissions::IsShared)
&& (item->_file == QLatin1String(".sys.admin#recall#") || item->_file.endsWith(QLatin1String("/.sys.admin#recall#")))) {
handleRecallFile(fn, propagator()->localPath(), *propagator()->_journal);
handleRecallFile(fullFileName, propagator()->localPath(), *propagator()->_journal);
}
const auto isLockOwnedByCurrentUser = item->_lockOwnerId == propagator()->account()->davUser();
@ -273,13 +206,13 @@ bool BulkPropagatorDownloadJob::updateMetadata(const SyncFileItemPtr &item)
const auto isTokenLockOwnedByCurrentUser = (item->_lockOwnerType == SyncFileItem::LockOwnerType::TokenLock && isLockOwnedByCurrentUser);
if (item->_locked == SyncFileItem::LockStatus::LockedItem && !isUserLockOwnedByCurrentUser && !isTokenLockOwnedByCurrentUser) {
qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is locked: making it read only";
FileSystem::setFileReadOnly(fn, true);
qCDebug(lcBulkPropagatorDownloadJob()) << fullFileName << "file is locked: making it read only";
FileSystem::setFileReadOnly(fullFileName, true);
} else {
qCDebug(lcBulkPropagatorDownloadJob()) << fn << "file is not locked: making it" << ((!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite))
qCDebug(lcBulkPropagatorDownloadJob()) << fullFileName << "file is not locked: making it" << ((!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite))
? "read only"
: "read write");
FileSystem::setFileReadOnlyWeak(fn, (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)));
FileSystem::setFileReadOnlyWeak(fullFileName, (!item->_remotePerm.isNull() && !item->_remotePerm.hasPermission(RemotePermissions::CanWrite)));
}
return true;
}

View File

@ -8,21 +8,18 @@
#include "owncloudpropagator.h"
#include "abstractnetworkjob.h"
#include <QLoggingCategory>
#include <QVector>
#include <QList>
namespace OCC {
class PropagateDownloadEncrypted;
Q_DECLARE_LOGGING_CATEGORY(lcBulkPropagatorDownloadJob)
class BulkPropagatorDownloadJob : public PropagatorJob
{
Q_OBJECT
public:
explicit BulkPropagatorDownloadJob(OwncloudPropagator *propagator, PropagateDirectory *parentDirJob, const std::vector<SyncFileItemPtr> &items = {});
explicit BulkPropagatorDownloadJob(OwncloudPropagator *propagator, PropagateDirectory *parentDirJob);
bool scheduleSelfOrChild() override;
@ -31,11 +28,9 @@ public:
public slots:
void addDownloadItem(const OCC::SyncFileItemPtr &item);
void start(const OCC::SyncFileItemPtr &item);
void start();
private slots:
void startAfterIsEncryptedIsChecked(const OCC::SyncFileItemPtr &item);
void finalizeOneFile(const OCC::SyncFileItemPtr &file);
void done(const OCC::SyncFileItem::Status status);
@ -45,11 +40,7 @@ private slots:
private:
bool updateMetadata(const SyncFileItemPtr &item);
void checkPropagationIsDone();
std::vector<SyncFileItemPtr> _filesToDownload;
std::vector<SyncFileItemPtr> _filesDownloading;
QList<SyncFileItemPtr> _filesToDownload;
PropagateDownloadEncrypted *_downloadEncryptedHelper = nullptr;

View File

@ -690,7 +690,8 @@ void OwncloudPropagator::startFilePropagation(const SyncFileItemPtr &item,
const auto isDownload = item->_direction == SyncFileItem::Down &&
(item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC);
const auto isVirtualFile = item->_type == ItemTypeVirtualFile;
const auto shouldAddBulkPropagateDownloadItem = isDownload && isVirtualFile && isVfsCfApi;
const auto isEncrypted = item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted;
const auto shouldAddBulkPropagateDownloadItem = isDownload && isVirtualFile && isVfsCfApi && !isEncrypted;
if (shouldAddBulkPropagateDownloadItem) {
addBulkPropagateDownloadItem(item, directories);

View File

@ -523,14 +523,14 @@ public:
*/
bool hasCaseClashAccessibilityProblem(const QString &relfile);
Q_REQUIRED_RESULT QString fullLocalPath(const QString &tmp_file_name) const;
[[nodiscard]] QString fullLocalPath(const QString &tmp_file_name) const;
[[nodiscard]] QString localPath() const;
/**
* Returns the full remote path including the folder root of a
* folder sync path.
*/
Q_REQUIRED_RESULT QString fullRemotePath(const QString &tmp_file_name) const;
[[nodiscard]] QString fullRemotePath(const QString &tmp_file_name) const;
[[nodiscard]] QString remotePath() const;
[[nodiscard]] QString fulllRemotePathToPathInSyncJournalDb(const QString &fullRemotePath) const;
@ -623,9 +623,9 @@ public:
SyncJournalDb * const journal,
Vfs::UpdateMetadataTypes updateType);
Q_REQUIRED_RESULT bool isDelayedUploadItem(const SyncFileItemPtr &item) const;
[[nodiscard]] bool isDelayedUploadItem(const SyncFileItemPtr &item) const;
Q_REQUIRED_RESULT const std::deque<SyncFileItemPtr>& delayedTasks() const
[[nodiscard]] const std::deque<SyncFileItemPtr>& delayedTasks() const
{
return _delayedTasks;
}

View File

@ -533,11 +533,6 @@ void PropagateDownloadFile::startAfterIsEncryptedIsChecked()
const auto fsPath = propagator()->fullLocalPath(_item->_file);
makeParentFolderModifiable(fsPath);
// do a klaas' case clash check.
if (propagator()->localFileNameClash(_item->_file)) {
done(SyncFileItem::FileNameClash, tr("File %1 can not be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)), ErrorCategory::GenericError);
return;
}
auto r = vfs->createPlaceholder(*_item);
if (!r) {
done(SyncFileItem::NormalError, r.error(), ErrorCategory::GenericError);

View File

@ -62,11 +62,11 @@ public:
UserStatus(const QString &id, const QString &message, const QString &icon,
OnlineStatus state, bool messagePredefined, const Optional<ClearAt> &clearAt = {});
Q_REQUIRED_RESULT QString id() const;
Q_REQUIRED_RESULT QString message() const;
Q_REQUIRED_RESULT QString icon() const;
Q_REQUIRED_RESULT OnlineStatus state() const;
Q_REQUIRED_RESULT Optional<ClearAt> clearAt() const;
[[nodiscard]] QString id() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString icon() const;
[[nodiscard]] OnlineStatus state() const;
[[nodiscard]] Optional<ClearAt> clearAt() const;
[[nodiscard]] QString clearAtDisplayString() const;
@ -77,9 +77,9 @@ public:
void setMessagePredefined(bool value);
void setClearAt(const Optional<ClearAt> &dateTime);
Q_REQUIRED_RESULT bool messagePredefined() const;
[[nodiscard]] bool messagePredefined() const;
Q_REQUIRED_RESULT QUrl stateIcon() const;
[[nodiscard]] QUrl stateIcon() const;
private:
QString _id;

View File

@ -906,6 +906,69 @@ OCC::Result<void, QString> OCC::CfApiWrapper::createPlaceholderInfo(const QStrin
return {};
}
OCC::Result<void, QString> OCC::CfApiWrapper::createPlaceholdersInfo(const QString &localBasePath, const QList<PlaceholdersInfo> &itemsInfo)
{
auto filteredItemsInfo = QList<PlaceholdersInfo>{};
filteredItemsInfo.reserve(itemsInfo.size());
std::copy_if(itemsInfo.begin(), itemsInfo.end(), std::back_inserter(filteredItemsInfo), [] (const auto &onePlaceholderInfo) -> bool {
if (onePlaceholderInfo.modtime <= 0) {
qCWarning(lcCfApiWrapper()) << "Skip invalid modtime file: " << onePlaceholderInfo.relativePath << "modtime:" << onePlaceholderInfo.modtime;
return false;
}
return true;
});
const auto stdWStringBasePath = localBasePath.toStdWString();
auto cloudEntry = std::make_unique<CF_PLACEHOLDER_CREATE_INFO[]>(filteredItemsInfo.size());
for(auto itemIndice = 0; itemIndice < filteredItemsInfo.size(); ++itemIndice) {
const auto &placeholderInfo = filteredItemsInfo[itemIndice];
cloudEntry[itemIndice].FileIdentity = placeholderInfo.fileId.data();
cloudEntry[itemIndice].FileIdentityLength = static_cast<DWORD>(placeholderInfo.fileId.length());
cloudEntry[itemIndice].RelativeFileName = placeholderInfo.platformNativeRelativePath.data();
cloudEntry[itemIndice].Flags = CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC;
cloudEntry[itemIndice].FsMetadata.FileSize.QuadPart = placeholderInfo.size;
cloudEntry[itemIndice].FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
OCC::Utility::UnixTimeToLargeIntegerFiletime(placeholderInfo.modtime, &cloudEntry[itemIndice].FsMetadata.BasicInfo.CreationTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(placeholderInfo.modtime, &cloudEntry[itemIndice].FsMetadata.BasicInfo.LastWriteTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(placeholderInfo.modtime, &cloudEntry[itemIndice].FsMetadata.BasicInfo.LastAccessTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(placeholderInfo.modtime, &cloudEntry[itemIndice].FsMetadata.BasicInfo.ChangeTime);
if (placeholderInfo.fileInfo.isDir()) {
cloudEntry[itemIndice].Flags |= CF_PLACEHOLDER_CREATE_FLAG_DISABLE_ON_DEMAND_POPULATION;
cloudEntry[itemIndice].FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_DIRECTORY;
cloudEntry[itemIndice].FsMetadata.FileSize.QuadPart = 0;
}
}
auto numberOfCreatedPlaceholders = 0ul;
const qint64 result = CfCreatePlaceholders(stdWStringBasePath.data(), cloudEntry.get(), filteredItemsInfo.size(), CF_CREATE_FLAG_NONE, &numberOfCreatedPlaceholders);
if (result != S_OK) {
qCWarning(lcCfApiWrapper) << "Couldn't create placeholders info" << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage()) << "number of placeholders created:" << numberOfCreatedPlaceholders;
for(auto itemIndice = 0; itemIndice < filteredItemsInfo.size(); ++itemIndice) {
qCDebug(lcCfApiWrapper) << QString::fromStdWString(cloudEntry[itemIndice].RelativeFileName) << QString::fromWCharArray(_com_error(cloudEntry[itemIndice].Result).ErrorMessage());
}
return { "Couldn't create placeholder info" };
}
for(auto itemIndice = 0; itemIndice < filteredItemsInfo.size(); ++itemIndice) {
const auto &placeholderInfo = filteredItemsInfo[itemIndice];
const auto parentInfo = findPlaceholderInfo(QDir::toNativeSeparators(QFileInfo(localBasePath + QDir::separator() + placeholderInfo.relativePath).absolutePath()));
const auto state = parentInfo && parentInfo->PinState == CF_PIN_STATE_UNPINNED ? CF_PIN_STATE_UNPINNED : CF_PIN_STATE_INHERIT;
if (!setPinState(QDir::toNativeSeparators(QFileInfo(localBasePath + QDir::separator() + placeholderInfo.relativePath).absoluteFilePath()), cfPinStateToPinState(state), NoRecurse)) {
return { "Couldn't set the default inherit pin state" };
}
}
return {};
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::updatePlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
{
return updatePlaceholderState(path, modtime, size, fileId, replacesPath, CfApiUpdateMetadataType::AllMetadata);

View File

@ -15,6 +15,8 @@
#include "common/result.h"
#include "common/vfs.h"
#include <QFileInfo>
#include <memory>
struct CF_PLACEHOLDER_BASIC_INFO;
@ -90,6 +92,17 @@ enum SetPinRecurseMode {
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> setPinState(const QString &path, PinState state, SetPinRecurseMode mode);
NEXTCLOUD_CFAPI_EXPORT Result<void, QString> createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);
struct PlaceholdersInfo {
QFileInfo fileInfo;
QString relativePath;
std::wstring platformNativeRelativePath;
QByteArray fileId;
time_t modtime;
qint64 size;
};
NEXTCLOUD_CFAPI_EXPORT Result<void, QString> createPlaceholdersInfo(const QString &localBasePath, const QList<PlaceholdersInfo> &itemsInfo);
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath = QString());
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> dehydratePlaceholder(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);

View File

@ -211,6 +211,22 @@ Result<void, QString> VfsCfApi::createPlaceholder(const SyncFileItem &item)
return result;
}
Result<void, QString> VfsCfApi::createPlaceholders(const QList<SyncFileItemPtr> &items)
{
const auto fileInfo = QFileInfo(_setupParams.filesystemPath + items[0]->_file);
const auto localPath = QDir::toNativeSeparators(fileInfo.absolutePath());
auto allPlaceholdersInfo = QList<cfapi::PlaceholdersInfo>{};
for (const auto &oneItem : items) {
auto fileInfo = QFileInfo(_setupParams.filesystemPath + oneItem->_file);
allPlaceholdersInfo.emplace_back(std::move(fileInfo), fileInfo.fileName(), QDir::toNativeSeparators(fileInfo.fileName()).toStdWString(), oneItem->_fileId, oneItem->_modtime, oneItem->_size);
}
const auto result = cfapi::createPlaceholdersInfo(localPath, {allPlaceholdersInfo});
return result;
}
Result<void, QString> VfsCfApi::dehydratePlaceholder(const SyncFileItem &item)
{
const auto localPath = QDir::toNativeSeparators(_setupParams.filesystemPath + item._file);
@ -534,7 +550,7 @@ VfsCfApi::HydratationAndPinStates VfsCfApi::computeRecursiveHydrationAndPinState
const auto dir = QDir(info.absoluteFilePath());
Q_ASSERT(dir.exists());
const auto children = dir.entryList();
return std::accumulate(std::cbegin(children), std::cend(children), dirState, [=](const HydratationAndPinStates &currentState, const QString &name) {
return std::accumulate(std::cbegin(children), std::cend(children), dirState, [=, this](const HydratationAndPinStates &currentState, const QString &name) {
if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
return currentState;
}

View File

@ -39,6 +39,8 @@ public:
[[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override;
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
Result<void, QString> createPlaceholders(const QList<SyncFileItemPtr> &items) override;
Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) override;
Result<Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile, UpdateMetadataTypes updateType) override;

View File

@ -108,6 +108,21 @@ Result<void, QString> VfsSuffix::createPlaceholder(const SyncFileItem &item)
return {};
}
Result<void, QString> VfsSuffix::createPlaceholders(const QList<SyncFileItemPtr> &items)
{
auto result = Result<void, QString>{};
for (const auto &oneItem : items) {
const auto itemResult = createPlaceholder(*oneItem);
if (!itemResult) {
result = itemResult;
break;
}
}
return result;
}
Result<void, QString> VfsSuffix::dehydratePlaceholder(const SyncFileItem &item)
{
SyncFileItem virtualItem(item);

View File

@ -35,6 +35,8 @@ public:
[[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; }
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
Result<void, QString> createPlaceholders(const QList<SyncFileItemPtr> &items) override;
Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) override;
Result<Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &, UpdateMetadataTypes updateType) override;

View File

@ -97,6 +97,21 @@ Result<void, QString> VfsXAttr::createPlaceholder(const SyncFileItem &item)
return xattr::addNextcloudPlaceholderAttributes(path);
}
Result<void, QString> VfsXAttr::createPlaceholders(const QList<SyncFileItemPtr> &items)
{
auto result = Result<void, QString>{};
for (const auto &oneItem : items) {
const auto itemResult = createPlaceholder(*oneItem);
if (!itemResult) {
result = itemResult;
break;
}
}
return result;
}
Result<void, QString> VfsXAttr::dehydratePlaceholder(const SyncFileItem &item)
{
const auto path = QString(_setupParams.filesystemPath + item._file);

View File

@ -34,6 +34,8 @@ public:
[[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; }
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
Result<void, QString> createPlaceholders(const QList<SyncFileItemPtr> &items) override;
Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) override;
Result<ConvertToPlaceholderResult, QString> convertToPlaceholder(const QString &filename,
const SyncFileItem &item,

View File

@ -1517,6 +1517,27 @@ private slots:
QVERIFY(fakeFolder.currentRemoteState().find("S/A/a2m"));
QVERIFY(fakeFolder.currentRemoteState().find("S/B/b2m"));
}
void createFolderAndFiles()
{
FakeFolder fakeFolder {FileInfo{}};
auto vfs = setupVfs(fakeFolder);
fakeFolder.remoteModifier().mkdir("first folder");
QVERIFY(fakeFolder.syncOnce());
fakeFolder.remoteModifier().insert("first folder/file1");
fakeFolder.remoteModifier().insert("first folder/file2");
fakeFolder.remoteModifier().insert("first folder/file3");
fakeFolder.remoteModifier().mkdir("first folder/second folder");
fakeFolder.remoteModifier().insert("first folder/second folder/second file1");
fakeFolder.remoteModifier().insert("first folder/second folder/second file2");
fakeFolder.remoteModifier().insert("first folder/second folder/second file3");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
}
};
QTEST_GUILESS_MAIN(TestSyncCfApi)