diff --git a/config b/config index 9ce2755..c6445ea 100644 --- a/config +++ b/config @@ -72,8 +72,14 @@ courier_socket = [storage] +# Storage backend +type = filesystem + # Folder for storing local calendars, created if not present -folder = ~/.config/radicale/calendars +filesystem_folder = ~/.config/radicale/calendars + +# Git repository for storing local calendars, created if not present +filesystem_folder = ~/.config/radicale/calendars [logging] diff --git a/radicale/__init__.py b/radicale/__init__.py index 4267d61..6b86ff7 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -46,7 +46,7 @@ except ImportError: from urlparse import urlparse # pylint: enable=F0401,E0611 -from radicale import acl, config, ical, log, xmlutils +from radicale import acl, config, ical, log, storage, xmlutils VERSION = "git" @@ -112,6 +112,7 @@ class Application(object): """Initialize application.""" super(Application, self).__init__() self.acl = acl.load() + storage.load() self.encoding = config.get("encoding", "request") if config.getboolean('logging', 'full_environment'): self.headers_log = lambda environ: environ diff --git a/radicale/config.py b/radicale/config.py index 51d939c..dbaa186 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -63,7 +63,11 @@ INITIAL_CONFIG = { "pam_group_membership": "", "courier_socket": ""}, "storage": { - "folder": os.path.expanduser("~/.config/radicale/calendars")}, + "type": "filesystem", + "filesystem_folder": + os.path.expanduser("~/.config/radicale/calendars"), + "git_folder": + os.path.expanduser("~/.config/radicale/calendars")}, "logging": { "config": "/etc/radicale/logging", "debug": "False", diff --git a/radicale/ical.py b/radicale/ical.py index 3c07d3c..81ac38f 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -25,26 +25,10 @@ Define the main classes of a calendar as seen from the server. """ -import codecs -from contextlib import contextmanager -import json import os import posixpath -import time import uuid - -from radicale import config - - -FOLDER = os.path.expanduser(config.get("storage", "folder")) - - -# This function overrides the builtin ``open`` function for this module -# pylint: disable=W0622 -def open(path, mode="r"): - """Open file at ``path`` with ``mode``, automagically managing encoding.""" - return codecs.open(path, mode, config.get("encoding", "stock")) -# pylint: enable=W0622 +from contextlib import contextmanager def serialize(headers=(), items=()): @@ -171,9 +155,8 @@ class Calendar(object): """ self.encoding = "utf-8" split_path = path.split("/") - self.path = os.path.join(FOLDER, path.replace("/", os.sep)) - self.props_path = self.path + '.props' - if principal and split_path and os.path.isdir(self.path): + self.path = path + if principal and split_path and self.is_collection(self.path): # Already existing principal calendar self.owner = split_path[0] elif len(split_path) > 1: @@ -193,7 +176,7 @@ class Calendar(object): ``include_container`` is ``True`` (the default), the containing object is included in the result. - The ``path`` is relative to the storage folder. + The ``path`` is relative. """ # First do normpath and then strip, to prevent access to FOLDER/../ @@ -201,28 +184,21 @@ class Calendar(object): attributes = sane_path.split("/") if not attributes: return None - if not (os.path.isfile(os.path.join(FOLDER, *attributes)) or - path.endswith("/")): + if not (cls.is_item("/".join(attributes)) or path.endswith("/")): attributes.pop() result = [] - path = "/".join(attributes) - abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) + principal = len(attributes) <= 1 - if os.path.isdir(abs_path): + if cls.is_collection(path): if depth == "0": result.append(cls(path, principal)) else: if include_container: result.append(cls(path, principal)) - try: - for filename in next(os.walk(abs_path))[2]: - if cls.is_vcalendar(os.path.join(abs_path, filename)): - result.append(cls(os.path.join(path, filename))) - except StopIteration: - # Directory does not exist yet - pass + for child in cls.children(path): + result.append(child) else: if depth == "0": result.append(cls(path)) @@ -233,12 +209,30 @@ class Calendar(object): result.extend(calendar.components) return result - @staticmethod - def is_vcalendar(path): - """Return ``True`` if there is a VCALENDAR file under ``path``.""" - with open(path) as stream: + def open(self, path): + """Return the content of the calendar under ``path``.""" + raise NotImplemented + + @classmethod + def is_collection(cls, path): + """Return ``True`` if relative ``path`` is a collection.""" + raise NotImplemented + + @classmethod + def is_item(cls, path): + """Return ``True`` if relative ``path`` is a collection item.""" + raise NotImplemented + + def is_vcalendar(self, path): + """Return ``True`` if there is a VCALENDAR under relative ``path``.""" + with self.open(path) as stream: return 'BEGIN:VCALENDAR' == stream.read(15) + @classmethod + def children(cls, path): + """Yield the children of the collection at local ``path``.""" + raise NotImplemented + @staticmethod def _parse(text, item_types, name=None): """Find items with type in ``item_types`` in ``text``. @@ -303,8 +297,7 @@ class Calendar(object): def delete(self): """Delete the calendar.""" - os.remove(self.path) - os.remove(self.props_path) + raise NotImplemented def remove(self, name): """Remove object named ``name`` from calendar.""" @@ -327,16 +320,12 @@ class Calendar(object): Header("VERSION:2.0")) items = items if items is not None else self.items - self._create_dirs(self.path) - text = serialize(headers, items) - return open(self.path, "w").write(text) + self.save(text) - @staticmethod - def _create_dirs(path): - """Create folder if absent.""" - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + def save(self, text): + """Save the text into the calendar.""" + raise NotImplemented @property def etag(self): @@ -353,10 +342,7 @@ class Calendar(object): @property def text(self): """Calendar as plain text.""" - try: - return open(self.path).read() - except IOError: - return "" + raise NotImplemented @property def headers(self): @@ -410,27 +396,13 @@ class Calendar(object): The date is formatted according to rfc1123-5.2.14. """ - # Create calendar if needed - if not os.path.exists(self.path): - self.write() - - modification_time = time.gmtime(os.path.getmtime(self.path)) - return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time) + raise NotImplemented @property @contextmanager def props(self): """Get the calendar properties.""" - # On enter - properties = {} - if os.path.exists(self.props_path): - with open(self.props_path) as prop_file: - properties.update(json.load(prop_file)) - yield properties - # On exit - self._create_dirs(self.props_path) - with open(self.props_path, 'w') as prop_file: - json.dump(properties, prop_file) + raise NotImplemented @property def owner_url(self): diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py new file mode 100644 index 0000000..02c0bab --- /dev/null +++ b/radicale/storage/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2012 Guillaume Ayoub +# +# 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 . + +""" +Storage backends. + +This module loads the storage backend, according to the storage +configuration. + +""" + +from radicale import config + + +def load(): + """Load list of available storage managers.""" + storage_type = config.get("storage", "type") + module = __import__("radicale.storage", fromlist=[storage_type]) + return getattr(module, storage_type) diff --git a/radicale/storage/filesystem.py b/radicale/storage/filesystem.py new file mode 100644 index 0000000..8c1db43 --- /dev/null +++ b/radicale/storage/filesystem.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2012 Guillaume Ayoub +# +# 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 . + +""" +Filesystem storage backend. + +""" + +import codecs +import os +import json +import time +from contextlib import contextmanager + +from radicale import config, ical + + +FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder")) + + +class Calendar(ical.Calendar): + @staticmethod + def open(path, mode="r"): + abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) + return codecs.open(abs_path, mode, config.get("encoding", "stock")) + + @classmethod + def is_collection(cls, path): + abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) + return os.path.isdir(abs_path) + + @classmethod + def is_item(cls, path): + abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) + return os.path.isfile(abs_path) + + @classmethod + def children(cls, path): + abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) + for filename in next(os.walk(abs_path))[2]: + if cls.is_collection(path): + yield cls(path) + + def delete(self): + os.remove(self._path) + os.remove(self._props_path) + + @property + def _path(self): + """Absolute path of the file at local ``path``.""" + return os.path.join(FOLDER, self.path.replace("/", os.sep)) + + @property + def _props_path(self): + """Absolute path of the file storing the calendar properties.""" + return self._path + ".props" + + def _create_dirs(self): + """Create folder storing the calendar if absent.""" + if not os.path.exists(os.path.dirname(self._path)): + os.makedirs(os.path.dirname(self._path)) + + @property + @contextmanager + def props(self): + # On enter + properties = {} + if os.path.exists(self._props_path): + with open(self._props_path) as prop_file: + properties.update(json.load(prop_file)) + yield properties + # On exit + self._create_dirs() + with open(self._props_path, 'w') as prop_file: + json.dump(properties, prop_file) + + @property + def last_modified(self): + # Create calendar if needed + if not os.path.exists(self._path): + self.write() + + modification_time = time.gmtime(os.path.getmtime(self._path)) + return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time) + + @property + def text(self): + try: + return open(self._path).read() + except IOError: + return "" + + def save(self, text): + self._create_dirs() + self.open(self._path, "w").write(text) + + +ical.Calendar = Calendar diff --git a/setup.py b/setup.py index 6733deb..6d863da 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ setup( "Radicale-%s.tar.gz" % radicale.VERSION, license="GNU GPL v3", platforms="Any", - packages=["radicale", "radicale.acl"], + packages=["radicale", "radicale.acl", "radicale.storage"], provides=["radicale"], scripts=["bin/radicale"], keywords=["calendar", "CalDAV"],