diff --git a/config b/config index 453a45e1..75b12bd4 100644 --- a/config +++ b/config @@ -105,7 +105,7 @@ file = ~/.config/radicale/rights [storage] # Storage backend -# Value: filesystem | database +# Value: filesystem | multifilesystem | database type = filesystem # Folder for storing local collections, created if not present diff --git a/radicale/ical.py b/radicale/ical.py index 6da00a15..166bffa9 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -107,6 +107,12 @@ class Item(object): self.text = self.text.replace( "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name) + def __hash__(self): + return hash(self.text) + + def __eq__(self, item): + return isinstance(item, Item) and self.text == item.text + @property def etag(self): """Item etag. @@ -114,7 +120,7 @@ class Item(object): Etag is mainly used to know if an item has changed. """ - return '"%s"' % hash(self.text) + return '"%s"' % hash(self) @property def name(self): @@ -487,7 +493,7 @@ class Collection(object): @property def timezones(self): - """Get list of ``Timezome`` items in calendar.""" + """Get list of ``Timezone`` items in calendar.""" return self._parse(self.text, (Timezone,)) @property @@ -498,10 +504,7 @@ class Collection(object): @property def owner_url(self): """Get the collection URL according to its owner.""" - if self.owner: - return "/%s/" % self.owner - else: - return None + return "/%s/" % self.owner if self.owner else None @property def url(self): diff --git a/radicale/storage/multifilesystem.py b/radicale/storage/multifilesystem.py new file mode 100644 index 00000000..2a605afb --- /dev/null +++ b/radicale/storage/multifilesystem.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2013 Guillaume Ayoub +# Copyright © 2013 Jean-Marc Martins +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 General Public License +# along with Radicale. If not, see . + +""" +Multi files per calendar filesystem storage backend. + +""" + +import os +import shutil +import time + +from . import filesystem +from .. import ical + + +class Collection(filesystem.Collection): + """Collection stored in several files per calendar.""" + def _create_dirs(self): + if not os.path.exists(self._path): + os.makedirs(self._path) + + @property + def headers(self): + return ( + ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), + ical.Header("VERSION:%s" % self.version)) + + def write(self, headers=None, items=None): + self._create_dirs() + headers = headers or self.headers + items = items if items is not None else self.items + timezones = list(set(i for i in items if isinstance(i, ical.Timezone))) + components = [i for i in items if isinstance(i, ical.Component)] + for component in components: + text = ical.serialize(self.tag, headers, [component] + timezones) + path = os.path.join(self._path, component.name) + with filesystem.open(path, "w") as fd: + fd.write(text) + + def delete(self): + shutil.rmtree(self._path) + + def remove(self, name): + if os.path.exists(os.path.join(self._path, name)): + os.remove(os.path.join(self._path, name)) + + @property + def text(self): + components = ( + ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card) + items = set() + try: + for filename in os.listdir(self._path): + with filesystem.open(os.path.join(self._path, filename)) as fd: + items.update(self._parse(fd.read(), components)) + except IOError: + return "" + else: + return ical.serialize( + self.tag, self.headers, sorted(items, key=lambda x: x.name)) + + @classmethod + def is_node(cls, path): + path = os.path.join(filesystem.FOLDER, path.replace("/", os.sep)) + return os.path.isdir(path) and not os.path.exists(path + ".props") + + @classmethod + def is_leaf(cls, path): + path = os.path.join(filesystem.FOLDER, path.replace("/", os.sep)) + return os.path.isdir(path) and os.path.exists(path + ".props") + + @property + def last_modified(self): + last = max([ + os.path.getmtime(os.path.join(self._path, filename)) + for filename in os.listdir(self._path)] or [0]) + return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(last))