diff options
author | Jan Lukas Gernert <jangernert@gmail.com> | 2019-10-06 23:19:35 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-06 23:19:35 +0300 |
commit | da6f6d3a74081891288af69ab0701cde0735c9a3 (patch) | |
tree | 4802a5ff482b9d6c9aaec427be6d39c713093f46 | |
parent | aaa2fc0aa248ef83a17aee3496c2eab45f95b3ee (diff) | |
parent | b0e8f161b13cc244a4a1ee35a305930a1c134bfd (diff) |
Merge pull request #948 from 39aldo39/update-libdecsync
Update libdecsync
-rw-r--r-- | plugins/backend/decsync/decsyncInterface.vala | 57 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/meson.build | 5 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/Decsync.vala | 1286 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/DirectoryMonitor.vala | 82 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/FileUtils.vala | 299 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/Log.vala | 32 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/OnEntryUpdateListener.vala | 38 | ||||
-rw-r--r-- | plugins/backend/decsync/libdecsync/src/Utils.vala | 72 |
8 files changed, 954 insertions, 917 deletions
diff --git a/plugins/backend/decsync/decsyncInterface.vala b/plugins/backend/decsync/decsyncInterface.vala index fd0f89a6..24f587cf 100644 --- a/plugins/backend/decsync/decsyncInterface.vala +++ b/plugins/backend/decsync/decsyncInterface.vala @@ -31,34 +31,6 @@ public class FeedReader.decsyncInterface : FeedServerInterface { m_session.timeout = 5; } - private bool initDecsync() - { - var decsyncDir = m_utils.getDecsyncDir(); - if (decsyncDir == "") - { - return false; - } - var dir = getDecsyncSubdir(decsyncDir, "rss"); - var ownAppId = getAppId("FeedReader"); - var listeners = new Gee.ArrayList<OnEntryUpdateListener>(); - listeners.add(new DecsyncListeners.ReadMarkListener(true, this)); - listeners.add(new DecsyncListeners.ReadMarkListener(false, this)); - listeners.add(new DecsyncListeners.SubscriptionsListener(this)); - listeners.add(new DecsyncListeners.FeedNamesListener(this)); - listeners.add(new DecsyncListeners.CategoriesListener(this)); - listeners.add(new DecsyncListeners.CategoryNamesListener(this)); - listeners.add(new DecsyncListeners.CategoryParentsListener(this)); - m_sync = new Decsync<Unit>(dir, ownAppId, listeners); - m_sync.syncComplete.connect((extra) => { - FeedReaderBackend.get_default().updateBadge(); - refreshFeedListCounter(); - newFeedList(); - updateArticleList(); - }); - m_sync.initMonitor(new Unit()); - return true; - } - public override string getWebsite() { return "https://github.com/39aldo39/DecSync"; @@ -262,13 +234,36 @@ public class FeedReader.decsyncInterface : FeedServerInterface { public override LoginResponse login() { - if (initDecsync()) + var decsyncDir = m_utils.getDecsyncDir(); + if (decsyncDir == "") { + return LoginResponse.ALL_EMPTY; + } + var dir = getDecsyncSubdir(decsyncDir, "rss"); + var ownAppId = getAppId("FeedReader"); + var listeners = new Gee.ArrayList<OnEntryUpdateListener>(); + listeners.add(new DecsyncListeners.ReadMarkListener(true, this)); + listeners.add(new DecsyncListeners.ReadMarkListener(false, this)); + listeners.add(new DecsyncListeners.SubscriptionsListener(this)); + listeners.add(new DecsyncListeners.FeedNamesListener(this)); + listeners.add(new DecsyncListeners.CategoriesListener(this)); + listeners.add(new DecsyncListeners.CategoryNamesListener(this)); + listeners.add(new DecsyncListeners.CategoryParentsListener(this)); + try + { + m_sync = new Decsync<Unit>(dir, ownAppId, listeners); + m_sync.syncComplete.connect((extra) => { + FeedReaderBackend.get_default().updateBadge(); + refreshFeedListCounter(); + newFeedList(); + updateArticleList(); + }); + m_sync.initMonitor(new Unit()); return LoginResponse.SUCCESS; } - else + catch (DecsyncError e) { - return LoginResponse.ALL_EMPTY; + return LoginResponse.API_ERROR; } } diff --git a/plugins/backend/decsync/libdecsync/meson.build b/plugins/backend/decsync/libdecsync/meson.build index f701917f..b694ddfe 100644 --- a/plugins/backend/decsync/libdecsync/meson.build +++ b/plugins/backend/decsync/libdecsync/meson.build @@ -1,4 +1,7 @@ -project('libdecsync', ['vala', 'c']) +project('libdecsync', ['vala', 'c'], + version: '1.2.0', + license: 'LGPL' +) gee = dependency('gee-0.8') json_glib = dependency('json-glib-1.0') diff --git a/plugins/backend/decsync/libdecsync/src/Decsync.vala b/plugins/backend/decsync/libdecsync/src/Decsync.vala index 4adc6f54..3f3495ce 100644 --- a/plugins/backend/decsync/libdecsync/src/Decsync.vala +++ b/plugins/backend/decsync/libdecsync/src/Decsync.vala @@ -1,744 +1,800 @@ /** -* libdecsync-vala - Decsync.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ - -public class Unit { public Unit() { -} + * libdecsync-vala - Decsync.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +public class Unit { public Unit() {} } + +public errordomain DecsyncError { + INVALID_INFO, + UNSUPPORTED_VERSION } /** -* The `DecSync` class represents an interface to synchronized key-value mappings stored on the file -* system. -* -* The mappings can be synchronized by synchronizing the directory [dir]. The stored mappings are -* stored in a conflict-free way. When the same keys are updated independently, the most recent -* value is taken. This should not cause problems when the individual values contain as little -* information as possible. -* -* Every entry consists of a path, a key and a value. The path is a list of strings which contains -* the location to the used mapping. This can make interacting with the data easier. It is also used -* to construct a path in the file system. All characters are allowed in the path. However, other -* limitations of the file system may apply. For example, there may be a maximum length or the file -* system may be case insensitive. -* -* To update an entry, use the method [setEntry]. When multiple keys in the same path are updated -* simultaneous, it is encouraged to use the more efficient methods [setEntriesForPath] and -* [setEntries]. -* -* To get notified about updated entries, use the method [executeAllNewEntries] to get all updated -* entries and execute the corresponding actions. The method [initObserver] creates a file observer -* which is notified about the updated entries immediately. -* -* Sometimes, updates cannot be execute immediately. For example, if the name of a category is -* updated when the category does not exist yet, the name cannot be changed. In such cases, the -* updates have to be executed retroactively. In the example, the update can be executed when the -* category is created. For such cases, use the method [executeStoredEntries]. -* -* Finally, to initialize the stored entries to the most recent values, use the method -* [initStoredEntries]. This method is almost exclusively used when the application is installed. It -* is almost always followed by a call to [executeStoredEntries]. -* -* @param T the type of the extra data passed to the [listeners] and [syncComplete]. -* @property dir the directory in which the synchronized DecSync files are stored. -* For the default location, use [getDecsyncSubdir]. -* @property ownAppId the unique appId corresponding to the stored data by the application. There -* must not be two simultaneous instances with the same appId. However, if an application is -* reinstalled, it may reuse its old appId. In that case, it has to call [initStoredEntries] and -* [executeStoredEntries]. Even if the old appId is not reused, it is still recommended call these. -* For the default appId, use [getAppId]. -* @property listeners a list of listeners describing the actions to execute on every updated entry. -* When an entry is updated, the method [OnEntryUpdateListener.onEntriesUpdate] is called on the -* listener whose method [OnEntryUpdateListener.matchesPath] returns true. -* @property syncComplete an optional function which is called when a sync is complete. For example, -* it can be used to update the UI. -*/ + * The `DecSync` class represents an interface to synchronized key-value mappings stored on the file + * system. + * + * The mappings can be synchronized by synchronizing the directory [dir]. The stored mappings are + * stored in a conflict-free way. When the same keys are updated independently, the most recent + * value is taken. This should not cause problems when the individual values contain as little + * information as possible. + * + * Every entry consists of a path, a key and a value. The path is a list of strings which contains + * the location to the used mapping. This can make interacting with the data easier. It is also used + * to construct a path in the file system. All characters are allowed in the path. However, other + * limitations of the file system may apply. For example, there may be a maximum length or the file + * system may be case insensitive. + * + * To update an entry, use the method [setEntry]. When multiple keys in the same path are updated + * simultaneous, it is encouraged to use the more efficient methods [setEntriesForPath] and + * [setEntries]. + * + * To get notified about updated entries, use the method [executeAllNewEntries] to get all updated + * entries and execute the corresponding actions. The method [initObserver] creates a file observer + * which is notified about the updated entries immediately. + * + * Sometimes, updates cannot be execute immediately. For example, if the name of a category is + * updated when the category does not exist yet, the name cannot be changed. In such cases, the + * updates have to be executed retroactively. In the example, the update can be executed when the + * category is created. For such cases, use the method [executeStoredEntries]. + * + * Finally, to initialize the stored entries to the most recent values, use the method + * [initStoredEntries]. This method is almost exclusively used when the application is installed. It + * is almost always followed by a call to [executeStoredEntries]. + * + * @param T the type of the extra data passed to the [listeners] and [syncComplete]. + * @property dir the directory in which the synchronized DecSync files are stored. + * For the default location, use [getDecsyncSubdir]. + * @property ownAppId the unique appId corresponding to the stored data by the application. There + * must not be two simultaneous instances with the same appId. However, if an application is + * reinstalled, it may reuse its old appId. In that case, it has to call [initStoredEntries] and + * [executeStoredEntries]. Even if the old appId is not reused, it is still recommended call these. + * For the default appId, use [getAppId]. + * @property listeners a list of listeners describing the actions to execute on every updated entry. + * When an entry is updated, the method [OnEntryUpdateListener.onEntriesUpdate] is called on the + * listener whose method [OnEntryUpdateListener.matchesPath] returns true. + * @property syncComplete an optional function which is called when a sync is complete. For example, + * it can be used to update the UI. + * @throws DecsyncException if a DecSync configuration error occurred. + */ public class Decsync<T> : GLib.Object { -string dir; -string ownAppId; -string ownAppIdEncoded; -Gee.Iterable<OnEntryUpdateListener<T>> listeners; -DirectoryMonitor? monitor = null; + string dir; + string ownAppId; + string ownAppIdEncoded; + Gee.Iterable<OnEntryUpdateListener<T>> listeners; + DirectoryMonitor? monitor = null; -/** -* Signal which is called when a sync is complete. For example, it can be used to update the UI. -*/ -public signal void syncComplete(T extra); + /** + * Signal which is called when a sync is complete. For example, it can be used to update the UI. + */ + public signal void syncComplete(T extra); -public Decsync(string dir, string ownAppId, Gee.Iterable<OnEntryUpdateListener<T>> listeners) -{ - this.dir = dir; - this.ownAppId = ownAppId; - this.ownAppIdEncoded = FileUtils.urlencode(ownAppId); - this.listeners = listeners; -} + public Decsync(string dir, string ownAppId, Gee.Iterable<OnEntryUpdateListener<T>> listeners) throws DecsyncError + { + this.dir = dir; + this.ownAppId = ownAppId; + this.ownAppIdEncoded = FileUtils.urlencode(ownAppId); + this.listeners = listeners; -/** -* Represents an [Entry] with its path. -*/ -public class EntryWithPath { - public Gee.List<string> path; - public Entry entry; + checkDecsyncSubdirInfo(dir); + } - public EntryWithPath(string[] path, Entry entry) - { - this.path = toList(path); - this.entry = entry; + /** + * Represents an [Entry] with its path. + */ + public class EntryWithPath { + public Gee.List<string> path; + public Entry entry; + + public EntryWithPath(string[] path, Entry entry) + { + this.path = toList(path); + this.entry = entry; + } + + public EntryWithPath.now(string[] path, Json.Node key, Json.Node value) + { + this.path = toList(path); + this.entry = new Entry.now(key, value); + } } - public EntryWithPath.now(string[] path, Json.Node key, Json.Node value) - { - this.path = toList(path); - this.entry = new Entry.now(key, value); + /** + * Represents a key/value pair stored by DecSync. Additionally, it has a datetime property + * indicating the most recent update. It does not store its path, see [EntryWithPath]. + */ + public class Entry { + internal string datetime; + public Json.Node key; + public Json.Node value; + + public Entry(string datetime, Json.Node key, Json.Node value) + { + this.datetime = datetime; + this.key = key; + this.value = value; + } + + public Entry.now(Json.Node key, Json.Node value) + { + this.datetime = new GLib.DateTime.now_utc().format("%FT%T"); + this.key = key; + this.value = value; + } + + internal string toLine() + { + var json = new Json.Node(Json.NodeType.ARRAY); + var array = new Json.Array(); + array.add_string_element(this.datetime); + array.add_element(this.key); + array.add_element(this.value); + json.set_array(array); + return Json.to_string(json, false); + } + + internal static Entry? fromLine(string line) + { + try { + var json = Json.from_string(line); + var array = json.get_array(); + if (array == null || array.get_length() != 3) { + Log.w("Invalid entry " + line); + return null; + } + var datetime = array.get_string_element(0); + if (datetime == null) { + Log.w("Invalid entry " + line); + return null; + } + var key = array.get_element(1); + var value = array.get_element(2); + return new Entry(datetime, key, value); + } catch (GLib.Error e) { + Log.w("Invalid JSON: " + line + "\n" + e.message); + return null; + } + } } -} -/** -* Represents a key/value pair stored by DecSync. Additionally, it has a datetime property -* indicating the most recent update. It does not store its path, see [EntryWithPath]. -*/ -public class Entry { - internal string datetime; - public Json.Node key; - public Json.Node value; - - public Entry(string datetime, Json.Node key, Json.Node value) - { - this.datetime = datetime; - this.key = key; - this.value = value; + private class EntriesLocation { + public Gee.List<string> path; + public File newEntriesFile; + public File? storedEntriesFile; + public File? readBytesFile; + + public EntriesLocation.getNewEntriesLocation(Decsync decsync, Gee.List<string> path, string appId) + { + var pathString = FileUtils.pathToString(path); + var appIdEncoded = FileUtils.urlencode(appId); + this.path = path; + this.newEntriesFile = File.new_for_path(decsync.dir + "/new-entries/" + appIdEncoded + "/" + pathString); + this.storedEntriesFile = File.new_for_path(decsync.dir + "/stored-entries/" + decsync.ownAppIdEncoded + "/" + pathString); + this.readBytesFile = File.new_for_path(decsync.dir + "/read-bytes/" + decsync.ownAppIdEncoded + "/" + appIdEncoded + "/" + pathString); + } + + public EntriesLocation.getStoredEntriesLocation(Decsync decsync, Gee.List<string> path) + { + var pathString = FileUtils.pathToString(path); + this.path = path; + this.newEntriesFile = File.new_for_path(decsync.dir + "/stored-entries/" + decsync.ownAppIdEncoded + "/" + pathString); + this.storedEntriesFile = null; + this.readBytesFile = null; + } } - public Entry.now(Json.Node key, Json.Node value) + /** + * Associates the given [value] with the given [key] in the map corresponding to the given + * [path]. This update is sent to synchronized devices. + */ + public void setEntry(string[] pathArray, Json.Node key, Json.Node value) { - this.datetime = new GLib.DateTime.now_utc().format("%FT%T"); - this.key = key; - this.value = value; + var entries = new Gee.ArrayList<Entry>(); + entries.add(new Entry.now(key, value)); + setEntriesForPath(toList(pathArray), entries); } - internal string toLine() + /** + * Like [setEntry], but allows multiple entries to be set. This is more efficient if multiple + * entries share the same path. + * + * @param entriesWithPath entries with path which are inserted. + */ + public void setEntries(Gee.Collection<EntryWithPath> entriesWithPath) { - var json = new Json.Node(Json.NodeType.ARRAY); - var array = new Json.Array(); - array.add_string_element(this.datetime); - array.add_element(this.key); - array.add_element(this.value); - json.set_array(array); - return Json.to_string(json, false); + var multiMap = groupByPath<EntryWithPath, Entry>( + entriesWithPath, + entryWithPath => { return entryWithPath.path; }, + entryWithPath => { return entryWithPath.entry; } + ); + multiMap.get_keys().@foreach(path => { + setEntriesForPath(path, multiMap.@get(path)); + return true; + }); } - internal static Entry? fromLine(string line) + /** + * Like [setEntries], but only allows the entries to have the same path. Consequently, it can + * be slightly more convenient since the path has to be specified just once. + * + * @param path path to the map in which the entries are inserted. + * @param entries entries which are inserted. + */ + public void setEntriesForPath(Gee.List<string> path, Gee.Collection<Entry> entries) { + Log.d("Write to path " + FileUtils.pathToString(path)); + var entriesLocation = new EntriesLocation.getNewEntriesLocation(this, path, ownAppId); + + // Write new entries + var builder = new StringBuilder(); + foreach (var entry in entries) { + builder.append(entry.toLine() + "\n"); + } try { - var json = Json.from_string(line); - var array = json.get_array(); - if (array == null || array.get_length() != 3) - { - Log.w("Invalid entry " + line); - return null; + FileUtils.writeFile(entriesLocation.newEntriesFile, builder.str, true); + } catch (Error e) { + Log.w(e.message); + } + + // Update .decsync-sequence files + while (!path.is_empty) { + path.remove_at(path.size - 1); + var dir = new EntriesLocation.getNewEntriesLocation(this, path, ownAppId).newEntriesFile; + var file = dir.get_child(".decsync-sequence"); + + // Get the old version + int64 version = 0; + if (file.query_exists()) { + try { + var stream = new DataInputStream(file.read()); + version = int64.parse(stream.read_line()); // Defaults to 0 + } catch (GLib.Error e) { + Log.w(e.message); + } } - var datetime = array.get_string_element(0); - if (datetime == null) - { - Log.w("Invalid entry " + line); - return null; + + // Write the new version + try { + FileUtils.writeFile(file, (version + 1).to_string()); + } catch (Error e) { + Log.w(e.message); } - var key = array.get_element(1); - var value = array.get_element(2); - return new Entry(datetime, key, value); - } catch (GLib.Error e) { - Log.w("Invalid JSON: " + line + "\n" + e.message); - return null; } - } -} -private class EntriesLocation { - public Gee.List<string> path; - public File newEntriesFile; - public File? storedEntriesFile; - public File? readBytesFile; - - public EntriesLocation.getNewEntriesLocation(Decsync decsync, Gee.List<string> path, string appId) - { - var pathString = FileUtils.pathToString(path); - var appIdEncoded = FileUtils.urlencode(appId); - this.path = path; - this.newEntriesFile = File.new_for_path(decsync.dir + "/new-entries/" + appIdEncoded + "/" + pathString); - this.storedEntriesFile = File.new_for_path(decsync.dir + "/stored-entries/" + decsync.ownAppIdEncoded + "/" + pathString); - this.readBytesFile = File.new_for_path(decsync.dir + "/read-bytes/" + decsync.ownAppIdEncoded + "/" + appIdEncoded + "/" + pathString); + // Update stored entries + updateStoredEntries(entriesLocation, entries); } - public EntriesLocation.getStoredEntriesLocation(Decsync decsync, Gee.List<string> path) + /** + * Initializes the monitor which watches the filesystem for updated entries and executes the + * corresponding actions. + * + * @param extra extra data passed to the [listeners]. + */ + public void initMonitor(T extra) { - var pathString = FileUtils.pathToString(path); - this.path = path; - this.newEntriesFile = File.new_for_path(decsync.dir + "/stored-entries/" + decsync.ownAppIdEncoded + "/" + pathString); - this.storedEntriesFile = null; - this.readBytesFile = null; + try { + var newEntriesDir = File.new_for_path(dir + "/new-entries"); + var parent = newEntriesDir.get_parent(); + if (!parent.query_exists()) { + parent.make_directory_with_parents(); + } + monitor = new DirectoryMonitor(newEntriesDir); + monitor.changed.connect(pathString => { + var pathEncoded = new Gee.ArrayList<string>.wrap(pathString.split("/")); + pathEncoded.remove(""); + if (pathEncoded.is_empty || pathEncoded.last()[0] == '.') { + return; + } + var path = new Gee.ArrayList<string>(); + path.add_all_iterator(pathEncoded.map<string>(part => { return FileUtils.urldecode(part); })); + if (path.fold<bool>((part, seed) => { return part == null || seed; }, false)) { + Log.w("Cannot decode path " + pathString); + return; + } + var appId = path.first(); + path.remove_at(0); + var entriesLocation = new EntriesLocation.getNewEntriesLocation(this, path, appId); + if (appId != ownAppId && entriesLocation.newEntriesFile.query_file_type(FileQueryInfoFlags.NONE) == FileType.REGULAR) { + executeEntriesLocation(entriesLocation, extra); + Log.d("Sync complete"); + syncComplete(extra); + } + }); + Log.d("Initialized folder monitor for " + dir + "/new-entries"); + } catch (GLib.Error e) { + Log.w(e.message); + } } -} - -/** -* Associates the given [value] with the given [key] in the map corresponding to the given -* [path]. This update is sent to synchronized devices. -*/ -public void setEntry(string[] pathArray, Json.Node key, Json.Node value) -{ - var entries = new Gee.ArrayList<Entry>(); - entries.add(new Entry.now(key, value)); - setEntriesForPath(toList(pathArray), entries); -} - -/** -* Like [setEntry], but allows multiple entries to be set. This is more efficient if multiple -* entries share the same path. -* -* @param entriesWithPath entries with path which are inserted. -*/ -public void setEntries(Gee.Collection<EntryWithPath> entriesWithPath) -{ - var multiMap = groupBy<EntryWithPath, Gee.List<string>, Entry>( - entriesWithPath, - entryWithPath => { return entryWithPath.path; }, - entryWithPath => { return entryWithPath.entry; } - ); - multiMap.get_keys().@foreach(path => { - setEntriesForPath(path, multiMap.@get(path)); - return true; - }); -} -/** -* Like [setEntries], but only allows the entries to have the same path. Consequently, it can -* be slightly more convenient since the path has to be specified just once. -* -* @param path path to the map in which the entries are inserted. -* @param entries entries which are inserted. -*/ -public void setEntriesForPath(Gee.List<string> path, Gee.Collection<Entry> entries) -{ - Log.d("Write to path " + FileUtils.pathToString(path)); - var entriesLocation = new EntriesLocation.getNewEntriesLocation(this, path, ownAppId); - - // Write new entries - var builder = new StringBuilder(); - foreach (var entry in entries) { - builder.append(entry.toLine() + "\n"); - } - try { - FileUtils.writeFile(entriesLocation.newEntriesFile, builder.str, true); - } catch (Error e) { - Log.w(e.message); + /** + * Gets all updated entries and executes the corresponding actions. + * + * @param extra extra data passed to the [listeners]. + */ + public void executeAllNewEntries(T extra) + { + Log.d("Execute all new entries in " + dir); + var newEntriesDir = File.new_for_path(dir + "/new-entries"); + var readBytesDir = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded); + Gee.Predicate<Gee.List<string>> pathPred = path => { return path.is_empty || path.first() != ownAppId; }; + FileUtils.listFilesRecursiveRelative(newEntriesDir, readBytesDir, pathPred) + .map<EntriesLocation>(path => { return new EntriesLocation.getNewEntriesLocation(this, path.slice(1, path.size), path.first()); }) + .@foreach (entriesLocation => { + executeEntriesLocation(entriesLocation, extra); + return true; + }); + Log.d("Sync complete"); + syncComplete(extra); } - // Update .decsync-sequence files - while (!path.is_empty) { - path.remove_at(path.size - 1); - var dir = new EntriesLocation.getNewEntriesLocation(this, path, ownAppId).newEntriesFile; - var file = dir.get_child(".decsync-sequence"); + private void executeEntriesLocation(EntriesLocation entriesLocation, T extra, Gee.Predicate<Json.Node>? keyPred = null, Gee.Predicate<Json.Node>? valuePred = null) + { + // Get the number of read bytes + int64 readBytes = 0; + if (entriesLocation.readBytesFile != null && entriesLocation.readBytesFile.query_exists()) { + try { + var stream = new DataInputStream(entriesLocation.readBytesFile.read()); + readBytes = int64.parse(stream.read_line()); // Defaults to 0 + } catch (GLib.Error e) { + Log.w(e.message); + } + } - // Get the old version - int64 version = 0; - if (file.query_exists()) - { + // Write the new number of read bytes (= size of the entry file) + if (entriesLocation.readBytesFile != null) { try { - var stream = new DataInputStream(file.read()); - version = int64.parse(stream.read_line()); // Defaults to 0 + var size = entriesLocation.newEntriesFile.query_info("standard::size", FileQueryInfoFlags.NONE).get_size(); + if (readBytes >= size) return; + FileUtils.writeFile(entriesLocation.readBytesFile, size.to_string()); } catch (GLib.Error e) { Log.w(e.message); } } - // Write the new version + Log.d("Execute entries of " + entriesLocation.newEntriesFile.get_path()); + + // Execute the entries + var entriesMap = new Gee.HashMap<Json.Node, Entry>( + a => { return a.hash(); }, + (a, b) => { return a.equal(b); } + ); try { - FileUtils.writeFile(file, (version + 1).to_string()); - } catch (Error e) { + var stream = new DataInputStream(entriesLocation.newEntriesFile.read()); + stream.seek(readBytes, SeekType.SET); + string line; + while ((line = stream.read_line(null)) != null) { + var entryLine = Entry.fromLine(line); + if (entryLine == null) { + continue; + } + if ((keyPred == null || keyPred(entryLine.key)) && + (valuePred == null || valuePred(entryLine.value))) { + var key = entryLine.key; + var entry = entriesMap.@get(key); + if (entry == null || entryLine.datetime > entry.datetime) { + entriesMap.@set(key, entryLine); + } + } + } + } catch (GLib.Error e) { Log.w(e.message); } + var entries = new Gee.ArrayList<Entry>(); + entries.add_all(entriesMap.values); + executeEntries(entriesLocation, entries, extra); } - // Update stored entries - updateStoredEntries(entriesLocation, entries); -} + private void executeEntries(EntriesLocation entriesLocation, Gee.Collection<Entry> entries, T extra) + { + updateStoredEntries(entriesLocation, entries); -/** -* Initializes the monitor which watches the filesystem for updated entries and executes the -* corresponding actions. -* -* @param extra extra data passed to the [listeners]. -*/ -public void initMonitor(T extra) -{ - try { - var newEntriesDir = File.new_for_path(dir + "/new-entries"); - var parent = newEntriesDir.get_parent(); - if (!parent.query_exists()) - { - parent.make_directory_with_parents(); + var listener = getListener(entriesLocation.path); + if (listener == null) { + Log.e("Unknown action for path " + FileUtils.pathToString(entriesLocation.path)); + return; } - monitor = new DirectoryMonitor(newEntriesDir); - monitor.changed.connect(pathString => { - var pathEncoded = new Gee.ArrayList<string>.wrap(pathString.split("/")); - pathEncoded.remove(""); - if (pathEncoded.is_empty || pathEncoded.last()[0] == '.') - { - return; - } - var path = new Gee.ArrayList<string>(); - path.add_all_iterator(pathEncoded.map<string>(part => { return FileUtils.urldecode(part); })); - if (path.any_match(part => { return part == null; })) - { - Log.w("Cannot decode path " + pathString); - return; - } - var appId = path.first(); - path.remove_at(0); - var entriesLocation = new EntriesLocation.getNewEntriesLocation(this, path, appId); - if (appId != ownAppId && entriesLocation.newEntriesFile.query_file_type(FileQueryInfoFlags.NONE) == FileType.REGULAR) - { - executeEntriesLocation(entriesLocation, extra); - Log.d("Sync complete"); - syncComplete(extra); - } - }); - Log.d("Initialized folder monitor for " + dir + "/new-entries"); - } catch (GLib.Error e) { - Log.w(e.message); - } -} -/** -* Gets all updated entries and executes the corresponding actions. -* -* @param extra extra data passed to the [listeners]. -*/ -public void executeAllNewEntries(T extra) -{ - Log.d("Execute all new entries in " + dir); - var newEntriesDir = File.new_for_path(dir + "/new-entries"); - var readBytesDir = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded); - Gee.Predicate<Gee.List<string>> pathPred = path => { return path.is_empty || path.first() != ownAppId; }; - FileUtils.listFilesRecursiveRelative(newEntriesDir, readBytesDir, pathPred) - .map<EntriesLocation>(path => { return new EntriesLocation.getNewEntriesLocation(this, path.slice(1, path.size), path.first()); }) - .@foreach (entriesLocation => { - executeEntriesLocation(entriesLocation, extra); - return true; - }); - Log.d("Sync complete"); - syncComplete(extra); -} + listener.onEntriesUpdate(entriesLocation.path, entries, extra); + } -private void executeEntriesLocation(EntriesLocation entriesLocation, T extra, Gee.Predicate<Json.Node>? keyPred = null, Gee.Predicate<Json.Node>? valuePred = null) -{ - // Get the number of read bytes - int64 readBytes = 0; - if (entriesLocation.readBytesFile != null && entriesLocation.readBytesFile.query_exists()) + private void updateStoredEntries(EntriesLocation entriesLocation, Gee.Collection<Entry> entries) { - try { - var stream = new DataInputStream(entriesLocation.readBytesFile.read()); - readBytes = int64.parse(stream.read_line()); // Defaults to 0 - } catch (GLib.Error e) { - Log.w(e.message); + if (entriesLocation.storedEntriesFile == null) { + return; } - } - // Write the new number of read bytes (= size of the entry file) - if (entriesLocation.readBytesFile != null) - { try { - var size = entriesLocation.newEntriesFile.query_info("standard::size", FileQueryInfoFlags.NONE).get_size(); - if (readBytes >= size) - { - return; + var haveToFilterFile = false; + if (entriesLocation.storedEntriesFile.query_exists()) { + var stream = new DataInputStream(entriesLocation.storedEntriesFile.read()); + string line; + while ((line = stream.read_line(null)) != null) { + var entryLine = Entry.fromLine(line); + if (entryLine == null) { + continue; + } + var entriesIterator = entries.iterator(); + while (entriesIterator.has_next()) { + entriesIterator.next(); + var entry = entriesIterator.get(); + if (entry.key.equal(entryLine.key)) { + if (entry.datetime > entryLine.datetime) { + haveToFilterFile = true; + } else { + entriesIterator.remove(); + } + } + } + } } - FileUtils.writeFile(entriesLocation.readBytesFile, size.to_string()); - } catch (GLib.Error e) { - Log.w(e.message); - } - } - Log.d("Execute entries of " + entriesLocation.newEntriesFile.get_path()); - - // Execute the entries - var entriesMap = new Gee.HashMap<Json.Node, Entry>( - a => { return a.hash(); }, - (a, b) => { return a.equal(b); } - ); - try { - var stream = new DataInputStream(entriesLocation.newEntriesFile.read()); - stream.seek(readBytes, SeekType.SET); - string line; - while ((line = stream.read_line(null)) != null) { - var entryLine = Entry.fromLine(line); - if (entryLine == null) - { - continue; + if (haveToFilterFile) { + FileUtils.filterFile(entriesLocation.storedEntriesFile, line => { + var entryLine = Entry.fromLine(line); + if (entryLine == null) { + return false; + } + return entries.fold<bool>((entry, seed) => { return !entry.key.equal(entryLine.key) && seed; }, true); + }); } - if ((keyPred == null || keyPred(entryLine.key)) && - (valuePred == null || valuePred(entryLine.value))) - { - var key = entryLine.key; - var entry = entriesMap.@get(key); - if (entry == null || entryLine.datetime > entry.datetime) - { - entriesMap.@set(key, entryLine); + + var builder = new StringBuilder(); + entries.@foreach(entry => { + builder.append(entry.toLine() + "\n"); + return true; + }); + FileUtils.writeFile(entriesLocation.storedEntriesFile, builder.str, true); + + var maxDatetime = entries.fold<string?>((entry, seed) => { if (seed == null || entry.datetime > seed) return entry.datetime; else return seed; }, null); + if (maxDatetime != null) { + var latestStoredEntryFile = File.new_for_path(dir + "/info/" + ownAppIdEncoded + "/latest-stored-entry"); + string? latestDatetime = null; + try { + var stream = new DataInputStream(latestStoredEntryFile.read()); + latestDatetime = stream.read_line(); + } catch (GLib.Error e) { + Log.w(e.message); + } + if (latestDatetime == null || maxDatetime > latestDatetime) { + FileUtils.writeFile(latestStoredEntryFile, maxDatetime); } } } - } catch (GLib.Error e) { - Log.w(e.message); + catch (GLib.Error e) + { + Log.w(e.message); + } } - var entries = new Gee.ArrayList<Entry>(); - entries.add_all(entriesMap.values); - executeEntries(entriesLocation, entries, extra); -} - -private void executeEntries(EntriesLocation entriesLocation, Gee.Collection<Entry> entries, T extra) -{ - updateStoredEntries(entriesLocation, entries); - var listener = getListener(entriesLocation.path); - if (listener == null) + /** + * Gets all stored entries satisfying the predicates and executes the corresponding actions. + * + * @param executePath path to the entries to executes. This can be either a file or a directory. + * If it specifies a file, the entries in that file are executed. If it specifies a directory, + * all entries in all subfiles are executed. + * @param extra extra data passed to the [listeners]. + * @param keyPred optional predicate on the keys. The key has to satisfy this predicate to be + * executed. + * @param valuePred optional predicate on the values. The value has to satisfy this predicate to + * be executed. + * @param pathPred optional predicate on the subpaths. Each subpath has to satisfy this + * predicate to be executed. This holds for directories as well. Furthermore, the path of + * specified in [executePath] is not part of the argument. + */ + public void executeStoredEntries(string[] executePathArray, T extra, + Gee.Predicate<Json.Node>? keyPred = null, + Gee.Predicate<Json.Node>? valuePred = null, + Gee.Predicate<Gee.List<string>>? pathPred = null) { - Log.e("Unknown action for path " + FileUtils.pathToString(entriesLocation.path)); - return; + var executePath = toList(executePathArray); + var executePathString = FileUtils.pathToString(executePath); + var executeDir = File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded + "/" + executePathString); + FileUtils.listFilesRecursiveRelative(executeDir, null, pathPred) + .@foreach(path => { + path.insert_all(0, executePath); + var entriesLocation = new EntriesLocation.getStoredEntriesLocation(this, path); + executeEntriesLocation(entriesLocation, extra, keyPred, valuePred); + return true; + }); } - listener.onEntriesUpdate(entriesLocation.path, entries, extra); -} - -private void updateStoredEntries(EntriesLocation entriesLocation, Gee.Collection<Entry> entries) -{ - if (entriesLocation.storedEntriesFile == null) + /** + * Initializes the stored entries. This method does not execute any actions. This is often + * followed with a call to [executeStoredEntries]. + */ + public void initStoredEntries() { - return; - } + // Get the most up-to-date appId + var appId = latestAppId(); - try { - var haveToFilterFile = false; - if (entriesLocation.storedEntriesFile.query_exists()) - { - var stream = new DataInputStream(entriesLocation.storedEntriesFile.read()); - string line; - while ((line = stream.read_line(null)) != null) { - var entryLine = Entry.fromLine(line); - if (entryLine == null) - { - continue; - } - var entriesIterator = entries.iterator(); - while (entriesIterator.has_next()) { - entriesIterator.next(); - var entry = entriesIterator.get(); - if (entry.key.equal(entryLine.key)) - { - if (entry.datetime > entryLine.datetime) - { - haveToFilterFile = true; - } - else - { - entriesIterator.remove(); - } - } - } + // Copy the stored files and update the read bytes + if (appId != ownAppId) { + var appIdEncoded = FileUtils.urlencode(appId); + + try { + FileUtils.@delete(File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded)); + FileUtils.copy(File.new_for_path(dir + "/stored-entries/" + appIdEncoded), File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded)); + } catch (GLib.Error e) { + Log.w(e.message); } - } - if (haveToFilterFile) - { - FileUtils.filterFile(entriesLocation.storedEntriesFile, line => { - var entryLine = Entry.fromLine(line); - if (entryLine == null) - { - return false; + try { + FileUtils.@delete(File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded)); + FileUtils.copy(File.new_for_path(dir + "/read-bytes/" + appIdEncoded), File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded)); + } catch (GLib.Error e) { + Log.w(e.message); + } + var newEntriesDir = File.new_for_path(dir + "/new-entries/" + appIdEncoded); + var ownReadBytesDir = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded + "/" + appIdEncoded); + FileUtils.listFilesRecursiveRelative(newEntriesDir, ownReadBytesDir).@foreach(path => { + var pathString = FileUtils.pathToString(path); + try { + var newEntriesFile = File.new_for_path(dir + "/new-entries/" + appIdEncoded + "/" + pathString); + var size = newEntriesFile.query_info("standard::size", FileQueryInfoFlags.NONE).get_size(); + var readBytesFile = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded + "/" + appIdEncoded + "/" + pathString); + FileUtils.writeFile(readBytesFile, size.to_string()); + } catch (GLib.Error e) { + Log.w(e.message); } - return !entries.any_match(entry => { return entry.key.equal(entryLine.key); }); + return true; }); } + } - var builder = new StringBuilder(); - entries.@foreach(entry => { - builder.append(entry.toLine() + "\n"); - return true; - }); - FileUtils.writeFile(entriesLocation.storedEntriesFile, builder.str, true); - } - catch (GLib.Error e) + /** + * Returns the most up-to-date appId. This is the appId which has stored the most recent entry. + * In case of a tie, the appId corresponding to the current application is used, if possible. + */ + public string latestAppId() { - Log.w(e.message); - } -} + string? latestAppId = null; + string? latestDatetime = null; + var infoDir = File.new_for_path(dir + "/info"); + try { + var enumerator = infoDir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); + FileInfo info; + while ((info = enumerator.next_file(null)) != null) { + if (info.get_name()[0] == '.') { + continue; + } -/** -* Gets all stored entries satisfying the predicates and executes the corresponding actions. -* -* @param executePath path to the entries to executes. This can be either a file or a directory. -* If it specifies a file, the entries in that file are executed. If it specifies a directory, -* all entries in all subfiles are executed. -* @param extra extra data passed to the [listeners]. -* @param keyPred optional predicate on the keys. The key has to satisfy this predicate to be -* executed. -* @param valuePred optional predicate on the values. The value has to satisfy this predicate to -* be executed. -* @param pathPred optional predicate on the subpaths. Each subpath has to satisfy this -* predicate to be executed. This holds for directories as well. Furthermore, the path of -* specified in [executePath] is not part of the argument. -*/ -public void executeStoredEntries(string[] executePathArray, T extra, - Gee.Predicate<Json.Node>? keyPred = null, - Gee.Predicate<Json.Node>? valuePred = null, -Gee.Predicate<Gee.List<string>>? pathPred = null) -{ - var executePath = toList(executePathArray); - var executePathString = FileUtils.pathToString(executePath); - var executeDir = File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded + "/" + executePathString); - FileUtils.listFilesRecursiveRelative(executeDir, null, pathPred) - .@foreach(path => { - path.insert_all(0, executePath); - var entriesLocation = new EntriesLocation.getStoredEntriesLocation(this, path); - executeEntriesLocation(entriesLocation, extra, keyPred, valuePred); - return true; - }); -} + var appId = FileUtils.urldecode(info.get_name()); + var file = File.new_for_path(dir + "/info/" + info.get_name() + "/latest-stored-entry"); -/** -* Initializes the stored entries. This method does not execute any actions. This is often -* followed with a call to [executeStoredEntries]. -*/ -public void initStoredEntries() -{ - // Get the most up-to-date appId - string? appId = null; - string? maxDatetime = null; - FileUtils.listFilesRecursiveRelative(File.new_for_path(dir + "/stored-entries")) - .filter(path => { return !path.is_empty; }) - .@foreach(path => { - var pathString = FileUtils.pathToString(path); - try { - var file = File.new_for_path(dir + "/stored-entries/" + pathString); - var stream = new DataInputStream(file.read()); - string line; - while ((line = stream.read_line(null)) != null) { - var entry = Entry.fromLine(line); - if (entry == null) + if (appId == null || + !file.query_exists() || + file.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) { continue; } - if (maxDatetime == null || entry.datetime > maxDatetime || - path.first() == ownAppId && entry.datetime == maxDatetime) // Prefer own appId + + string? datetime = null; + try { + var stream = new DataInputStream(file.read()); + datetime = stream.read_line(); + } catch (GLib.Error e) { + Log.w(e.message); + } + if (datetime > latestDatetime || + appId == ownAppId && datetime == latestDatetime) { - maxDatetime = entry.datetime; - appId = path.first(); + latestDatetime = datetime; + latestAppId = appId; } } } catch (GLib.Error e) { Log.w(e.message); } - return true; - }); - if (appId == null) - { - Log.i("No appId found for initialization"); - return; + + return latestAppId ?? ownAppId; } - // Copy the stored files and update the read bytes - if (appId != ownAppId) + /** + * Returns the value of the given [key] in the map of the given [path], and in the given + * [DecSync directory][decsyncDir] without specifying an appId, or `null` if there is no + * such value. The use of this method is discouraged. It is recommended to use the method + * [executeStoredEntries] when possible. + * + * @throws DecsyncException if a DecSync configuration error occurred. + */ + public static Json.Node? getStoredStaticValue(string decsyncDir, string[] pathArray, Json.Node key) throws DecsyncError { - var appIdEncoded = FileUtils.urlencode(appId); - + Log.d("Get value for key " + Json.to_string(key, false) + " for path " + string.joinv("/", pathArray) + " in " + decsyncDir); + checkDecsyncSubdirInfo(decsyncDir); + var path = toList(pathArray); + var pathString = FileUtils.pathToString(path); + Json.Node? result = null; + string? maxDatetime = null; + var storedEntriesDir = File.new_for_path(decsyncDir + "/stored-entries"); try { - FileUtils.@delete(File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded)); - FileUtils.copy(File.new_for_path(dir + "/stored-entries/" + appIdEncoded), File.new_for_path(dir + "/stored-entries/" + ownAppIdEncoded)); - } catch (GLib.Error e) { - Log.w(e.message); - } + var enumerator = storedEntriesDir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); + FileInfo info; + while ((info = enumerator.next_file(null)) != null) { + if (info.get_name()[0] == '.') { + continue; + } - try { - FileUtils.@delete(File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded)); - FileUtils.copy(File.new_for_path(dir + "/read-bytes/" + appIdEncoded), File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded)); + var appIdEncoded = info.get_name(); + var file = File.new_for_path(decsyncDir + "/stored-entries/" + appIdEncoded + "/" + pathString); + if (!file.query_exists() || file.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) { + continue; + } + + var stream = new DataInputStream(file.read()); + string line; + while ((line = stream.read_line(null)) != null) { + var entry = Entry.fromLine(line); + if (entry == null) { + continue; + } + if (entry.key.equal(key) && (maxDatetime == null || entry.datetime > maxDatetime)) { + maxDatetime = entry.datetime; + result = entry.value; + } + } + } } catch (GLib.Error e) { Log.w(e.message); } - var newEntriesDir = File.new_for_path(dir + "/new-entries/" + appIdEncoded); - var ownReadBytesDir = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded + "/" + appIdEncoded); - FileUtils.listFilesRecursiveRelative(newEntriesDir, ownReadBytesDir).@foreach(path => { - var pathString = FileUtils.pathToString(path); - try { - var newEntriesFile = File.new_for_path(dir + "/new-entries/" + appIdEncoded + "/" + pathString); - var size = newEntriesFile.query_info("standard::size", FileQueryInfoFlags.NONE).get_size(); - var readBytesFile = File.new_for_path(dir + "/read-bytes/" + ownAppIdEncoded + "/" + appIdEncoded + "/" + pathString); - FileUtils.writeFile(readBytesFile, size.to_string()); - } catch (GLib.Error e) { - Log.w(e.message); - } - return true; - }); - } -} -/** -* Returns the value of the given [key] in the map of the given [path], and in the given -* [DecSync directory][decsyncDir] without specifying an appId, or `null` if there is no -* such value. The use of this method is discouraged. It is recommended to use the method -* [executeStoredEntries] when possible. -*/ -public static Json.Node? getStoredStaticValue(string decsyncDir, string[] pathArray, Json.Node key) -{ - Log.d("Get value for key " + Json.to_string(key, false) + " for path " + string.joinv("/", pathArray) + " in " + decsyncDir); - var path = toList(pathArray); - var pathString = FileUtils.pathToString(path); - Json.Node? result = null; - string? maxDatetime = null; - var storedEntriesDir = File.new_for_path(decsyncDir + "/stored-entries"); - try { - var enumerator = storedEntriesDir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); - FileInfo info; - while ((info = enumerator.next_file(null)) != null) { - if (info.get_name()[0] == '.') - { - continue; - } - - var appIdEncoded = info.get_name(); - var file = File.new_for_path(decsyncDir + "/stored-entries/" + appIdEncoded + "/" + pathString); - if (!file.query_exists() || file.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) - { - continue; - } + return result; + } - var stream = new DataInputStream(file.read()); - string line; - while ((line = stream.read_line(null)) != null) { - var entry = Entry.fromLine(line); - if (entry == null) - { - continue; - } - if (entry.key.equal(key) && (maxDatetime == null || entry.datetime > maxDatetime)) - { - maxDatetime = entry.datetime; - result = entry.value; - } + private OnEntryUpdateListener<T>? getListener(Gee.List<string> path) + { + foreach (var listener in listeners) { + if (listener.matchesPath(path)) { + return listener; } } - } catch (GLib.Error e) { - Log.w(e.message); + return null; } +} - return result; +private void checkDecsyncSubdirInfo(string decsyncSubdir) throws DecsyncError +{ + var syncTypes = new Gee.ArrayList<string>.wrap({"rss", "contacts", "calendars"}); + var file = File.new_for_path(decsyncSubdir); + File? decsyncDir = null; + if (syncTypes.contains(file.get_basename())) { + decsyncDir = file.get_parent(); + } else if (syncTypes.contains(file.get_parent().get_basename())) { + decsyncDir = file.get_parent().get_parent(); + } + if (decsyncDir != null) { + checkDecsyncInfo(decsyncDir.get_path()); + } } -private OnEntryUpdateListener<T>? getListener(Gee.List<string> path) +/** + * Checks whether the .decsync-info file in [decsyncDir] is of the right format and contains a + * supported version. If it does not exist, a new one with version 1 is created. + * + * @throws DecsyncException if a DecSync configuration error occurred. + */ +public void checkDecsyncInfo(string decsyncDir) throws DecsyncError { - foreach (var listener in listeners) { - if (listener.matchesPath(path)) - { - return listener; + var infoFile = File.new_for_path(decsyncDir).get_child(".decsync-info"); + if (infoFile.query_exists()) { + int64 version; + try { + var stream = new DataInputStream(infoFile.read()); + var text = stream.read_line(); + var obj = Json.from_string(text).get_object(); + version = obj.get_int_member("version"); + } catch (GLib.Error e) { + throw new DecsyncError.INVALID_INFO("Invalid .decsync-info.\n" + e.message); + } + if (version != 1) { + throw new DecsyncError.UNSUPPORTED_VERSION("Unsupported DecSync version.\n" + + "Required version: " + version.to_string() + ".\n" + + "Supported version: 1."); + } + } else { + var obj = new Json.Object(); + obj.set_int_member("version", 1); + var json = new Json.Node(Json.NodeType.OBJECT); + json.set_object(obj); + var text = Json.to_string(json, false); + try { + FileUtils.writeFile(infoFile, text); + } catch (GLib.Error e) { + throw new DecsyncError.INVALID_INFO("Could not write .decsync-info.\n" + e.message); } } - return null; -} } /** -* Returns the path to the DecSync subdirectory in a [decsyncBaseDir] for a [syncType] and -* optionally with a [collection]. -* -* @param decsyncBaseDir the path to the main DecSync directory, or null for the default one. -* @param syncType the type of data to sync. For example, "rss", "contacts" or "calendars". -* @param collection an optional collection identifier when multiple instances of the [syncType] are -* supported. For example, this is the case for "contacts" and "calendars", but not for "rss". -*/ + * Returns the path to the DecSync subdirectory in a [decsyncBaseDir] for a [syncType] and + * optionally with a [collection]. + * + * @param decsyncBaseDir the path to the main DecSync directory, or null for the default one. + * @param syncType the type of data to sync. For example, "rss", "contacts" or "calendars". + * @param collection an optional collection identifier when multiple instances of the [syncType] are + * supported. For example, this is the case for "contacts" and "calendars", but not for "rss". + */ public string getDecsyncSubdir(string? decsyncBaseDir, string syncType, string? collection = null) { -string dir = decsyncBaseDir ?? getDefaultDecsyncBaseDir(); -dir += "/" + FileUtils.urlencode(syncType); -if (collection != null) -{ - dir += "/" + FileUtils.urlencode(collection); -} -return dir; + string dir = decsyncBaseDir ?? getDefaultDecsyncBaseDir(); + dir += "/" + FileUtils.urlencode(syncType); + if (collection != null) { + dir += "/" + FileUtils.urlencode(collection); + } + return dir; } /** -* Returns the default DecSync directory. This is the "decsync" subdirectory on the user data dir -* ("~/.local/share" by default). -*/ + * Returns the default DecSync directory. This is the "decsync" subdirectory on the user data dir + * ("~/.local/share" by default). + */ public string getDefaultDecsyncBaseDir() { -return GLib.Environment.get_user_data_dir() + "/decsync"; + return GLib.Environment.get_user_data_dir() + "/decsync"; } /** -* Returns a list of DecSync collections inside a [decsyncBaseDir] for a [syncType]. This function -* does not apply for sync types with single instances. -* -* @param decsyncBaseDir the path to the main DecSync directory, or null for the default one. -* @param syncType the type of data to sync. For example, "contacts" or "calendars". -* @param ignoreDeleted `true` to ignore deleted collections. A collection is considered deleted if -* the most recent value of the key "deleted" with the path ["info"] is set to `true`. -*/ + * Returns a list of DecSync collections inside a [decsyncBaseDir] for a [syncType]. This function + * does not apply for sync types with single instances. + * + * @param decsyncBaseDir the path to the main DecSync directory, or null for the default one. + * @param syncType the type of data to sync. For example, "contacts" or "calendars". + * @param ignoreDeleted `true` to ignore deleted collections. A collection is considered deleted if + * the most recent value of the key "deleted" with the path ["info"] is set to `true`. + * @throws DecsyncException if a DecSync configuration error occurred. + */ public Gee.ArrayList<string> listDecsyncCollections(string? decsyncBaseDir, string syncType, bool ignoreDeleted = true) throws GLib.Error { -var decsyncSubdir = File.new_for_path(getDecsyncSubdir(decsyncBaseDir, syncType)); -var enumerator = decsyncSubdir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); -FileInfo info; -Gee.ArrayList<string> result = new Gee.ArrayList<string>(); -while ((info = enumerator.next_file(null)) != null) { - if (info.get_file_type() != FileType.DIRECTORY || info.get_name()[0] == '.') - { - continue; - } - if (ignoreDeleted) - { - var deleted = Decsync.getStoredStaticValue(decsyncSubdir.get_child(info.get_name()).get_path(), {"info"}, stringToNode("deleted")); - if (deleted != null && deleted.get_boolean()) - { + checkDecsyncInfo(decsyncBaseDir ?? getDefaultDecsyncBaseDir()); + var decsyncSubdir = File.new_for_path(getDecsyncSubdir(decsyncBaseDir, syncType)); + var enumerator = decsyncSubdir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); + FileInfo info; + Gee.ArrayList<string> result = new Gee.ArrayList<string>(); + while ((info = enumerator.next_file(null)) != null) { + if (info.get_file_type() != FileType.DIRECTORY || info.get_name()[0] == '.') { continue; } + if (ignoreDeleted) { + var deleted = Decsync.getStoredStaticValue(decsyncSubdir.get_child(info.get_name()).get_path(), {"info"}, stringToNode("deleted")); + if (deleted != null && deleted.get_boolean()) { + continue; + } + } + var collection = FileUtils.urldecode(info.get_name()); + if (collection != null) { + result.add(collection); + } } - var collection = FileUtils.urldecode(info.get_name()); - if (collection != null) - { - result.add(collection); - } -} -return result; + return result; } /** -* Returns the appId of the current device and application combination. -* -* @param appName the name of the application. -* @param id an optional integer (between 0 and 100000 exclusive) to distinguish different instances -* on the same device and application. -*/ + * Returns the appId of the current device and application combination. + * + * @param appName the name of the application. + * @param id an optional integer (between 0 and 100000 exclusive) to distinguish different instances + * on the same device and application. + */ public string getAppId(string appName, int? id = null) { -string appId = GLib.Environment.get_host_name() + "-" + appName; -if (id == null) -{ - return appId; -} -else -{ - return appId + "-" + "%05d".printf(id); -} + string appId = GLib.Environment.get_host_name() + "-" + appName; + if (id == null) { + return appId; + } else { + return appId + "-" + "%05d".printf(id); + } } diff --git a/plugins/backend/decsync/libdecsync/src/DirectoryMonitor.vala b/plugins/backend/decsync/libdecsync/src/DirectoryMonitor.vala index a125d26c..e08f6f46 100644 --- a/plugins/backend/decsync/libdecsync/src/DirectoryMonitor.vala +++ b/plugins/backend/decsync/libdecsync/src/DirectoryMonitor.vala @@ -1,20 +1,20 @@ /** -* libdecsync-vala - DirectoryMonitor.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ + * libdecsync-vala - DirectoryMonitor.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ public class DirectoryMonitor : GLib.Object { @@ -37,8 +37,7 @@ public class DirectoryMonitor : GLib.Object { var currentDir = File.new_for_path(dir.get_path() + path); mMonitor = currentDir.monitor_directory(FileMonitorFlags.NONE); mMonitor.changed.connect((file, otherFile, event) => { - if (file.get_path() != mDir.get_path() + path) - { + if (file.get_path() != mDir.get_path() + path) { onEvent(path + "/" + file.get_basename(), event); } }); @@ -47,8 +46,7 @@ public class DirectoryMonitor : GLib.Object { var enumerator = currentDir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); FileInfo info = null; while (((info = enumerator.next_file(null)) != null)) { - if (info.get_file_type() == FileType.DIRECTORY) - { + if (info.get_file_type() == FileType.DIRECTORY) { var childMonitor = new DirectoryMonitor.withPath(mDir, path + "/" + info.get_name()); childMonitor.changed.connect((path) => { changed(path); @@ -60,36 +58,32 @@ public class DirectoryMonitor : GLib.Object { private void onEvent(string path, FileMonitorEvent event) { - Log.d("Received inotify event " + event.to_string() + " at " + mDir.get_path() + "/" + path); + Log.d("Received inotify event " + event.to_string() + " at " + mDir.get_path() + "/" + path); switch (event) { case FileMonitorEvent.DELETED: - foreach (var c in mChilds) { - if (c.mPath == path) - { - mChilds.remove(c); - break; + foreach (var c in mChilds) { + if (c.mPath == path) { + mChilds.remove(c); + break; + } } - } - break; + break; case FileMonitorEvent.CREATED: case FileMonitorEvent.CHANGED: - var file = File.new_for_path(mDir.get_path() + path); - if (file.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) - { - try { - var childMonitor = new DirectoryMonitor.withPath(mDir, path); - childMonitor.changed.connect((path) => { - changed(path); - }); - mChilds.add(childMonitor); - } catch (GLib.Error e) { - Log.w(e.message); + var file = File.new_for_path(mDir.get_path() + path); + if (file.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) { + try { + var childMonitor = new DirectoryMonitor.withPath(mDir, path); + childMonitor.changed.connect((path) => { + changed(path); + }); + mChilds.add(childMonitor); + } catch (GLib.Error e) { + Log.w(e.message); + } + } else { + changed(path); } - } - else - { - changed(path); - } break; } } diff --git a/plugins/backend/decsync/libdecsync/src/FileUtils.vala b/plugins/backend/decsync/libdecsync/src/FileUtils.vala index 1445e728..6f40e2b4 100644 --- a/plugins/backend/decsync/libdecsync/src/FileUtils.vala +++ b/plugins/backend/decsync/libdecsync/src/FileUtils.vala @@ -1,40 +1,35 @@ /** -* libdecsync-vala - FileUtils.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ + * libdecsync-vala - FileUtils.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ public class FileUtils : GLib.Object { public static void writeFile(File file, string content, bool append = false) throws GLib.Error { var parent = file.get_parent(); - if (!parent.query_exists()) - { + if (!parent.query_exists()) { parent.make_directory_with_parents(); } GLib.FileOutputStream stream; - if (append) - { + if (append) { stream = file.append_to(FileCreateFlags.NONE); - } - else - { - if (file.query_exists()) - { + } else { + if (file.query_exists()) { file.@delete(); } stream = file.create(FileCreateFlags.REPLACE_DESTINATION); @@ -44,12 +39,10 @@ public class FileUtils : GLib.Object { public static void @delete(File src) throws GLib.Error { - if (!src.query_exists()) - { + if (!src.query_exists()) { return; } - if (src.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) - { + if (src.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) { var enumerator = src.enumerate_children("standard::name", FileQueryInfoFlags.NONE); FileInfo info; while ((info = enumerator.next_file(null)) != null) { @@ -64,22 +57,21 @@ public class FileUtils : GLib.Object { { switch (src.query_file_type(FileQueryInfoFlags.NONE)) { case FileType.REGULAR: - var parent = dst.get_parent(); - if (!parent.query_exists()) - { - parent.make_directory_with_parents(); - } - src.copy(dst, overwrite ? FileCopyFlags.OVERWRITE : FileCopyFlags.NONE); - return; + var parent = dst.get_parent(); + if (!parent.query_exists()) { + parent.make_directory_with_parents(); + } + src.copy(dst, overwrite ? FileCopyFlags.OVERWRITE : FileCopyFlags.NONE); + return; case FileType.DIRECTORY: - dst.make_directory_with_parents(); - var enumerator = src.enumerate_children("standard::name", FileQueryInfoFlags.NONE); - FileInfo info; - while ((info = enumerator.next_file(null)) != null) { - var name = info.get_name(); - copy(src.get_child(name), dst.get_child(name), overwrite); - } - return; + dst.make_directory_with_parents(); + var enumerator = src.enumerate_children("standard::name", FileQueryInfoFlags.NONE); + FileInfo info; + while ((info = enumerator.next_file(null)) != null) { + var name = info.get_name(); + copy(src.get_child(name), dst.get_child(name), overwrite); + } + return; } } @@ -90,8 +82,7 @@ public class FileUtils : GLib.Object { var outstream = new DataOutputStream(tempFile.create(FileCreateFlags.NONE)); string line; while ((line = instream.read_line(null)) != null) { - if (linePred(line)) - { + if (linePred(line)) { outstream.put_string(line + "\n"); } } @@ -100,151 +91,127 @@ public class FileUtils : GLib.Object { public static Gee.ArrayList<Gee.ArrayList<string>> listFilesRecursiveRelative(File src, File? readBytesSrc = null, Gee.Predicate<Gee.List<string>>? pathPred = null) { - if (src.get_basename()[0] == '.') - { + if (src.get_basename()[0] == '.') { return new Gee.ArrayList<Gee.ArrayList<string>>(); } - if (pathPred != null && !pathPred(new Gee.ArrayList<string>())) - { + if (pathPred != null && !pathPred(new Gee.ArrayList<string>())) { return new Gee.ArrayList<Gee.ArrayList<string>>(); } switch (src.query_file_type(FileQueryInfoFlags.NONE)) { case FileType.REGULAR: - var result = new Gee.ArrayList<Gee.ArrayList<string>>(); - result.add(new Gee.ArrayList<string>()); - return result; + var result = new Gee.ArrayList<Gee.ArrayList<string>>(); + result.add(new Gee.ArrayList<string>()); + return result; case FileType.DIRECTORY: - // Skip same versions - if (readBytesSrc != null) - { - var file = src.get_child(".decsync-sequence"); - string? version = null; - if (file.query_exists()) - { - try { - version = new DataInputStream(file.read()).read_line(); - } catch (GLib.Error e) { - Log.w(e.message); - } - } - var readBytesFile = readBytesSrc.get_child(".decsync-sequence"); - string? readBytesVersion = null; - if (readBytesFile.query_exists()) - { - try { - readBytesVersion = new DataInputStream(readBytesFile.read()).read_line(); - } catch (GLib.Error e) { - Log.w(e.message); - } - } - if (version != null) - { - if (version == readBytesVersion) - { - return new Gee.ArrayList<Gee.ArrayList<string>>(); + // Skip same versions + if (readBytesSrc != null) { + var file = src.get_child(".decsync-sequence"); + string? version = null; + if (file.query_exists()) { + try { + version = new DataInputStream(file.read()).read_line(); + } catch (GLib.Error e) { + Log.w(e.message); + } } - else - { + var readBytesFile = readBytesSrc.get_child(".decsync-sequence"); + string? readBytesVersion = null; + if (readBytesFile.query_exists()) { try { - copy(file, readBytesFile, true); + readBytesVersion = new DataInputStream(readBytesFile.read()).read_line(); } catch (GLib.Error e) { Log.w(e.message); } } + if (version != null) { + if (version == readBytesVersion) { + return new Gee.ArrayList<Gee.ArrayList<string>>(); + } else { + try { + copy(file, readBytesFile, true); + } catch (GLib.Error e) { + Log.w(e.message); + } + } + } } - } - var result = new Gee.ArrayList<Gee.ArrayList<string>>(); - try { - var enumerator = src.enumerate_children("standard::name", FileQueryInfoFlags.NONE); - FileInfo info; - while ((info = enumerator.next_file(null)) != null) { - string name = info.get_name(); - string? nameDecoded = urldecode(name); - if (nameDecoded == null) - { - Log.w("Cannot decode name " + name); - continue; - } + var result = new Gee.ArrayList<Gee.ArrayList<string>>(); + try { + var enumerator = src.enumerate_children("standard::name", FileQueryInfoFlags.NONE); + FileInfo info; + while ((info = enumerator.next_file(null)) != null) { + string name = info.get_name(); + string? nameDecoded = urldecode(name); + if (nameDecoded == null) { + Log.w("Cannot decode name " + name); + continue; + } - var newReadBytesSrc = readBytesSrc == null ? null : readBytesSrc.get_child(name); - Gee.Predicate<Gee.List<string>>? newPathPred = null; - if (pathPred != null) - { - newPathPred = path => { path.insert(0, nameDecoded); return pathPred(path); }; - } - var paths = listFilesRecursiveRelative(src.get_child(name), newReadBytesSrc, newPathPred); - foreach (var path in paths) { - path.insert(0, nameDecoded); + var newReadBytesSrc = readBytesSrc == null ? null : readBytesSrc.get_child(name); + Gee.Predicate<Gee.List<string>>? newPathPred = null; + if (pathPred != null) { + newPathPred = path => { path.insert(0, nameDecoded); return pathPred(path); }; + } + var paths = listFilesRecursiveRelative(src.get_child(name), newReadBytesSrc, newPathPred); + foreach (var path in paths) { + path.insert(0, nameDecoded); + } + result.add_all(paths); } - result.add_all(paths); + } catch (GLib.Error e) { + Log.w(e.message); } - } catch (GLib.Error e) { - Log.w(e.message); - } - return result; + return result; default: - return new Gee.ArrayList<Gee.ArrayList<string>>(); + return new Gee.ArrayList<Gee.ArrayList<string>>(); } } - public static string pathToString(Gee.List<string> path) - { - var encodedPath = new Gee.ArrayList<string>(); - encodedPath.add_all_iterator(path.map<string>(part => { return urlencode(part); })); - return string.joinv("/", encodedPath.to_array()); - } + public static string pathToString(Gee.List<string> path) + { + var encodedPath = new Gee.ArrayList<string>(); + encodedPath.add_all_iterator(path.map<string>(part => { return urlencode(part); })); + return string.joinv("/", encodedPath.to_array()); + } - public static string urlencode(string input) - { - var builder = new StringBuilder(); - for (int i = 0; i < input.length; i++) { - char byte = input[i]; - if (byte.isalnum() || "-_.~".contains(byte.to_string())) - { - builder.append_c(byte); - } - else - { - builder.append("%%%2X".printf(byte)); - } - } - var output = builder.str; + public static string urlencode(string input) + { + var builder = new StringBuilder(); + for (int i = 0; i < input.length; i++) { + char byte = input[i]; + if (byte.isalnum() || "-_.~".contains(byte.to_string())) { + builder.append_c(byte); + } else { + builder.append("%%%2X".printf(byte)); + } + } + var output = builder.str; - if (output != "" && output[0] == '.') - { - output = "%2E" + output.substring(1); - } + if (output != "" && output[0] == '.') { + output = "%2E" + output.substring(1); + } - return output; - } + return output; + } - public static string? urldecode(string input) - { - var builder = new StringBuilder(); - for (int i = 0; i < input.length; i++) { - char byte = input[i]; - if (byte != '%') - { - builder.append_c(byte); - } - else - { - if (i + 2 >= input.length) - { - return null; - } - if (!input[i+1].isxdigit() || !input[i+2].isxdigit()) - { - return null; - } - char value1 = (char)input[i+1].xdigit_value(); - char value2 = (char)input[i+2].xdigit_value(); - builder.append_c(16 * value1 + value2); - i += 2; - } - } - return builder.str; - } + public static string? urldecode(string input) + { + var builder = new StringBuilder(); + for (int i = 0; i < input.length; i++) { + char byte = input[i]; + if (byte != '%') { + builder.append_c(byte); + } else { + if (i + 2 >= input.length) return null; + if (!input[i+1].isxdigit() || !input[i+2].isxdigit()) return null; + char value1 = (char)input[i+1].xdigit_value(); + char value2 = (char)input[i+2].xdigit_value(); + builder.append_c(16 * value1 + value2); + i += 2; + } + } + return builder.str; + } } diff --git a/plugins/backend/decsync/libdecsync/src/Log.vala b/plugins/backend/decsync/libdecsync/src/Log.vala index 906ecdd0..2420dce0 100644 --- a/plugins/backend/decsync/libdecsync/src/Log.vala +++ b/plugins/backend/decsync/libdecsync/src/Log.vala @@ -1,20 +1,20 @@ /** -* libdecsync-vala - Log.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ + * libdecsync-vala - Log.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ public class Log : GLib.Object { const string TAG = "DecSync"; diff --git a/plugins/backend/decsync/libdecsync/src/OnEntryUpdateListener.vala b/plugins/backend/decsync/libdecsync/src/OnEntryUpdateListener.vala index ca4d21a1..07d8e97b 100644 --- a/plugins/backend/decsync/libdecsync/src/OnEntryUpdateListener.vala +++ b/plugins/backend/decsync/libdecsync/src/OnEntryUpdateListener.vala @@ -1,20 +1,20 @@ /** -* libdecsync-vala - Subpath.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ + * libdecsync-vala - Subpath.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ public interface OnEntryUpdateListener<T> : GLib.Object { public abstract bool matchesPath(Gee.List<string> path); @@ -64,13 +64,11 @@ public abstract class OnSubfileEntryUpdateListener<T> : GLib.Object, OnEntryUpda private bool pathEquals(Gee.List<string> path1, Gee.List<string> path2) { - if (path1.size != path2.size) - { + if (path1.size != path2.size) { return false; } for (var i = 0; i < path1.size; ++i) { - if (path1[i] != path2[i]) - { + if (path1[i] != path2[i]) { return false; } } diff --git a/plugins/backend/decsync/libdecsync/src/Utils.vala b/plugins/backend/decsync/libdecsync/src/Utils.vala index 3c288889..92d17042 100644 --- a/plugins/backend/decsync/libdecsync/src/Utils.vala +++ b/plugins/backend/decsync/libdecsync/src/Utils.vala @@ -1,20 +1,20 @@ /** -* libdecsync-vala - Utils.vala -* -* Copyright (C) 2018 Aldo Gunsing -* -* This library is free software; you can redistribute it and/or modify it -* under the terms of the GNU Lesser General Public License as published by -* the Free Software Foundation. -* -* This library 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. -* -* You should have received a copy of the GNU Lesser General Public License -* along with this library; if not, see <http://www.gnu.org/licenses/>. -*/ + * libdecsync-vala - Utils.vala + * + * Copyright (C) 2018 Aldo Gunsing + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation. + * + * This library 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. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <http://www.gnu.org/licenses/>. + */ public Gee.List<string> toList(string[] input) { @@ -23,9 +23,9 @@ public Gee.List<string> toList(string[] input) public Gee.Predicate<Json.Node> stringEquals(string input) { - return json => { - return json.get_string() == input; - }; + return json => { + return json.get_string() == input; + }; } public Json.Node boolToNode(bool input) @@ -38,12 +38,9 @@ public Json.Node boolToNode(bool input) public Json.Node stringToNode(string? input) { Json.Node node; - if (input == null) - { + if (input == null) { node = new Json.Node(Json.NodeType.NULL); - } - else - { + } else { node = new Json.Node(Json.NodeType.VALUE); node.set_string(input); } @@ -57,6 +54,7 @@ public Json.Node objectToNode(Json.Object input) return node; } +[Version (deprecated = true, deprecated_since = "1.1.1", replacement = "groupByPath")] public Gee.MultiMap<K, V> groupBy<T, K, V>(Gee.Collection<T> inputs, Gee.MapFunc<K, T> k, Gee.MapFunc<V, T>? f = null) { var resultsMap = new Gee.HashMultiMap<K, V>(); @@ -69,3 +67,29 @@ public Gee.MultiMap<K, V> groupBy<T, K, V>(Gee.Collection<T> inputs, Gee.MapFunc return resultsMap; } + +public int pathCompare(Gee.List<string> lhs, Gee.List<string> rhs) +{ + for (int i = 0; i < lhs.size && i < rhs.size; ++i) { + if (lhs[i] < rhs[i]) return -1; + if (lhs[i] > rhs[i]) return 1; + } + if (lhs.size < rhs.size) return -1; + if (lhs.size > rhs.size) return 1; + return 0; +} + +public Gee.MultiMap<Gee.List<string>, V> groupByPath<T, V>( + Gee.Collection<T> inputs, + Gee.MapFunc<Gee.List<string>, T> toPath, + Gee.MapFunc<V, T>? f = null) +{ + var resultsMap = new Gee.TreeMultiMap<Gee.List<string>, V>(pathCompare); + foreach (var input in inputs) + { + var path = toPath(input); + var value = f == null ? input : f(input); + resultsMap.@set(path, value); + } + return resultsMap; +} |