diff --git a/.gitignore b/.gitignore index 85a87ed91464e..c8b8ee783160b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ bld/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +# clangd cache directory +.cache/ + #NUNIT *.VisualState.xml TestResult.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index ee8bc487a8b14..8b5b43e0407d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ if (CLANG_TIDY_EXE) set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables) endif() +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + project(client) include(FeatureSummary) diff --git a/src/common/checksumcalculator.cpp b/src/common/checksumcalculator.cpp index ec9faa2de8c5a..49edabee0f465 100644 --- a/src/common/checksumcalculator.cpp +++ b/src/common/checksumcalculator.cpp @@ -12,11 +12,14 @@ * for more details. */ #include "checksumcalculator.h" +#include "filesystembase.h" #include +#include #include #include +#include #include namespace @@ -48,7 +51,7 @@ static QCryptographicHash::Algorithm algorithmTypeToQCryptoHashAlgorithm(Checksu } ChecksumCalculator::ChecksumCalculator(const QString &filePath, const QByteArray &checksumTypeName) - : _device(new QFile(filePath)) + : _device(openFile(filePath)) { if (checksumTypeName == checkSumMD5C) { _algorithmType = AlgorithmType::MD5; @@ -140,6 +143,18 @@ QByteArray ChecksumCalculator::calculate() return result; } +QScopedPointer ChecksumCalculator::openFile(const QString &filePath) +{ + if (QFileInfo(filePath).isSymLink()) { + auto symlinkContent = FileSystem::readlink(filePath); + QScopedPointer symlinkDevice(new QBuffer()); + symlinkDevice->setData(symlinkContent); + return QScopedPointer(symlinkDevice.take()); + } else { + return QScopedPointer(new QFile(filePath)); + } +} + void ChecksumCalculator::initChecksumAlgorithm() { if (_algorithmType == AlgorithmType::Undefined) { diff --git a/src/common/checksumcalculator.h b/src/common/checksumcalculator.h index da1263abeee57..a7f885f3c37d1 100644 --- a/src/common/checksumcalculator.h +++ b/src/common/checksumcalculator.h @@ -46,6 +46,7 @@ class OCSYNC_EXPORT ChecksumCalculator [[nodiscard]] QByteArray calculate(); private: + static QScopedPointer openFile(const QString &filePath); void initChecksumAlgorithm(); bool addChunk(const QByteArray &chunk, const qint64 size); QScopedPointer _device; diff --git a/src/common/filesystembase.cpp b/src/common/filesystembase.cpp index ef9ba1efd6778..f981081806110 100644 --- a/src/common/filesystembase.cpp +++ b/src/common/filesystembase.cpp @@ -20,6 +20,8 @@ #include "utility.h" #include "common/asserts.h" +#include + #include #include #include @@ -127,6 +129,15 @@ bool FileSystem::setFileReadOnlyWeak(const QString &filename, bool readonly) return true; } +QByteArray FileSystem::readlink(const QString &filename) +{ + if (!QFileInfo(filename).isSymLink()) { + return QByteArray(); + } + auto symlinkContent = std::filesystem::read_symlink(filename.toStdString()).string(); + return QByteArray(symlinkContent.data()); +} + bool FileSystem::rename(const QString &originFileName, const QString &destinationFileName, QString *errorString) @@ -312,13 +323,13 @@ bool FileSystem::fileExists(const QString &filename, const QFileInfo &fileInfo) return fileExistsWin(filename); } #endif - bool re = fileInfo.exists(); + bool re = fileInfo.exists() || fileInfo.isSymLink(); // if the filename is different from the filename in fileInfo, the fileInfo is // not valid. There needs to be one initialised here. Otherwise the incoming // fileInfo is re-used. if (fileInfo.filePath() != filename) { QFileInfo myFI(filename); - re = myFI.exists(); + re = myFI.exists() || myFI.isSymLink(); } return re; } diff --git a/src/common/filesystembase.h b/src/common/filesystembase.h index fd0572804dcb2..323aa5e48e2d8 100644 --- a/src/common/filesystembase.h +++ b/src/common/filesystembase.h @@ -84,6 +84,16 @@ namespace FileSystem { */ bool OCSYNC_EXPORT fileExists(const QString &filename, const QFileInfo & = QFileInfo()); + /** + * @brief Return raw content of symlink at given path. + * + * If the file is not a symlink or does not exist, the returned string will be empty. + * In Qt6.6+, QFileInfo::readSymLink() can be used instead. + * QFileInfo::symLinkTarget() can *not* be used because it transforms the target to an + * absolute path which might break relative symlinks for cross-device synchronization. + */ + QByteArray OCSYNC_EXPORT readlink(const QString &filename); + /** * @brief Rename the file \a originFileName to \a destinationFileName. * diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index 7270fac137962..cf292b7bba6d5 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -65,6 +65,7 @@ class OCSYNC_EXPORT SyncJournalFileRecord [[nodiscard]] QDateTime modDateTime() const { return Utility::qDateTimeFromTime_t(_modtime); } [[nodiscard]] bool isDirectory() const { return _type == ItemTypeDirectory; } + [[nodiscard]] bool isSymLink() const { return _type == ItemTypeSoftLink; } [[nodiscard]] bool isFile() const { return _type == ItemTypeFile || _type == ItemTypeVirtualFileDehydration; } [[nodiscard]] bool isVirtualFile() const { return _type == ItemTypeVirtualFile || _type == ItemTypeVirtualFileDownload; } [[nodiscard]] QString path() const { return QString::fromUtf8(_path); } diff --git a/src/csync/std/c_time.cpp b/src/csync/std/c_time.cpp index b51b5a2d1a0cb..75ce65b936089 100644 --- a/src/csync/std/c_time.cpp +++ b/src/csync/std/c_time.cpp @@ -25,8 +25,8 @@ #include #ifdef HAVE_UTIMES -int c_utimes(const QString &uri, const struct timeval *times) { - int ret = utimes(QFile::encodeName(uri).constData(), times); +int c_utimes(const QString &uri, const struct timespec *times) { + int ret = utimensat(AT_FDCWD, QFile::encodeName(uri).constData(), times, AT_SYMLINK_NOFOLLOW); return ret; } #else // HAVE_UTIMES @@ -39,15 +39,15 @@ int c_utimes(const QString &uri, const struct timeval *times) { #define CSYNC_SECONDS_SINCE_1601 11644473600LL #define CSYNC_USEC_IN_SEC 1000000LL //after Microsoft KB167296 -static void UnixTimevalToFileTime(struct timeval t, LPFILETIME pft) +static void UnixTimevalToFileTime(struct timespec t, LPFILETIME pft) { LONGLONG ll = 0; - ll = Int32x32To64(t.tv_sec, CSYNC_USEC_IN_SEC*10) + t.tv_usec*10 + CSYNC_SECONDS_SINCE_1601*CSYNC_USEC_IN_SEC*10; + ll = Int32x32To64(t.tv_sec, CSYNC_USEC_IN_SEC*10) + t.tv_nsec/100 + CSYNC_SECONDS_SINCE_1601*CSYNC_USEC_IN_SEC*10; pft->dwLowDateTime = (DWORD)ll; pft->dwHighDateTime = ll >> 32; } -int c_utimes(const QString &uri, const struct timeval *times) { +int c_utimes(const QString &uri, const struct timespec *times) { FILETIME LastAccessTime; FILETIME LastModificationTime; HANDLE hFile = nullptr; diff --git a/src/csync/std/c_time.h b/src/csync/std/c_time.h index 55a6aa6bc836f..b09da715f7fdf 100644 --- a/src/csync/std/c_time.h +++ b/src/csync/std/c_time.h @@ -31,7 +31,7 @@ #include #endif -OCSYNC_EXPORT int c_utimes(const QString &uri, const struct timeval *times); +OCSYNC_EXPORT int c_utimes(const QString &uri, const struct timespec *times); #endif /* _C_TIME_H */ diff --git a/src/gui/conflictdialog.cpp b/src/gui/conflictdialog.cpp index bae7b190f957a..a96c44836d1a9 100644 --- a/src/gui/conflictdialog.cpp +++ b/src/gui/conflictdialog.cpp @@ -16,6 +16,7 @@ #include "ui_conflictdialog.h" #include "conflictsolver.h" +#include "filesystem.h" #include #include @@ -132,9 +133,10 @@ void ConflictDialog::updateWidgets() const auto fileUrl = QUrl::fromLocalFile(filename).toString(); linkLabel->setText(QStringLiteral("%2").arg(fileUrl).arg(linkText)); - const auto info = QFileInfo(filename); - mtimeLabel->setText(info.lastModified().toString()); - sizeLabel->setText(locale().formattedDataSize(info.size())); + const auto lastModified = QDateTime::fromTime_t(FileSystem::getModTime(filename)); + const auto fileSize = FileSystem::getSize(filename); + mtimeLabel->setText(lastModified.toString()); + sizeLabel->setText(locale().formattedDataSize(fileSize)); const auto mime = mimeDb.mimeTypeForFile(filename); if (QIcon::hasThemeIcon(mime.iconName())) { diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 21f3423460326..7bbf2e67e0838 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1096,6 +1096,7 @@ SyncOptions Folder::initializeSyncOptions() const auto newFolderLimit = cfgFile.newBigFolderSizeLimit(); opt._newBigFolderSizeLimit = newFolderLimit.first ? newFolderLimit.second * 1000LL * 1000LL : -1; // convert from MB to B opt._confirmExternalStorage = cfgFile.confirmExternalStorage(); + opt._synchronizeSymlinks = cfgFile.synchronizeSymlinks(); opt._moveFilesToTrash = cfgFile.moveToTrash(); opt._vfs = _vfs; opt._parallelNetworkJobs = _accountState->account()->isHttp2Supported() ? 20 : 6; diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index 114aab7cd9dd8..00ed6f6e50393 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -191,6 +191,7 @@ GeneralSettings::GeneralSettings(QWidget *parent) connect(_ui->existingFolderLimitCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->stopExistingFolderNowBigSyncCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->newExternalStorage, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); + connect(_ui->synchronizeSymlinks, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->moveFilesToTrashCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); #ifndef WITH_CRASHREPORTER @@ -259,6 +260,7 @@ void GeneralSettings::loadMiscSettings() _ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane()); _ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter()); _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); + _ui->synchronizeSymlinks->setChecked(cfgFile.synchronizeSymlinks()); _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); _ui->moveFilesToTrashCheckBox->setChecked(cfgFile.moveToTrash()); @@ -270,6 +272,7 @@ void GeneralSettings::loadMiscSettings() _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(_ui->existingFolderLimitCheckBox->isChecked()); _ui->stopExistingFolderNowBigSyncCheckBox->setChecked(_ui->existingFolderLimitCheckBox->isChecked() && cfgFile.stopSyncingExistingFoldersOverLimit()); _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); + _ui->synchronizeSymlinks->setChecked(cfgFile.synchronizeSymlinks()); _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); } @@ -447,6 +450,7 @@ void GeneralSettings::saveMiscSettings() cfgFile.setMoveToTrash(_ui->moveFilesToTrashCheckBox->isChecked()); cfgFile.setNewBigFolderSizeLimit(newFolderLimitEnabled, _ui->newFolderLimitSpinBox->value()); cfgFile.setConfirmExternalStorage(_ui->newExternalStorage->isChecked()); + cfgFile.setSynchronizeSymlinks(_ui->synchronizeSymlinks->isChecked()); cfgFile.setNotifyExistingFoldersOverLimit(existingFolderLimitEnabled); cfgFile.setStopSyncingExistingFoldersOverLimit(stopSyncingExistingFoldersOverLimit); diff --git a/src/gui/generalsettings.ui b/src/gui/generalsettings.ui index 0bbb5483dc6dc..4f4d60abfe16c 100644 --- a/src/gui/generalsettings.ui +++ b/src/gui/generalsettings.ui @@ -139,6 +139,17 @@ + + + + + + [experimental] Synchronize symlinks + + + + + diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 09336e0c8a443..86d0aa1a4217a 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -613,7 +613,7 @@ void ActivityListModel::addIgnoredFileToList(const Activity &newActivity) bool duplicate = false; if (_listOfIgnoredFiles.size() == 0) { _notificationIgnoredFiles = newActivity; - _notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced."); + _notificationIgnoredFiles._subject = tr("Files from the ignore list are not synced."); addEntriesToActivityList({_notificationIgnoredFiles}); _listOfIgnoredFiles.append(newActivity); return; diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index fab99be586e3c..7f6c53612b1cd 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -85,6 +85,8 @@ set(libsync_SRCS propagateuploadencrypted.cpp propagatedownloadencrypted.h propagatedownloadencrypted.cpp + symlinkuploaddevice.h + symlinkuploaddevice.cpp syncengine.h syncengine.cpp syncfileitem.h diff --git a/src/libsync/bulkpropagatorjob.cpp b/src/libsync/bulkpropagatorjob.cpp index 0f0a7db3fcfa4..02581954ca40a 100644 --- a/src/libsync/bulkpropagatorjob.cpp +++ b/src/libsync/bulkpropagatorjob.cpp @@ -16,6 +16,7 @@ #include "putmultifilejob.h" #include "owncloudpropagator_p.h" +#include "symlinkuploaddevice.h" #include "syncfileitem.h" #include "syncengine.h" #include "propagateupload.h" @@ -203,10 +204,18 @@ void BulkPropagatorJob::triggerUpload() int timeout = 0; for(auto &singleFile : _filesToUpload) { // job takes ownership of device via a QScopedPointer. Job deletes itself when finishing - auto device = std::make_unique(singleFile._localPath, + std::unique_ptr device; + if (singleFile._item->isSymLink()) { + device = std::make_unique(singleFile._localPath, + 0, + std::numeric_limits::max(), + &propagator()->_bandwidthManager); + } else { + device = std::make_unique(singleFile._localPath, 0, singleFile._fileSize, &propagator()->_bandwidthManager); + } if (!device->open(QIODevice::ReadOnly)) { qCWarning(lcBulkPropagatorJob) << "Could not prepare upload device: " << device->errorString(); @@ -224,6 +233,7 @@ void BulkPropagatorJob::triggerUpload() } singleFile._headers["X-File-Path"] = singleFile._remotePath.toUtf8(); + singleFile._headers["OC-File-Type"] = QString::number(singleFile._item->_type).toUtf8(); uploadParametersData.push_back({std::move(device), singleFile._headers}); timeout += singleFile._fileSize; } diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index 60cb2cc71aeba..d275abaaffa23 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -102,6 +102,7 @@ static constexpr char useNewBigFolderSizeLimitC[] = "useNewBigFolderSizeLimit"; static constexpr char notifyExistingFoldersOverLimitC[] = "notifyExistingFoldersOverLimit"; static constexpr char stopSyncingExistingFoldersOverLimitC[] = "stopSyncingExistingFoldersOverLimit"; static constexpr char confirmExternalStorageC[] = "confirmExternalStorage"; +static constexpr char synchronizeSymlinksC[] = "synchronizeSymlinks"; static constexpr char moveToTrashC[] = "moveToTrash"; static constexpr char certPath[] = "http_certificatePath"; @@ -947,6 +948,12 @@ bool ConfigFile::confirmExternalStorage() const return getPolicySetting(QLatin1String(confirmExternalStorageC), fallback).toBool(); } +bool ConfigFile::synchronizeSymlinks() const +{ + const auto fallback = getValue(synchronizeSymlinksC, QString(), false); + return getPolicySetting(QLatin1String(synchronizeSymlinksC), fallback).toBool(); +} + bool ConfigFile::useNewBigFolderSizeLimit() const { const auto fallback = getValue(useNewBigFolderSizeLimitC, QString(), true); @@ -981,6 +988,11 @@ void ConfigFile::setConfirmExternalStorage(bool isChecked) setValue(confirmExternalStorageC, isChecked); } +void ConfigFile::setSynchronizeSymlinks(bool isChecked) +{ + setValue(synchronizeSymlinksC, isChecked); +} + bool ConfigFile::moveToTrash() const { return getValue(moveToTrashC, QString(), false).toBool(); diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 21e58412afcc5..fa193d406e143 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -149,6 +149,9 @@ class OWNCLOUDSYNC_EXPORT ConfigFile [[nodiscard]] bool confirmExternalStorage() const; void setConfirmExternalStorage(bool); + [[nodiscard]] bool synchronizeSymlinks() const; + void setSynchronizeSymlinks(bool); + /** If we should move the files deleted on the server in the trash */ [[nodiscard]] bool moveToTrash() const; void setMoveToTrash(bool); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index de62a8075065b..177f8dd096539 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -39,16 +39,17 @@ namespace OCC { Q_LOGGING_CATEGORY(lcDisco, "nextcloud.sync.discovery", QtInfoMsg) -ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, qint64 lastSyncTimestamp, QObject *parent) +ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, qint64 lastSyncTimestamp, bool synchronizeSymlinks, QObject *parent) : QObject(parent) , _lastSyncTimestamp(lastSyncTimestamp) , _discoveryData(data) + , _synchronizeSymlinks(synchronizeSymlinks) { qCDebug(lcDisco) << data; computePinState(basePinState); } -ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, ProcessDirectoryJob *parent) +ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, bool synchronizeSymlinks, ProcessDirectoryJob *parent) : QObject(parent) , _dirItem(dirItem) , _lastSyncTimestamp(lastSyncTimestamp) @@ -56,18 +57,20 @@ ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileIt , _queryLocal(queryLocal) , _discoveryData(parent->_discoveryData) , _currentFolder(path) + , _synchronizeSymlinks(synchronizeSymlinks) { qCDebug(lcDisco) << path._server << queryServer << path._local << queryLocal << lastSyncTimestamp; computePinState(parent->_pinState); } -ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent) +ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, bool synchronizeSymlinks, QObject *parent) : QObject(parent) , _dirItem(dirItem) , _lastSyncTimestamp(lastSyncTimestamp) , _queryLocal(queryLocal) , _discoveryData(data) , _currentFolder(path) + , _synchronizeSymlinks(synchronizeSymlinks) { computePinState(basePinState); } @@ -306,7 +309,9 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent } } - if (excluded == CSYNC_NOT_EXCLUDED && !entries.localEntry.isSymLink) { + bool isSymlink = entries.localEntry.isSymLink || entries.serverEntry.isSymLink; + // All not excluded files except for symlinks if symlink synchronization is disabled + if (excluded == CSYNC_NOT_EXCLUDED && (!isSymlink || (_synchronizeSymlinks && isSymlink))) { return false; } else if (excluded == CSYNC_FILE_SILENTLY_EXCLUDED || excluded == CSYNC_FILE_EXCLUDE_AND_REMOVE) { emit _discoveryData->silentlyExcluded(path); @@ -326,9 +331,8 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent return true; } - if (entries.localEntry.isSymLink) { - /* Symbolic links are ignored. */ - item->_errorString = tr("Symbolic links are not supported in syncing."); + if (isSymlink && !_synchronizeSymlinks) { + item->_errorString = tr("Symbolic links synchronization is not enabled."); } else { switch (excluded) { case CSYNC_NOT_EXCLUDED: @@ -623,7 +627,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe; item->_sharedByMe = serverEntry.sharedByMe; item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); - item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile; + item->_type = serverEntry.isDirectory ? ItemTypeDirectory : + (serverEntry.isSymLink ? ItemTypeSoftLink : ItemTypeFile); item->_etag = serverEntry.etag; item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadCookies = serverEntry.directDownloadCookies; @@ -713,7 +718,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it _discoveryData->checkSelectiveSyncExistingFolder(path._server); } - if (serverEntry.isDirectory != dbEntry.isDirectory()) { + if (serverEntry.isSymLink != dbEntry.isSymLink() || serverEntry.isDirectory != dbEntry.isDirectory()) { // If the type of the entity changed, it's like NEW, but // needs to delete the other entity first. item->_instruction = CSYNC_INSTRUCTION_TYPE_CHANGE; @@ -1070,8 +1075,18 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_inode = localEntry.inode; + + auto getItemType = [](const LocalInfo& localEntry, bool allowVirtualFile = false) { + return localEntry.isDirectory ? ItemTypeDirectory : + localEntry.isSymLink ? ItemTypeSoftLink : + localEntry.isVirtualFile && allowVirtualFile ? ItemTypeVirtualFile : + ItemTypeFile; + }; + + if (dbEntry.isValid()) { - bool typeChange = localEntry.isDirectory != dbEntry.isDirectory(); + bool typeChange = localEntry.isDirectory != dbEntry.isDirectory() || + localEntry.isSymLink != dbEntry.isSymLink(); if (!typeChange && localEntry.isVirtualFile) { if (noServerEntry) { item->_instruction = CSYNC_INSTRUCTION_REMOVE; @@ -1141,7 +1156,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_checksumHeader.clear(); item->_size = localEntry.size; item->_modtime = localEntry.modtime; - item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile; + item->_type = getItemType(localEntry); _childModified = true; } else if (dbEntry._modtime > 0 && (localEntry.modtime <= 0 || localEntry.modtime >= 0xFFFFFFFF) && dbEntry._fileSize == localEntry.size) { item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA; @@ -1149,7 +1164,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_size = localEntry.size > 0 ? localEntry.size : dbEntry._fileSize; item->_modtime = dbEntry._modtime; item->_previousModtime = dbEntry._modtime; - item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile; + item->_type = getItemType(localEntry); qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: File" << item->_file << "if (dbEntry._modtime > 0 && localEntry.modtime <= 0)" << "dbEntry._modtime:" << dbEntry._modtime << "localEntry.modtime:" << localEntry.modtime; @@ -1211,7 +1226,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_checksumHeader.clear(); item->_size = localEntry.size; item->_modtime = localEntry.modtime; - item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile; + item->_type = getItemType(localEntry, true); _childModified = true; if (!localEntry.caseClashConflictingName.isEmpty()) { @@ -1324,7 +1339,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( return false; } - if (base.isDirectory() != item->isDirectory()) { + if (item->_type != base._type) { qCInfo(lcDisco) << "Not a move, types don't match" << base._type << item->_type << localEntry.type; return false; } @@ -1645,7 +1660,7 @@ void ProcessDirectoryJob::processFileFinalize( } if (recurse) { auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer, - _lastSyncTimestamp, this); + _lastSyncTimestamp, _synchronizeSymlinks, this); job->setInsideEncryptedTree(isInsideEncryptedTree() || item->isEncrypted()); if (removed) { job->setParent(_discoveryData); @@ -1689,7 +1704,7 @@ void ProcessDirectoryJob::processBlacklisted(const PathTuple &path, const OCC::L qCInfo(lcDisco) << "Discovered (blacklisted) " << item->_file << item->_instruction << item->_direction << item->isDirectory(); if (item->isDirectory() && item->_instruction != CSYNC_INSTRUCTION_IGNORE) { - auto job = new ProcessDirectoryJob(path, item, NormalQuery, InBlackList, _lastSyncTimestamp, this); + auto job = new ProcessDirectoryJob(path, item, NormalQuery, InBlackList, _lastSyncTimestamp, _synchronizeSymlinks, this); connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished); _queuedJobs.push_back(job); } else { diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index eaa2657697a91..78116bda6fb13 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -101,15 +101,15 @@ class ProcessDirectoryJob : public QObject * The base pin state is used if the root dir's pin state can't be retrieved. */ explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, - qint64 lastSyncTimestamp, QObject *parent); + qint64 lastSyncTimestamp, bool synchronizeSymlinks, QObject *parent); /// For creating subjobs explicit ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem, - QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, + QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, bool synchronizeSymlinks, ProcessDirectoryJob *parent); explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, - QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent); + QueryMode queryLocal, qint64 lastSyncTimestamp, bool synchronizeSymlinks, QObject *parent); void start(); /** Start up to nbJobs, return the number of job started; emit finished() when done */ @@ -296,6 +296,7 @@ class ProcessDirectoryJob : public QObject bool _childIgnored = false; // The directory contains ignored item that would prevent deletion PinState _pinState = PinState::Unspecified; // The directory's pin-state, see computePinState() bool _isInsideEncryptedTree = false; // this directory is encrypted or is within the tree of directories with root directory encrypted + const bool _synchronizeSymlinks = false; signals: void finished(); diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 77a10b4fe244e..b6012a7abf167 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -16,6 +16,7 @@ #include "common/utility.h" #include "configfile.h" #include "discovery.h" +#include "filesystem.h" #include "helpers.h" #include "progressdispatcher.h" @@ -335,7 +336,7 @@ void DiscoverySingleLocalDirectoryJob::run() { continue; } i.modtime = dirent->modtime; - i.size = dirent->size; + i.size = FileSystem::getSize(localPath + '/' + dirent->path); i.inode = dirent->inode; i.isDirectory = dirent->type == ItemTypeDirectory; i.isHidden = dirent->is_hidden; @@ -442,6 +443,7 @@ static void propertyMapToRemoteInfo(const QMap &map, RemoteInf QString value = it.value(); if (property == QLatin1String("resourcetype")) { result.isDirectory = value.contains(QLatin1String("collection")); + result.isSymLink = value.contains(QLatin1String("symlink")); } else if (property == QLatin1String("getlastmodified")) { const auto date = QDateTime::fromString(value, Qt::RFC2822Date); Q_ASSERT(date.isValid()); diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 561e9ee9f08d6..b4b74d9102d10 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -68,6 +68,7 @@ struct RemoteInfo int64_t size = 0; int64_t sizeOfFolder = 0; bool isDirectory = false; + bool isSymLink = false; bool _isE2eEncrypted = false; bool isFileDropDetected = false; QString e2eMangledName; diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index 7c58c7b2bde45..042da06df18dc 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -30,6 +30,11 @@ namespace OCC { bool FileSystem::fileEquals(const QString &fn1, const QString &fn2) { // compare two files with given filename and return true if they have the same content + auto symlinkTargetFile1 = readlink(fn1); + auto symlinkTargetFile2 = readlink(fn2); + if (!symlinkTargetFile1.isEmpty() && symlinkTargetFile1 == symlinkTargetFile2) { + return true; + } QFile f1(fn1); QFile f2(fn2); if (!f1.open(QIODevice::ReadOnly) || !f2.open(QIODevice::ReadOnly)) { @@ -71,9 +76,9 @@ time_t FileSystem::getModTime(const QString &filename) bool FileSystem::setModTime(const QString &filename, time_t modTime) { - struct timeval times[2]; + struct timespec times[2]; times[0].tv_sec = times[1].tv_sec = modTime; - times[0].tv_usec = times[1].tv_usec = 0; + times[0].tv_nsec = times[1].tv_nsec = 0; int rc = c_utimes(filename, times); if (rc != 0) { qCWarning(lcFileSystem) << "Error setting mtime for" << filename @@ -128,7 +133,11 @@ qint64 FileSystem::getSize(const QString &filename) return getSizeWithCsync(filename); } #endif - return QFileInfo(filename).size(); + QFileInfo info(filename); + if (info.isSymLink()) { + return readlink(filename).size(); + } + return info.size(); } // Code inspired from Qt5's QDir::removeRecursively diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 4745b4f846fbe..1527234777f5d 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -364,9 +364,13 @@ PropagateItemJob *OwncloudPropagator::createJob(const SyncFileItemPtr &item) } //fall through case CSYNC_INSTRUCTION_SYNC: if (item->_direction != SyncFileItem::Up) { - auto job = new PropagateDownloadFile(this, item); - job->setDeleteExistingFolder(deleteExisting); - return job; + if (item->_type != ItemTypeSoftLink || _syncOptions._synchronizeSymlinks) { + auto job = new PropagateDownloadFile(this, item); + job->setDeleteExistingFolder(deleteExisting); + return job; + } else { + return nullptr; + } } else { if (deleteExisting || !isDelayedUploadItem(item)) { auto job = createUploadJob(item, deleteExisting); @@ -531,13 +535,13 @@ void OwncloudPropagator::start(SyncFileItemVector &&items) _rootJob.reset(new PropagateRootDirectory(this)); QStack> directories; directories.push(qMakePair(QString(), _rootJob.data())); - QVector directoriesToRemove; + QVector remoteItemsToRemove; QString removedDirectory; QString maybeConflictDirectory; foreach (const SyncFileItemPtr &item, items) { if (!removedDirectory.isEmpty() && item->_file.startsWith(removedDirectory)) { // this is an item in a directory which is going to be removed. - auto *delDirJob = qobject_cast(directoriesToRemove.first()); + auto *delDirJob = qobject_cast(remoteItemsToRemove.first()); const auto isNewDirectory = item->isDirectory() && (item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE); @@ -585,19 +589,19 @@ void OwncloudPropagator::start(SyncFileItemVector &&items) if (item->isDirectory()) { startDirectoryPropagation(item, directories, - directoriesToRemove, + remoteItemsToRemove, removedDirectory, items); } else if (!directories.top().second->_item->_isFileDropDetected) { startFilePropagation(item, directories, - directoriesToRemove, + remoteItemsToRemove, removedDirectory, maybeConflictDirectory); } } - foreach (PropagatorJob *it, directoriesToRemove) { + foreach (PropagatorJob *it, remoteItemsToRemove) { _rootJob->appendDirDeletionJob(it); } @@ -609,7 +613,7 @@ void OwncloudPropagator::start(SyncFileItemVector &&items) void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, QStack> &directories, - QVector &directoriesToRemove, + QVector &remoteItemsToRemove, QString &removedDirectory, const SyncFileItemVector &items) { @@ -633,7 +637,7 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) { // We do the removal of directories at the end, because there might be moves from // these directories that will happen later. - directoriesToRemove.prepend(directoryPropagationJob.get()); + remoteItemsToRemove.prepend(directoryPropagationJob.get()); removedDirectory = item->_file + "/"; // We should not update the etag of parent directories of the removed directory @@ -676,15 +680,16 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, void OwncloudPropagator::startFilePropagation(const SyncFileItemPtr &item, QStack > &directories, - QVector &directoriesToRemove, + QVector &remoteItemsToRemove, QString &removedDirectory, QString &maybeConflictDirectory) { if (item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) { - // will delete directories, so defer execution + // will delete previous remote item (since directories cannot be overwritten), + // so defer execution auto job = createJob(item); if (job) { - directoriesToRemove.prepend(job); + remoteItemsToRemove.prepend(job); } removedDirectory = item->_file + "/"; } else { diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index 440b98551e51f..3921785699498 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -437,13 +437,13 @@ class OWNCLOUDSYNC_EXPORT OwncloudPropagator : public QObject void startDirectoryPropagation(const SyncFileItemPtr &item, QStack> &directories, - QVector &directoriesToRemove, + QVector &remoteItemsToRemove, QString &removedDirectory, const SyncFileItemVector &items); void startFilePropagation(const SyncFileItemPtr &item, QStack> &directories, - QVector &directoriesToRemove, + QVector &remoteItemsToRemove, QString &removedDirectory, QString &maybeConflictDirectory); diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index 6d7d5e2ebee0b..f3553da7749f0 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef Q_OS_UNIX #include @@ -290,12 +291,29 @@ qint64 GETFileJob::writeToDevice(const QByteArray &data) return _device->write(data); } +bool GETFileJob::writeSymlink(const QString &symlinkTarget) +{ + auto file_device = qobject_cast(_device); + if (!file_device) { + qCWarning(lcGetJob) << "Unable to create symlink on given device!"; + return false; + } + file_device->close(); + FileSystem::remove(file_device->fileName()); + if (!QFile::link(symlinkTarget, file_device->fileName())) { + qCWarning(lcGetJob) << "Error creating symlink '" << file_device->fileName() << '\''; + return false; + } + return true; +} + void GETFileJob::slotReadyRead() { if (!reply()) return; int bufferSize = qMin(1024 * 8ll, reply()->bytesAvailable()); QByteArray buffer(bufferSize, Qt::Uninitialized); + std::optional symlinkTarget; while (reply()->bytesAvailable() > 0 && _saveBodyToFile) { if (_bandwidthChoked) { @@ -321,16 +339,30 @@ void GETFileJob::slotReadyRead() return; } - const qint64 writtenBytes = writeToDevice(QByteArray::fromRawData(buffer.constData(), readBytes)); - if (writtenBytes != readBytes) { - _errorString = _device->errorString(); - _errorStatus = SyncFileItem::NormalError; - qCWarning(lcGetJob) << "Error while writing to file" << writtenBytes << readBytes << _errorString; - reply()->abort(); - return; + if (reply()->hasRawHeader("OC-File-Type") && + reply()->rawHeader("OC-File-Type").toInt() == ItemTypeSoftLink) { + if (symlinkTarget) { + *symlinkTarget += buffer; + } else { + symlinkTarget = buffer; + } + } else { + const qint64 writtenBytes = writeToDevice(QByteArray::fromRawData(buffer.constData(), readBytes)); + if (writtenBytes != readBytes) { + _errorString = _device->errorString(); + _errorStatus = SyncFileItem::NormalError; + qCWarning(lcGetJob) << "Error while writing to file" << writtenBytes << readBytes << _errorString; + reply()->abort(); + return; + } } } + if (symlinkTarget && !writeSymlink(*symlinkTarget)) { + reply()->abort(); + return; + } + if (reply()->isFinished() && (reply()->bytesAvailable() == 0 || !_saveBodyToFile)) { qCDebug(lcGetJob) << "Actually finished!"; if (_bandwidthManager) { @@ -789,7 +821,7 @@ void PropagateDownloadFile::slotGetFinished() // Don't keep the temporary file if it is empty or we // used a bad range header or the file's not on the server anymore. - if (_tmpFile.exists() && (_tmpFile.size() == 0 || badRangeHeader || fileNotFound)) { + if (!QFileInfo(_tmpFile).isSymLink() && _tmpFile.exists() && (_tmpFile.size() == 0 || badRangeHeader || fileNotFound)) { _tmpFile.close(); FileSystem::remove(_tmpFile.fileName()); propagator()->_journal->setDownloadInfo(_item->_file, SyncJournalDb::DownloadInfo()); @@ -885,19 +917,21 @@ void PropagateDownloadFile::slotGetFinished() return; } - if (bodySize > 0 && bodySize != _tmpFile.size() - job->resumeStart()) { - qCDebug(lcPropagateDownload) << bodySize << _tmpFile.size() << job->resumeStart(); - propagator()->_anotherSyncNeeded = true; - done(SyncFileItem::SoftError, tr("The file could not be downloaded completely."), ErrorCategory::GenericError); - return; - } + if (!QFileInfo(_tmpFile).isSymLink()) { + if (bodySize > 0 && bodySize != _tmpFile.size() - job->resumeStart()) { + qCDebug(lcPropagateDownload) << bodySize << _tmpFile.size() << job->resumeStart(); + propagator()->_anotherSyncNeeded = true; + done(SyncFileItem::SoftError, tr("The file could not be downloaded completely."), ErrorCategory::GenericError); + return; + } - if (_tmpFile.size() == 0 && _item->_size > 0) { - FileSystem::remove(_tmpFile.fileName()); - done(SyncFileItem::NormalError, - tr("The downloaded file is empty, but the server said it should have been %1.") - .arg(Utility::octetsToString(_item->_size)), ErrorCategory::GenericError); - return; + if (_tmpFile.size() == 0 && _item->_size > 0) { + FileSystem::remove(_tmpFile.fileName()); + done(SyncFileItem::NormalError, + tr("The downloaded file is empty, but the server said it should have been %1.") + .arg(Utility::octetsToString(_item->_size)), ErrorCategory::GenericError); + return; + } } // Did the file come with conflict headers? If so, store them now! @@ -1074,7 +1108,8 @@ namespace { // Anonymous namespace for the recall feature static void preserveGroupOwnership(const QString &fileName, const QFileInfo &fi) { #ifdef Q_OS_UNIX - int chownErr = chown(fileName.toLocal8Bit().constData(), -1, fi.groupId()); + int chownErr = fchownat(AT_FDCWD, fileName.toLocal8Bit().constData(), -1, + fi.groupId(), AT_SYMLINK_NOFOLLOW); if (chownErr) { // TODO: Consider further error handling! qCWarning(lcPropagateDownload) << QString("preserveGroupOwnership: chown error %1: setting group %2 failed on file %3").arg(chownErr).arg(fi.groupId()).arg(fileName); @@ -1185,9 +1220,9 @@ void PropagateDownloadFile::downloadFinished() auto previousFileExists = FileSystem::fileExists(filename) && _item->_instruction != CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT; if (previousFileExists) { - // Preserve the existing file permissions. + // Preserve the existing file permissions (except it was a symlink) const auto existingFile = QFileInfo{filename}; - if (existingFile.permissions() != _tmpFile.permissions()) { + if (!existingFile.isSymLink() && existingFile.permissions() != _tmpFile.permissions()) { _tmpFile.setPermissions(existingFile.permissions()); } preserveGroupOwnership(_tmpFile.fileName(), existingFile); diff --git a/src/libsync/propagatedownload.h b/src/libsync/propagatedownload.h index 2a5fea962ce50..976a501261642 100644 --- a/src/libsync/propagatedownload.h +++ b/src/libsync/propagatedownload.h @@ -115,6 +115,7 @@ class OWNCLOUDSYNC_EXPORT GETFileJob : public AbstractNetworkJob protected: virtual qint64 writeToDevice(const QByteArray &data); + virtual bool writeSymlink(const QString &symlinkTarget); signals: void finishedSignal(); diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index f74727ec7bb80..8a7f1b82a481a 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -258,6 +258,7 @@ void PropagateUploadFileCommon::setupUnencryptedFile() _fileToUpload._file = _item->_file; _fileToUpload._size = _item->_size; _fileToUpload._path = propagator()->fullLocalPath(_fileToUpload._file); + _fileToUpload._isSymlink = _item->_type == ItemTypeSoftLink; startUploadFile(); } diff --git a/src/libsync/propagateupload.h b/src/libsync/propagateupload.h index 33db43f297755..dc32649f2486f 100644 --- a/src/libsync/propagateupload.h +++ b/src/libsync/propagateupload.h @@ -60,7 +60,7 @@ class UploadDevice : public QIODevice signals: -private: +protected: /// The local file to read data from QFile _file; @@ -233,6 +233,7 @@ class PropagateUploadFileCommon : public PropagateItemJob QString _file; /// I'm still unsure if I should use a SyncFilePtr here. QString _path; /// the full path on disk. qint64 _size = 0LL; + bool _isSymlink = false; }; UploadFileInfo _fileToUpload; QByteArray _transmissionChecksumHeader; diff --git a/src/libsync/propagateuploadv1.cpp b/src/libsync/propagateuploadv1.cpp index 33cd8892d7335..83b48036561bd 100644 --- a/src/libsync/propagateuploadv1.cpp +++ b/src/libsync/propagateuploadv1.cpp @@ -23,6 +23,7 @@ #include "filesystem.h" #include "propagatorjobs.h" #include "common/checksums.h" +#include "symlinkuploaddevice.h" #include "syncengine.h" #include "propagateremotedelete.h" #include "common/asserts.h" @@ -99,6 +100,7 @@ void PropagateUploadFileV1::startNextChunk() auto headers = PropagateUploadFileCommon::headers(); headers[QByteArrayLiteral("OC-Total-Length")] = QByteArray::number(fileSize); headers[QByteArrayLiteral("OC-Chunk-Size")] = QByteArray::number(chunkSize()); + headers[QByteArrayLiteral("OC-File-Type")] = QByteArray::number(_fileToUpload._isSymlink ? ItemTypeSoftLink : ItemTypeFile); QString path = _fileToUpload._file; @@ -135,8 +137,14 @@ void PropagateUploadFileV1::startNextChunk() } const QString fileName = _fileToUpload._path; - auto device = std::make_unique( - fileName, chunkStart, currentChunkSize, &propagator()->_bandwidthManager); + std::unique_ptr device; + if (_fileToUpload._isSymlink) { + device = std::make_unique( + fileName, chunkStart, currentChunkSize, &propagator()->_bandwidthManager); + } else { + device = std::make_unique( + fileName, chunkStart, currentChunkSize, &propagator()->_bandwidthManager); + } if (!device->open(QIODevice::ReadOnly)) { qCWarning(lcPropagateUploadV1) << "Could not prepare upload device: " << device->errorString(); diff --git a/src/libsync/symlinkuploaddevice.cpp b/src/libsync/symlinkuploaddevice.cpp new file mode 100644 index 0000000000000..c7612abcd207b --- /dev/null +++ b/src/libsync/symlinkuploaddevice.cpp @@ -0,0 +1,78 @@ +#include "symlinkuploaddevice.h" + +#include "filesystem.h" + +#include + +namespace OCC { +SymLinkUploadDevice::SymLinkUploadDevice(const QString &fileName, qint64 start, qint64 size, BandwidthManager *bwm) + : UploadDevice(fileName, start, size, bwm) +{ +} + +bool SymLinkUploadDevice::open(QIODevice::OpenMode mode) +{ + if (mode & QIODevice::WriteOnly) + return false; + + _symlinkContent = FileSystem::readlink(_file.fileName()); + if (_symlinkContent.isEmpty()) { + setErrorString("Unable to read symlink '" + _file.fileName() + "'"); + return false; + } + + _size = qBound(0ll, _size, _symlinkContent.size() - _start); + _read = 0; + + return QIODevice::open(mode); +} + +qint64 SymLinkUploadDevice::readData(char *data, qint64 maxlen) +{ + if (_size - _read <= 0) { + // at end + if (_bandwidthManager) { + _bandwidthManager->unregisterUploadDevice(this); + } + return -1; + } + maxlen = qMin(maxlen, _size - _read); + if (maxlen <= 0) { + return 0; + } + if (isChoked()) { + return 0; + } + if (isBandwidthLimited()) { + maxlen = qMin(maxlen, _bandwidthQuota); + if (maxlen <= 0) { // no quota + return 0; + } + _bandwidthQuota -= maxlen; + } + + auto readStart = _start + _read; + auto readEnd = _size; + if (_size - _read < maxlen) { + _read = _size; + } else { + _read += maxlen; + readEnd = _start + _read; + } + Q_ASSERT(readEnd <= _symlinkContent.size()); + std::copy(_symlinkContent.begin() + readStart, _symlinkContent.begin() + readEnd, data); + return readEnd - readStart; +} + +bool SymLinkUploadDevice::seek(qint64 pos) +{ + if (!QIODevice::seek(pos)) { + return false; + } + if (pos < 0 || pos > _size) { + return false; + } + _read = pos; + return true; +} +} diff --git a/src/libsync/symlinkuploaddevice.h b/src/libsync/symlinkuploaddevice.h new file mode 100644 index 0000000000000..c5c76e6be7d00 --- /dev/null +++ b/src/libsync/symlinkuploaddevice.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) by Tamino Bauknecht + * + * 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 "propagateupload.h" + +namespace OCC { + +/** + * @brief Symlink specialization of an UploadDevice + * @ingroup libsync + */ +class SymLinkUploadDevice : public UploadDevice +{ + Q_OBJECT + +public: + SymLinkUploadDevice(const QString &fileName, qint64 start, qint64 size, BandwidthManager *bwm); + + bool open(QIODevice::OpenMode mode) override; + + qint64 readData(char *data, qint64 maxlen) override; + bool seek(qint64 pos) override; + +protected: + QByteArray _symlinkContent; +}; +} diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index e4e76a7ff79b7..8cd1475a23240 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -717,6 +717,7 @@ void SyncEngine::startSync() singleItemDiscoveryOptions().discoveryDirItem, localQueryMode, _journal->keyValueStoreGetInt("last_sync", 0), + _syncOptions._synchronizeSymlinks, _discoveryPhase.data() ); } else { @@ -724,6 +725,7 @@ void SyncEngine::startSync() _discoveryPhase.data(), PinState::AlwaysLocal, _journal->keyValueStoreGetInt("last_sync", 0), + _syncOptions._synchronizeSymlinks, _discoveryPhase.data() ); } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 399546dcd0459..3777a4de42d71 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -205,6 +205,11 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem return _type == ItemTypeDirectory; } + [[nodiscard]] bool isSymLink() const + { + return _type == ItemTypeSoftLink; + } + /** * True if the item had any kind of error. */ diff --git a/src/libsync/syncoptions.h b/src/libsync/syncoptions.h index 092ac6fb6f1c6..d3e91b79c4340 100644 --- a/src/libsync/syncoptions.h +++ b/src/libsync/syncoptions.h @@ -42,6 +42,9 @@ class OWNCLOUDSYNC_EXPORT SyncOptions /** If a confirmation should be asked for external storages */ bool _confirmExternalStorage = false; + /** If symlinks should be synchronized to the server as symlinks */ + bool _synchronizeSymlinks = false; + /** If remotely deleted files are needed to move to trash */ bool _moveFilesToTrash = false;