Placeholders: Ignore placeholder files in older clients
authorChristian Kamm <mail@ckamm.de>
Fri, 26 Jan 2018 12:14:54 +0000 (13:14 +0100)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Tue, 15 Dec 2020 09:57:49 +0000 (10:57 +0100)
To do this, we add the placeholder extension to the user exclude file
automatically. However, newer clients shouldn't use that exclude
pattern: so we also add version directives that allow making exclude
patterns dependent on the client version.

src/csync/csync_exclude.cpp
src/csync/csync_exclude.h
src/gui/application.cpp
test/csync/csync_tests/check_csync_exclude.cpp

index 61bfdd443b7a67d8abf76eb533927341dd523035..1bc5a6c5ddafab48536a7a47490ee7ebac72aac4 100644 (file)
 #include "csync_misc.h"
 
 #include "common/utility.h"
+#include "../version.h"
 
 #include <QString>
 #include <QFileInfo>
+#include <QFile>
+#include <QDir>
 
 
 /** Expands C-like escape sequences (in place)
@@ -246,6 +249,7 @@ using namespace OCC;
 
 ExcludedFiles::ExcludedFiles(QString localPath)
     : _localPath(std::move(localPath))
+    , _clientVersion(MIRALL_VERSION_MAJOR, MIRALL_VERSION_MINOR, MIRALL_VERSION_PATCH)
 {
     Q_ASSERT(_localPath.endsWith("/"));
     // Windows used to use PathMatchSpec which allows *foo to match abc/deffoo.
@@ -311,6 +315,34 @@ void ExcludedFiles::setWildcardsMatchSlash(bool onoff)
     prepare();
 }
 
+void ExcludedFiles::setClientVersion(ExcludedFiles::Version version)
+{
+    _clientVersion = version;
+}
+
+void ExcludedFiles::setupPlaceholderExclude(
+    const QString &excludeFile, const QByteArray &placeholderExtension)
+{
+    if (!QFile::exists(excludeFile)) {
+        // Ensure the parent paths exist
+        QDir().mkpath(QFileInfo(excludeFile).dir().absolutePath());
+    } else {
+        // Does the exclude file contain the exclude already?
+        QFile file(excludeFile);
+        file.open(QIODevice::ReadOnly | QIODevice::Text);
+        auto data = file.readAll();
+        file.close();
+        if (data.contains("\n*" + placeholderExtension + "\n"))
+            return;
+    }
+
+    // Add it to the file
+    QFile file(excludeFile);
+    file.open(QIODevice::ReadWrite | QIODevice::Append);
+    file.write("\n#!version < 2.5.0\n*" + placeholderExtension + "\n");
+    file.close();
+}
+
 bool ExcludedFiles::loadExcludeFile(const QByteArray & basePath, const QString & file)
 {
     QFile f(file);
@@ -320,6 +352,10 @@ bool ExcludedFiles::loadExcludeFile(const QByteArray & basePath, const QString &
     QList<QByteArray> patterns;
     while (!f.atEnd()) {
         QByteArray line = f.readLine().trimmed();
+        if (line.startsWith("#!version")) {
+            if (!versionDirectiveKeepNextLine(line))
+                f.readLine();
+        }
         if (line.isEmpty() || line.startsWith('#'))
             continue;
         csync_exclude_expand_escapes(line);
@@ -363,6 +399,32 @@ bool ExcludedFiles::reloadExcludeFiles()
     return success;
 }
 
+bool ExcludedFiles::versionDirectiveKeepNextLine(const QByteArray &directive) const
+{
+    if (!directive.startsWith("#!version"))
+        return true;
+    QByteArrayList args = directive.split(' ');
+    if (args.size() != 3)
+        return true;
+    QByteArray op = args[1];
+    QByteArrayList argVersions = args[2].split('.');
+    if (argVersions.size() != 3)
+        return true;
+
+    auto argVersion = std::make_tuple(argVersions[0].toInt(), argVersions[1].toInt(), argVersions[2].toInt());
+    if (op == "<=")
+        return _clientVersion <= argVersion;
+    if (op == "<")
+        return _clientVersion < argVersion;
+    if (op == ">")
+        return _clientVersion > argVersion;
+    if (op == ">=")
+        return _clientVersion >= argVersion;
+    if (op == "==")
+        return _clientVersion == argVersion;
+    return true;
+}
+
 bool ExcludedFiles::isExcluded(
     const QString &filePath,
     const QString &basePath,
index 8134d2384afd3ca13806b348fe89cba841f44412..4267aa14fcb961bfaab4a1df7901ae90b759a4ae 100644 (file)
@@ -65,6 +65,8 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject
 {
     Q_OBJECT
 public:
+    typedef std::tuple<int, int, int> Version;
+
     ExcludedFiles(QString localPath = "/");
     ~ExcludedFiles();
 
@@ -115,6 +117,11 @@ public:
      */
     void setWildcardsMatchSlash(bool onoff);
 
+    /**
+     * Sets the client version, only used for testing.
+     */
+    void setClientVersion(Version version);
+
     /**
      * Generate a hook for traversal exclude pattern matching
      * that csync can use.
@@ -125,6 +132,12 @@ public:
     auto csyncTraversalMatchFun()
         -> std::function<CSYNC_EXCLUDE_TYPE(const char *path, ItemType filetype)>;
 
+    /**
+     * Adds the exclude that skips placeholder files in older versions
+     * to the user exclude file.
+     */
+    static void setupPlaceholderExclude(const QString &excludeFile, const QByteArray &placeholderExtension);
+
 public slots:
     /**
      * Reloads the exclude patterns from the registered paths.
@@ -136,6 +149,23 @@ public slots:
     bool loadExcludeFile(const QByteArray & basePath, const QString & file);
 
 private:
+    /**
+     * Returns true if the version directive indicates the next line
+     * should be skipped.
+     *
+     * A version directive has the form "#!version <op> <version>"
+     * where <op> can be <, <=, ==, >, >= and <version> can be any version
+     * like 2.5.0.
+     *
+     * Example:
+     *
+     * #!version < 2.5.0
+     * myexclude
+     *
+     * Would enable the "myexclude" pattern only for versions before 2.5.0.
+     */
+    bool versionDirectiveKeepNextLine(const QByteArray &directive) const;
+
     /**
      * @brief Match the exclude pattern against the full path.
      *
@@ -247,6 +277,12 @@ private:
      */
     bool _wildcardsMatchSlash = false;
 
+    /**
+     * The client version. Used to evaluate version-dependent excludes,
+     * see versionDirectiveKeepNextLine().
+     */
+    Version _clientVersion;
+
     friend class ExcludedFilesTest;
 };
 
index c4fb2009a304fc9e8409100ec3002188bd94f88e..d7be0774011f713ce823096c22d8fd425b215728 100644 (file)
@@ -41,6 +41,7 @@
 
 #include "owncloudsetupwizard.h"
 #include "version.h"
+#include "csync_exclude.h"
 
 #include "config.h"
 
@@ -191,6 +192,9 @@ Application::Application(int &argc, char **argv)
     if (!AbstractNetworkJob::httpTimeout)
         AbstractNetworkJob::httpTimeout = cfg.timeout();
 
+    ExcludedFiles::setupPlaceholderExclude(
+        cfg.excludeFile(ConfigFile::UserScope), OWNCLOUD_PLACEHOLDER_SUFFIX);
+
     _folderManager.reset(new FolderMan);
 
     connect(this, &SharedTools::QtSingleApplication::messageReceived, this, &Application::slotParseMessage);
index 1a0dca0f16f6c5ccd9a577dc843ba69d3032a3ba..fa62df538ef90b6ab0a9b795a386a0014542e75e 100644 (file)
@@ -23,6 +23,8 @@
 #include <sys/time.h>
 #include <cstdio>
 
+#include <QTemporaryDir>
+
 #define CSYNC_TEST 1
 #include "csync_exclude.cpp"
 
@@ -694,6 +696,81 @@ static void check_csync_exclude_expand_escapes(void **state)
     assert_true(0 == strcmp(line.constData(), "\\"));
 }
 
+static void check_placeholder_exclude(void **state)
+{
+    (void)state;
+
+    auto readFile = [](const QString &file) {
+        QFile f(file);
+        f.open(QIODevice::ReadOnly | QIODevice::Text);
+        return f.readAll();
+    };
+
+    QTemporaryDir tempDir;
+    QString path;
+    QByteArray expected = "\n#!version < 2.5.0\n*.owncloud\n";
+
+    // Case 1: No file exists yet, parent dirs are missing too
+    path = tempDir.filePath("foo/bar/exclude.lst");
+    ExcludedFiles::setupPlaceholderExclude(path, ".owncloud");
+
+    assert_true(QFile::exists(path));
+    assert_true(readFile(path) == expected);
+
+    // Case 2: Running it again
+    ExcludedFiles::setupPlaceholderExclude(path, ".owncloud");
+    assert_true(readFile(path) == expected);
+
+    // Case 3: File exists, has some data
+    {
+        QFile f(path);
+        f.open(QIODevice::WriteOnly | QIODevice::Truncate);
+        f.write("# bla\nmyexclude\n\nanotherexclude");
+        f.close();
+    }
+    ExcludedFiles::setupPlaceholderExclude(path, ".owncloud");
+    assert_true(readFile(path) == "# bla\nmyexclude\n\nanotherexclude" + expected);
+
+    // Case 4: Running it again still does nothing
+    ExcludedFiles::setupPlaceholderExclude(path, ".owncloud");
+    assert_true(readFile(path) == "# bla\nmyexclude\n\nanotherexclude" + expected);
+
+    // Case 5: Verify that reading this file doesn't actually include the exclude
+    ExcludedFiles excludes;
+    excludes.addExcludeFilePath(path);
+    excludes.reloadExcludeFiles();
+    assert_false(excludes._allExcludes.value("/").contains("*.owncloud"));
+    assert_true(excludes._allExcludes.value("/").contains("myexclude"));
+}
+
+static void check_version_directive(void **state)
+{
+    (void)state;
+
+    ExcludedFiles excludes;
+    excludes.setClientVersion(ExcludedFiles::Version(2, 5, 0));
+
+    std::vector<std::pair<const char *, bool>> tests = {
+        { "#!version == 2.5.0", true },
+        { "#!version == 2.6.0", false },
+        { "#!version < 2.6.0", true },
+        { "#!version <= 2.6.0", true },
+        { "#!version > 2.6.0", false },
+        { "#!version >= 2.6.0", false },
+        { "#!version < 2.4.0", false },
+        { "#!version <= 2.4.0", false },
+        { "#!version > 2.4.0", true },
+        { "#!version >= 2.4.0", true },
+        { "#!version < 2.5.0", false },
+        { "#!version <= 2.5.0", true },
+        { "#!version > 2.5.0", false },
+        { "#!version >= 2.5.0", true },
+    };
+    for (auto test : tests) {
+        assert_true(excludes.versionDirectiveKeepNextLine(test.first) == test.second);
+    }
+}
+
 }; // class ExcludedFilesTest
 
 int torture_run_tests(void)
@@ -715,6 +792,8 @@ int torture_run_tests(void)
         cmocka_unit_test_setup_teardown(T::check_csync_is_windows_reserved_word, T::setup_init, T::teardown),
         cmocka_unit_test_setup_teardown(T::check_csync_excluded_performance, T::setup_init, T::teardown),
         cmocka_unit_test(T::check_csync_exclude_expand_escapes),
+        cmocka_unit_test(T::check_placeholder_exclude),
+        cmocka_unit_test(T::check_version_directive),
     };
 
     return cmocka_run_group_tests(tests, nullptr, nullptr);