Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sync unix symlinks as links #6205

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4f4fd60
gitignore: Ignore clangd-generated directory .cache/
taminob Oct 31, 2023
ed5fdd8
cmake: Export compile_commands.json by default
taminob Oct 31, 2023
5812d5c
libsync: Remove auto-exclude for symlinks
taminob Oct 31, 2023
40145c9
libsync/common: Store actual type for items
taminob Oct 31, 2023
0370594
libsync: Add header field OC-File-Type for symlink synchronization
taminob Oct 31, 2023
d66c704
libsync: Add SymLinkUploadDevice for reading symlinks
taminob Nov 2, 2023
c34659b
libsync: Use SymLinkUploadDevice for single symlink uploads
taminob Nov 2, 2023
7b92b2e
libsync: Add support for symlinks in bulk uploads
taminob Nov 4, 2023
390e8f6
libsync: Fix _read for SymLinkUploadDevice
taminob Nov 5, 2023
af53d58
libsync: Allow download of symlinks
taminob Nov 5, 2023
aee6601
libsync: Fix Content-Length for symlinks in bulk upload
taminob Nov 12, 2023
a551b24
common: Do not dereference symlink when checking existence
taminob Nov 15, 2023
7876341
libsync: Fix checksum and size for symlinks in bulk upload
taminob Nov 15, 2023
c5b8235
libsync: Add symlink type to DiscoveryPhase
taminob Nov 23, 2023
10d0fcc
common: Allow checksum calculation on QIODevice again
taminob Nov 27, 2023
e7c357c
libsync: Add readlink to FileSystem and fix getSize for symlinks
taminob Nov 27, 2023
719e014
BulkPropagatorJob: Remove symlink size special case
taminob Nov 28, 2023
8f05e93
FileSystem: Fix fileEquals for symlinks
taminob Nov 28, 2023
f5d6110
gui/config: Add symlink synchronization as sync option
taminob Nov 30, 2023
4866922
Revert "common: Allow checksum calculation on QIODevice again"
taminob Dec 2, 2023
6df006d
filesystem: Move readlink from libsync/filesystem.h to common/filesys…
taminob Dec 4, 2023
97f015c
ChecksumCalculator: Calculate symlink checksum on symlink target
taminob Dec 4, 2023
845e108
discoveryphase: Fix local entry size for symlinks
taminob Dec 4, 2023
7a6df0e
ChecksumCalculator: Fix creation of QBuffer
taminob Dec 4, 2023
81dbb9d
FileSystem: Make readlink C++20 compliant
taminob Dec 4, 2023
91a5e54
csync/FileSystem: Adjust c_utimes to not follow symlinks
taminob Dec 6, 2023
d5b61b2
discovery: Check if symlink for server entries
taminob Dec 6, 2023
d36b7b9
OwncloudPropagator: Fix condition for deletion of previous remote item
taminob Dec 6, 2023
5bcf57a
OwncloudPropagator: Only download symlink if enabled by option
taminob Dec 6, 2023
fa16c33
PropagateDownloadFile: Allow symlink to be larger than 8kB
taminob Dec 6, 2023
f779b3e
PropagateDownloadFile/FileSystem: Preserve permissions for symlinks
taminob Dec 8, 2023
5265ed4
PropagateDownloadFile/FileSystem: Improve previous commit (by mainly …
taminob Dec 8, 2023
c61a574
ConflictDialog: Fix size and mtime for symlinks in conflict resolution
taminob Dec 10, 2023
38f5dac
ActivityListModel: Modify ignore text to not include symlinks
taminob Dec 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ bld/
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# clangd cache directory
.cache/

#NUNIT
*.VisualState.xml
TestResult.xml
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion src/common/checksumcalculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
* for more details.
*/
#include "checksumcalculator.h"
#include "filesystembase.h"

#include <zlib.h>

#include <QBuffer>
#include <QCryptographicHash>
#include <QFile>
#include <QFileInfo>
#include <QLoggingCategory>

namespace
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -140,6 +143,18 @@ QByteArray ChecksumCalculator::calculate()
return result;
}

QScopedPointer<QIODevice> ChecksumCalculator::openFile(const QString &filePath)
{
if (QFileInfo(filePath).isSymLink()) {
auto symlinkContent = FileSystem::readlink(filePath);
QScopedPointer<QBuffer> symlinkDevice(new QBuffer());
symlinkDevice->setData(symlinkContent);
return QScopedPointer<QIODevice>(symlinkDevice.take());
} else {
return QScopedPointer<QIODevice>(new QFile(filePath));
}
}

void ChecksumCalculator::initChecksumAlgorithm()
{
if (_algorithmType == AlgorithmType::Undefined) {
Expand Down
1 change: 1 addition & 0 deletions src/common/checksumcalculator.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class OCSYNC_EXPORT ChecksumCalculator
[[nodiscard]] QByteArray calculate();

private:
static QScopedPointer<QIODevice> openFile(const QString &filePath);
void initChecksumAlgorithm();
bool addChunk(const QByteArray &chunk, const qint64 size);
QScopedPointer<QIODevice> _device;
Expand Down
15 changes: 13 additions & 2 deletions src/common/filesystembase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "utility.h"
#include "common/asserts.h"

#include <filesystem>

#include <QDateTime>
#include <QDir>
#include <QUrl>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down
10 changes: 10 additions & 0 deletions src/common/filesystembase.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions src/common/syncjournalfilerecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down
10 changes: 5 additions & 5 deletions src/csync/std/c_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
#include <QFile>

#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
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/csync/std/c_time.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
#include <sys/time.h>
#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 */
8 changes: 5 additions & 3 deletions src/gui/conflictdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "ui_conflictdialog.h"

#include "conflictsolver.h"
#include "filesystem.h"

#include <QDateTime>
#include <QDebug>
Expand Down Expand Up @@ -132,9 +133,10 @@ void ConflictDialog::updateWidgets()
const auto fileUrl = QUrl::fromLocalFile(filename).toString();
linkLabel->setText(QStringLiteral("<a href='%1'>%2</a>").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())) {
Expand Down
1 change: 1 addition & 0 deletions src/gui/folder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/gui/generalsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());

Expand All @@ -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());
}

Expand Down Expand Up @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions src/gui/generalsettings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QCheckBox" name="synchronizeSymlinks">
<property name="text">
<string>[experimental] Synchronize symlinks</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
Expand Down
2 changes: 1 addition & 1 deletion src/gui/tray/activitylistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/libsync/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ set(libsync_SRCS
propagateuploadencrypted.cpp
propagatedownloadencrypted.h
propagatedownloadencrypted.cpp
symlinkuploaddevice.h
symlinkuploaddevice.cpp
syncengine.h
syncengine.cpp
syncfileitem.h
Expand Down
12 changes: 11 additions & 1 deletion src/libsync/bulkpropagatorjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "putmultifilejob.h"
#include "owncloudpropagator_p.h"
#include "symlinkuploaddevice.h"
#include "syncfileitem.h"
#include "syncengine.h"
#include "propagateupload.h"
Expand Down Expand Up @@ -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<UploadDevice>(singleFile._localPath,
std::unique_ptr<UploadDevice> device;
if (singleFile._item->isSymLink()) {
device = std::make_unique<SymLinkUploadDevice>(singleFile._localPath,
0,
std::numeric_limits<qint64>::max(),
&propagator()->_bandwidthManager);
} else {
device = std::make_unique<UploadDevice>(singleFile._localPath,
0,
singleFile._fileSize,
&propagator()->_bandwidthManager);
}

if (!device->open(QIODevice::ReadOnly)) {
qCWarning(lcBulkPropagatorJob) << "Could not prepare upload device: " << device->errorString();
Expand All @@ -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;
}
Expand Down
12 changes: 12 additions & 0 deletions src/libsync/configfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/libsync/configfile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading