def test_instance(self): decsync = Decsync(".tests/decsync", "sync-type", None, "app-id") extra = {} def listener(path, datetime, key, value, extra): extra[(tuple(path), key)] = value decsync.add_listener([], listener) decsync.init_done() decsync.set_entry(["foo1", "bar1"], "key1", "value1 ☺") decsync.set_entries( [Decsync.EntryWithPath(["foo2", "bar2"], "key2", "value2")]) decsync.set_entries_for_path(["foo3", "bar3"], [Decsync.Entry("key3", "value3")]) decsync.execute_all_new_entries(extra) decsync.execute_stored_entry(["foo1", "bar1"], "key1", extra) decsync.execute_stored_entries( [Decsync.StoredEntry(["foo2", "bar2"], "key2")], extra) decsync.execute_stored_entries_for_path_exact(["foo3", "bar3"], extra, keys=["key3"]) self.assertEqual(extra[(("foo1", "bar1"), "key1")], "value1 ☺") self.assertEqual(extra[(("foo2", "bar2"), "key2")], "value2") self.assertEqual(extra[(("foo3", "bar3"), "key3")], "value3") extra = {} decsync.execute_stored_entries_for_path_exact(["foo1", "bar1"], extra) decsync.execute_stored_entries_for_path_prefix(["foo2"], extra, keys=["key2"]) decsync.execute_stored_entries_for_path_prefix(["foo3"], extra) self.assertEqual(extra[(("foo1", "bar1"), "key1")], "value1 ☺") self.assertEqual(extra[(("foo2", "bar2"), "key2")], "value2") self.assertEqual(extra[(("foo3", "bar3"), "key3")], "value3") decsync.init_stored_entries() self.assertEqual(decsync.latest_app_id(), "app-id")
class Collection(storage.Collection, CollectionHrefMappingsMixin): def __init__(self, storage_, path, filesystem_path=None): super().__init__(storage_, path, filesystem_path=filesystem_path) attributes = _get_attributes_from_path(path) if len(attributes) == 2: decsync_dir = self._storage.decsync_dir sync_type = attributes[1].split("-")[0] if sync_type not in ["contacts", "calendars", "tasks", "memos"]: raise RuntimeError("Unknown sync type " + sync_type) collection = attributes[1][len(sync_type) + 1:] own_app_id = Decsync.get_app_id("Radicale") self.decsync = Decsync(decsync_dir, sync_type, collection, own_app_id) def info_listener(path, datetime, key, value, extra): if key == "name": extra._set_meta_key("D:displayname", value, update_decsync=False) elif key == "deleted": if value: extra.delete(update_decsync=False) elif key == "color": extra._set_meta_key("ICAL:calendar-color", value, update_decsync=False) else: raise ValueError("Unknown info key " + key) self.decsync.add_listener(["info"], info_listener) def resources_listener(path, datetime, key, value, extra): if len(path) != 1: raise ValueError("Invalid path " + str(path)) uid = path[0] href = extra.get_href(uid) if value is None: if extra._get(href) is not None: extra.delete(href, update_decsync=False) else: vobject_item = vobject.readOne(value) if sync_type == "contacts": tag = "VADDRESSBOOK" else: tag = "VCALENDAR" radicale_item.check_and_sanitize_items([vobject_item], tag=tag) item = radicale_item.Item(collection=extra, vobject_item=vobject_item, uid=uid) item.prepare() extra.upload(href, item, update_decsync=False) self.decsync.add_listener(["resources"], resources_listener) self.load_hrefs(sync_type) def upload(self, href, orig_item, update_decsync=True): item = super().upload(href, orig_item) if update_decsync: tag = self.get_meta("tag") if tag == "VCALENDAR": supported_components = ( self.get_meta("C:supported-calendar-component-set") or "VEVENT,VTODO,VJOURNAL").split(",") component_name = item.component_name if len(supported_components ) > 1 and component_name != "VEVENT": raise RuntimeError( "Component " + component_name + " is not supported by old DecSync collections. Create a new collection in Radicale for support." ) self.set_href(item.uid, href) self.decsync.set_entry(["resources", item.uid], None, item.serialize()) return item def delete(self, href=None, update_decsync=True): if update_decsync: if href is None: self.decsync.set_entry(["info"], "deleted", True) else: uid = self.get_uid(href) self.decsync.set_entry(["resources", uid], None, None) super().delete(href) def set_meta(self, props, update_decsync=True): for key, value in props.items(): old_value = self.get_meta(key) if old_value == value: continue if key == "D:displayname": if update_decsync: self.decsync.set_entry(["info"], "name", value) elif key == "ICAL:calendar-color": if update_decsync: self.decsync.set_entry(["info"], "color", value) elif key == "C:supported-calendar-component-set" and old_value != None: # Changing the supported components is not allowed props[key] = old_value super().set_meta(props) def _set_meta_key(self, key, value, update_decsync=True): props = self.get_meta() props[key] = value self.set_meta(props, update_decsync) @property def etag(self): self.decsync.execute_all_new_entries(self) return super().etag def sync(self, old_token=None): if hasattr(self, "decsync"): self.decsync.execute_all_new_entries(self) return super().sync(old_token)
class Collection(storage.Collection, CollectionHrefMappingsMixin): def __init__(self, path, filesystem_path=None): super().__init__(path, filesystem_path=filesystem_path) attributes = _get_attributes_from_path(path) if len(attributes) == 2: decsync_dir = self.__class__.decsync_dir sync_type = attributes[1].split("-")[0] collection = attributes[1][len(sync_type) + 1:] own_app_id = Decsync.get_app_id("Radicale") self.decsync = Decsync(decsync_dir, sync_type, collection, own_app_id) def info_listener(path, datetime, key, value, extra): if key == "name": extra._set_meta_key("D:displayname", value, update_decsync=False) elif key == "deleted": extra.delete(update_decsync=False) elif key == "color": extra._set_meta_key("ICAL:calendar-color", value, update_decsync=False) else: raise ValueError("Unknown info key " + key) self.decsync.add_listener(["info"], info_listener) def resources_listener(path, datetime, key, value, extra): if len(path) != 1: raise ValueError("Invalid path " + str(path)) uid = path[0] href = extra.get_href(uid) if value is None: if extra.get(href) is not None: extra.delete(href, update_decsync=False) else: item = vobject.readOne(value) if sync_type == "contacts": tag = "VADDRESSBOOK" elif sync_type == "calendars": tag = "VCALENDAR" else: raise RuntimeError("Unknown sync type " + sync_type) storage.check_and_sanitize_item(item, uid=uid, tag=tag) extra.upload(href, item, update_decsync=False) self.decsync.add_listener(["resources"], resources_listener) self.load_hrefs(sync_type) @classmethod def static_init(cls): cls.decsync_dir = os.path.expanduser( cls.configuration.get("storage", "decsync_dir", fallback="")) super().static_init() @classmethod def discover(cls, path, depth="0"): collections = list(super().discover(path, depth)) for collection in collections: yield collection if depth == "0": return attributes = _get_attributes_from_path(path) if len(attributes) == 0: return elif len(attributes) == 1: known_paths = [collection.path for collection in collections] for sync_type in ["contacts", "calendars"]: for collection in Decsync.list_collections( cls.decsync_dir, sync_type): child_path = storage.sanitize_path(path + "/" + sync_type + "-" + collection).strip("/") if child_path in known_paths: continue if Decsync.get_static_info(cls.decsync_dir, sync_type, collection, "deleted") == True: continue props = {} if sync_type == "contacts": props["tag"] = "VADDRESSBOOK" elif sync_type == "calendars": props["tag"] = "VCALENDAR" props["C:supported-calendar-component-set"] = "VEVENT" else: raise RuntimeError("Unknown sync type " + sync_type) child = super().create_collection(child_path, props=props) child.decsync.init_stored_entries() child.decsync.execute_stored_entries_for_path(["info"], child) child.decsync.execute_stored_entries_for_path( ["resources"], child) yield child elif len(attributes) == 2: return else: raise ValueError("Invalid number of attributes") @classmethod def create_collection(cls, href, items=None, props=None): if props is None: return super().create_collection(href, items, props) if props.get("tag") == "VADDRESSBOOK": sync_type = "contacts" elif props.get("tag") == "VCALENDAR": sync_type = "calendars" else: raise ValueError("Unknown tag " + props.get("tag")) attributes = _get_attributes_from_path(href) attributes[-1] = sync_type + "-" + attributes[-1] path = "/".join(attributes) return super().create_collection(path, items, props) def upload(self, href, vobject_item, update_decsync=True): item = super().upload(href, vobject_item) if update_decsync: self.set_href(item.uid, href) self.decsync.set_entry(["resources", item.uid], None, item.serialize()) return item def delete(self, href=None, update_decsync=True): if update_decsync: if href is None: self.decsync.set_entry(["info"], "deleted", True) else: uid = self.get_uid(href) self.decsync.set_entry(["resources", uid], None, None) super().delete(href) def set_meta_all(self, props, update_decsync=True): if update_decsync: for key, value in props.items(): if self.get_meta(key) == value: continue if key == "D:displayname": self.decsync.set_entry(["info"], "name", value) elif key == "ICAL:calendar-color": self.decsync.set_entry(["info"], "color", value) super().set_meta_all(props) def _set_meta_key(self, key, value, update_decsync=True): props = self.get_meta() props[key] = value self.set_meta_all(props, update_decsync) @property def etag(self): self.decsync.execute_all_new_entries(self) return super().etag def sync(self, old_token=None): self.decsync.execute_all_new_entries(self) return super().sync(old_token)