From 984a99f88f1bf94580e28f2aa5478496b90b2d73 Mon Sep 17 00:00:00 2001 From: Aldo Gunsing Date: Thu, 26 Sep 2019 22:10:18 +0200 Subject: Squashed 'plugins/backend/decsync/libdecsync/' changes from 30681106..8e566326 8e566326 Bump version to 1.2.0 d510f661 Add .decsync-info check e855291a Bump version to 1.1.1 3408ae37 Replace groupBy with groupByPath a953724b Bump version to 1.1.0 51d8c6e1 Add latestAppId method 425f02b1 Add support for libgee < 0.19 git-subtree-dir: plugins/backend/decsync/libdecsync git-subtree-split: 8e566326dfb6f0630ba943bcc5b4cac4d70c3449 --- meson.build | 5 +- src/Decsync.vala | 170 +++++++++++++++++++++++++++++++++++++++++++------------ src/Utils.vala | 27 +++++++++ 3 files changed, 166 insertions(+), 36 deletions(-) diff --git a/meson.build b/meson.build index f701917f..b694ddfe 100644 --- a/meson.build +++ b/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/src/Decsync.vala b/src/Decsync.vala index 32795ea6..3f3495ce 100644 --- a/src/Decsync.vala +++ b/src/Decsync.vala @@ -18,6 +18,11 @@ 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. @@ -63,6 +68,7 @@ public class Unit { public Unit() {} } * 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 : GLib.Object { @@ -77,12 +83,14 @@ public class Decsync : GLib.Object { */ public signal void syncComplete(T extra); - public Decsync(string dir, string ownAppId, Gee.Iterable> listeners) + public Decsync(string dir, string ownAppId, Gee.Iterable> listeners) throws DecsyncError { this.dir = dir; this.ownAppId = ownAppId; this.ownAppIdEncoded = FileUtils.urlencode(ownAppId); this.listeners = listeners; + + checkDecsyncSubdirInfo(dir); } /** @@ -208,7 +216,7 @@ public class Decsync : GLib.Object { */ public void setEntries(Gee.Collection entriesWithPath) { - var multiMap = groupBy, Entry>( + var multiMap = groupByPath( entriesWithPath, entryWithPath => { return entryWithPath.path; }, entryWithPath => { return entryWithPath.entry; } @@ -294,7 +302,7 @@ public class Decsync : GLib.Object { } var path = new Gee.ArrayList(); path.add_all_iterator(pathEncoded.map(part => { return FileUtils.urldecode(part); })); - if (path.any_match(part => { return part == null; })) { + if (path.fold((part, seed) => { return part == null || seed; }, false)) { Log.w("Cannot decode path " + pathString); return; } @@ -441,7 +449,7 @@ public class Decsync : GLib.Object { if (entryLine == null) { return false; } - return !entries.any_match(entry => { return entry.key.equal(entryLine.key); }); + return entries.fold((entry, seed) => { return !entry.key.equal(entryLine.key) && seed; }, true); }); } @@ -451,6 +459,21 @@ public class Decsync : GLib.Object { return true; }); FileUtils.writeFile(entriesLocation.storedEntriesFile, builder.str, true); + + var maxDatetime = entries.fold((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) { @@ -497,36 +520,7 @@ public class Decsync : GLib.Object { 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) { - continue; - } - if (maxDatetime == null || entry.datetime > maxDatetime || - path.first() == ownAppId && entry.datetime == maxDatetime) { // Prefer own appId - maxDatetime = entry.datetime; - appId = path.first(); - } - } - } catch (GLib.Error e) { - Log.w(e.message); - } - return true; - }); - if (appId == null) { - Log.i("No appId found for initialization"); - return; - } + var appId = latestAppId(); // Copy the stored files and update the read bytes if (appId != ownAppId) { @@ -562,15 +556,66 @@ public class Decsync : GLib.Object { } } + /** + * 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() + { + 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; + } + + var appId = FileUtils.urldecode(info.get_name()); + var file = File.new_for_path(dir + "/info/" + info.get_name() + "/latest-stored-entry"); + + if (appId == null || + !file.query_exists() || + file.query_file_type(FileQueryInfoFlags.NONE) != FileType.REGULAR) + { + continue; + } + + 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) + { + latestDatetime = datetime; + latestAppId = appId; + } + } + } catch (GLib.Error e) { + Log.w(e.message); + } + + return latestAppId ?? 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) + public static Json.Node? getStoredStaticValue(string decsyncDir, string[] pathArray, Json.Node key) throws DecsyncError { 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; @@ -621,6 +666,59 @@ public class Decsync : GLib.Object { } } +private void checkDecsyncSubdirInfo(string decsyncSubdir) throws DecsyncError +{ + var syncTypes = new Gee.ArrayList.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()); + } +} + +/** + * 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 +{ + 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); + } + } +} + /** * Returns the path to the DecSync subdirectory in a [decsyncBaseDir] for a [syncType] and * optionally with a [collection]. @@ -657,9 +755,11 @@ public string getDefaultDecsyncBaseDir() * @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 listDecsyncCollections(string? decsyncBaseDir, string syncType, bool ignoreDeleted = true) throws GLib.Error { + checkDecsyncInfo(decsyncBaseDir ?? getDefaultDecsyncBaseDir()); var decsyncSubdir = File.new_for_path(getDecsyncSubdir(decsyncBaseDir, syncType)); var enumerator = decsyncSubdir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); FileInfo info; diff --git a/src/Utils.vala b/src/Utils.vala index c9c9a4ae..92d17042 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -54,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 groupBy(Gee.Collection inputs, Gee.MapFunc k, Gee.MapFunc? f = null) { var resultsMap = new Gee.HashMultiMap(); @@ -66,3 +67,29 @@ public Gee.MultiMap groupBy(Gee.Collection inputs, Gee.MapFunc return resultsMap; } + +public int pathCompare(Gee.List lhs, Gee.List 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, V> groupByPath( + Gee.Collection inputs, + Gee.MapFunc, T> toPath, + Gee.MapFunc? f = null) +{ + var resultsMap = new Gee.TreeMultiMap, V>(pathCompare); + foreach (var input in inputs) + { + var path = toPath(input); + var value = f == null ? input : f(input); + resultsMap.@set(path, value); + } + return resultsMap; +} -- cgit v1.2.3