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/automate windows file name compatibility #7850

Merged
merged 7 commits into from
Feb 19, 2025
82 changes: 71 additions & 11 deletions src/libsync/discovery.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*

Check notice on line 1 in src/libsync/discovery.cpp

View workflow job for this annotation

GitHub Actions / build

Run clang-format on src/libsync/discovery.cpp

File src/libsync/discovery.cpp does not conform to Custom style guidelines. (lines 294, 305, 306, 308, 313, 314, 315, 348, 2294, 2299, 2305, 2322)
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
Expand Down Expand Up @@ -272,8 +272,7 @@

const auto fileName = path.mid(path.lastIndexOf('/') + 1);

const auto osDoesNotSupportsSpaces = Utility::isWindows();
if (excluded == CSYNC_NOT_EXCLUDED && osDoesNotSupportsSpaces) {
if (excluded == CSYNC_NOT_EXCLUDED) {
const auto endsWithSpace = fileName.endsWith(QLatin1Char(' '));
const auto startsWithSpace = fileName.startsWith(QLatin1Char(' '));
if (startsWithSpace && endsWithSpace) {
Expand All @@ -291,20 +290,30 @@
const auto hasLeadingOrTrailingSpaces = excluded == CSYNC_FILE_EXCLUDE_LEADING_SPACE
|| excluded == CSYNC_FILE_EXCLUDE_TRAILING_SPACE
|| excluded == CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE;
const auto leadingAndTrailingSpacesFilesAllowed = _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(_discoveryData->_localDir + path);
if (hasLeadingOrTrailingSpaces && ((!osDoesNotSupportsSpaces && wasSyncedAlready) || leadingAndTrailingSpacesFilesAllowed)) {

const auto leadingAndTrailingSpacesFilesAllowed = !_discoveryData->_shouldEnforceWindowsFileNameCompatibility || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(_discoveryData->_localDir + path);
#if defined Q_OS_WINDOWS
if (hasLeadingOrTrailingSpaces && leadingAndTrailingSpacesFilesAllowed) {
#else
if (hasLeadingOrTrailingSpaces && (wasSyncedAlready || leadingAndTrailingSpacesFilesAllowed)) {
#endif
excluded = CSYNC_NOT_EXCLUDED;
}

// FIXME: move to ExcludedFiles 's regexp ?
bool isInvalidPattern = false;
if (excluded == CSYNC_NOT_EXCLUDED && !_discoveryData->_invalidFilenameRx.pattern().isEmpty()) {
if (path.contains(_discoveryData->_invalidFilenameRx)) {
excluded = CSYNC_FILE_EXCLUDE_INVALID_CHAR;
isInvalidPattern = true;
}
if (excluded == CSYNC_NOT_EXCLUDED
&& !wasSyncedAlready
&& !_discoveryData->_invalidFilenameRx.pattern().isEmpty()
&& path.contains(_discoveryData->_invalidFilenameRx)) {

excluded = CSYNC_FILE_EXCLUDE_INVALID_CHAR;
isInvalidPattern = true;
}
if (excluded == CSYNC_NOT_EXCLUDED && _discoveryData->_ignoreHiddenFiles && isHidden) {
if (excluded == CSYNC_NOT_EXCLUDED
&& !entries.dbEntry.isValid()
&& _discoveryData->_ignoreHiddenFiles && isHidden) {

excluded = CSYNC_FILE_EXCLUDE_HIDDEN;
}

Expand Down Expand Up @@ -335,7 +344,8 @@
});

if (excluded == CSYNC_NOT_EXCLUDED && !localName.isEmpty()
&& (_discoveryData->_serverBlacklistedFiles.contains(localName)
&& !wasSyncedAlready
&&(_discoveryData->_serverBlacklistedFiles.contains(localName)
|| hasForbiddenFilename
|| hasForbiddenBasename
|| hasForbiddenExtension
Expand Down Expand Up @@ -415,14 +425,23 @@
case CSYNC_FILE_EXCLUDE_TRAILING_SPACE:
item->_errorString = tr("Filename contains trailing spaces.");
item->_status = SyncFileItem::FileNameInvalid;
if (!maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) {
item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded."));
}
break;
case CSYNC_FILE_EXCLUDE_LEADING_SPACE:
item->_errorString = tr("Filename contains leading spaces.");
item->_status = SyncFileItem::FileNameInvalid;
if (!maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) {
item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded."));
}
break;
case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE:
item->_errorString = tr("Filename contains leading and trailing spaces.");
item->_status = SyncFileItem::FileNameInvalid;
if (!maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) {
item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded."));
}
break;
case CSYNC_FILE_EXCLUDE_LONG_FILENAME:
item->_errorString = tr("Filename is too long.");
Expand Down Expand Up @@ -462,6 +481,9 @@
}
item->_errorString = reasonString.isEmpty() ? errorString : QStringLiteral("%1 %2").arg(errorString, reasonString);
item->_status = SyncFileItem::FileNameInvalidOnServer;
if (!maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded)) {
item->_errorString += QStringLiteral(" %1").arg(tr("Cannot be renamed or uploaded."));
}
break;
}
}
Expand Down Expand Up @@ -2269,4 +2291,42 @@
}
}

bool ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &absoluteFileName,
CSYNC_EXCLUDE_TYPE excludeReason)
{
auto result = true;

const auto leadingAndTrailingSpacesFilesAllowed = !_discoveryData->_shouldEnforceWindowsFileNameCompatibility || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(absoluteFileName);
if (leadingAndTrailingSpacesFilesAllowed) {
return result;
}

const auto fileInfo = QFileInfo{absoluteFileName};
switch (excludeReason)
{
case CSYNC_NOT_EXCLUDED:
case CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT:
case CSYNC_FILE_EXCLUDE_AND_REMOVE:
case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
case CSYNC_FILE_EXCLUDE_INVALID_CHAR:
case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED:
case CSYNC_FILE_EXCLUDE_HIDDEN:
case CSYNC_FILE_SILENTLY_EXCLUDED:
case CSYNC_FILE_EXCLUDE_STAT_FAILED:
case CSYNC_FILE_EXCLUDE_LONG_FILENAME:
case CSYNC_FILE_EXCLUDE_LIST:
case CSYNC_FILE_EXCLUDE_CONFLICT:
break;
case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE:
case CSYNC_FILE_EXCLUDE_LEADING_SPACE:
case CSYNC_FILE_EXCLUDE_TRAILING_SPACE:
{
const auto renameTarget = QString{fileInfo.absolutePath() + QStringLiteral("/") + fileInfo.fileName().trimmed()};
result = FileSystem::rename(absoluteFileName, renameTarget);
break;
}
}
return result;
}

}
4 changes: 4 additions & 0 deletions src/libsync/discovery.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*

Check notice on line 1 in src/libsync/discovery.h

View workflow job for this annotation

GitHub Actions / build

Run clang-format on src/libsync/discovery.h

File src/libsync/discovery.h does not conform to Custom style guidelines. (lines 259)
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
Expand All @@ -14,7 +14,8 @@

#pragma once

#include <QObject>

Check failure on line 17 in src/libsync/discovery.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/discovery.h:17:10 [clang-diagnostic-error]

'QObject' file not found
#include "csync_exclude.h"
#include "discoveryphase.h"
#include "syncfileitem.h"
#include "common/asserts.h"
Expand Down Expand Up @@ -255,6 +256,9 @@
*/
void setupDbPinStateActions(SyncJournalFileRecord &record);

bool maybeRenameForWindowsCompatibility(const QString &absoluteFileName,
CSYNC_EXCLUDE_TYPE excludeReason);

qint64 _lastSyncTimestamp = 0;

QueryMode _queryServer = QueryMode::NormalQuery;
Expand Down
6 changes: 6 additions & 0 deletions src/libsync/discoveryphase.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#pragma once

#include <QObject>

Check failure on line 17 in src/libsync/discoveryphase.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/discoveryphase.h:17:10 [clang-diagnostic-error]

'QObject' file not found
#include <QElapsedTimer>
#include <QStringList>
#include <csync.h>
Expand Down Expand Up @@ -326,6 +326,7 @@
QRegularExpression _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles
QStringList _serverBlacklistedFiles; // The blacklist from the capabilities
QStringList _leadingAndTrailingSpacesFilesAllowed;
bool _shouldEnforceWindowsFileNameCompatibility = false;
bool _ignoreHiddenFiles = false;
std::function<bool(const QString &)> _shouldDiscoverLocaly;

Expand All @@ -342,6 +343,11 @@

QStringList _listExclusiveFiles;

QStringList _forbiddenFilenames;
QStringList _forbiddenBasenames;
QStringList _forbiddenExtensions;
QStringList _forbiddenChars;

bool _hasUploadErrorItems = false;
bool _hasDownloadRemovedItems = false;

Expand Down
27 changes: 27 additions & 0 deletions src/libsync/syncengine.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*

Check notice on line 1 in src/libsync/syncengine.cpp

View workflow job for this annotation

GitHub Actions / build

Run clang-format on src/libsync/syncengine.cpp

File src/libsync/syncengine.cpp does not conform to Custom style guidelines. (lines 684, 685, 686)
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
Expand Down Expand Up @@ -671,6 +671,28 @@
return;
}

const auto accountCaps = _account->capabilities();
const auto forbiddenFilenames = accountCaps.forbiddenFilenames();
const auto forbiddenBasenames = accountCaps.forbiddenFilenameBasenames();
const auto forbiddenExtensions = accountCaps.forbiddenFilenameExtensions();
const auto forbiddenChars = accountCaps.forbiddenFilenameCharacters();

_discoveryPhase->_forbiddenFilenames = forbiddenFilenames;
_discoveryPhase->_forbiddenBasenames = forbiddenBasenames;
_discoveryPhase->_forbiddenExtensions = forbiddenExtensions;
_discoveryPhase->_forbiddenChars = forbiddenChars;
if (!forbiddenFilenames.isEmpty() &&
!forbiddenBasenames.isEmpty() &&
!forbiddenExtensions.isEmpty() &&
!forbiddenChars.isEmpty()) {
_shouldEnforceWindowsFileNameCompatibility = true;
_discoveryPhase->_shouldEnforceWindowsFileNameCompatibility = _shouldEnforceWindowsFileNameCompatibility;
}
#if defined Q_OS_WINDOWS
_shouldEnforceWindowsFileNameCompatibility = true;
_discoveryPhase->_shouldEnforceWindowsFileNameCompatibility = _shouldEnforceWindowsFileNameCompatibility;
#endif

// Check for invalid character in old server version
QString invalidFilenamePattern = _account->capabilities().invalidFilenameRegex();
if (invalidFilenamePattern.isNull()
Expand Down Expand Up @@ -1208,6 +1230,11 @@
_leadingAndTrailingSpacesFilesAllowed.append(filePath);
}

void SyncEngine::setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value)
{
_shouldEnforceWindowsFileNameCompatibility = value;
}

bool SyncEngine::wasFileTouched(const QString &fn) const
{
// Start from the end (most recent) and look for our path. Check the time just in case.
Expand Down
2 changes: 2 additions & 0 deletions src/libsync/syncengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

#pragma once

#include <cstdint>

Check failure on line 18 in src/libsync/syncengine.h

View workflow job for this annotation

GitHub Actions / build

src/libsync/syncengine.h:18:10 [clang-diagnostic-error]

'cstdint' file not found

#include <QMutex>
#include <QThread>
Expand Down Expand Up @@ -159,6 +159,7 @@
*/
void setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle style, std::set<QString> paths = {});
void addAcceptedInvalidFileName(const QString& filePath);
void setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value);

signals:
// During update, before reconcile
Expand Down Expand Up @@ -409,6 +410,7 @@
std::set<QString> _localDiscoveryPaths;

QStringList _leadingAndTrailingSpacesFilesAllowed;
bool _shouldEnforceWindowsFileNameCompatibility = false;

// Hash of files we have scheduled for later sync runs, along with a
// pointer to the timer which will trigger the sync run for it.
Expand Down
75 changes: 75 additions & 0 deletions test/syncenginetestutils.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*

Check notice on line 1 in test/syncenginetestutils.cpp

View workflow job for this annotation

GitHub Actions / build

Run clang-format on test/syncenginetestutils.cpp

File test/syncenginetestutils.cpp does not conform to Custom style guidelines. (lines 1212, 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278, 1279, 1280, 1281)
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
Expand Down Expand Up @@ -1207,6 +1207,81 @@
vfs->start(vfsParams);
}

void FakeFolder::enableEnforceWindowsFileNameCompatibility()
{
syncEngine().account()->setCapabilities(QVariantMap{
{
"files",
QVariantMap{
{
"forbidden_filename_basenames",
QStringList{"con",
"prn",
"aux",
"nul",
"com0",
"com1",
"com2",
"com3",
"com4",
"com5",
"com6",
"com7",
"com8",
"com9",
"com¹",
"com²",
"com³",
"lpt0",
"lpt1",
"lpt2",
"lpt3",
"lpt4",
"lpt5",
"lpt6",
"lpt7",
"lpt8",
"lpt9",
"lpt¹",
"lpt²",
"lpt³"
}
},
{
"forbidden_filename_characters",
QStringList{"\\",
"/",
"<",
">",
":",
"\"",
"|",
"?",
"*",
"\\",
"/"
}
},
{
"forbidden_filename_extensions",
QStringList{" ",
".",
".filepart",
".part",
".part"
}
},
{
"forbidden_filenames",
QStringList{"\\",
".htaccess"
}
}
}
}
});
}

FileInfo FakeFolder::currentLocalState()
{
QDir rootDir { _tempDir.path() };
Expand Down
2 changes: 2 additions & 0 deletions test/syncenginetestutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
#pragma once

#include "account.h"

Check failure on line 9 in test/syncenginetestutils.h

View workflow job for this annotation

GitHub Actions / build

test/syncenginetestutils.h:9:10 [clang-diagnostic-error]

'account.h' file not found
#include "common/result.h"
#include "creds/abstractcredentials.h"
#include "logger.h"
Expand Down Expand Up @@ -559,6 +559,8 @@

void switchToVfs(QSharedPointer<OCC::Vfs> vfs);

void enableEnforceWindowsFileNameCompatibility();

[[nodiscard]] OCC::AccountPtr account() const { return _account; }
[[nodiscard]] OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
[[nodiscard]] OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
Expand Down
Loading
Loading