Explorar el Código

Merge branch 'git'

Guillaume Ayoub hace 14 años
padre
commit
6eb9b21aac
Se han modificado 8 ficheros con 230 adiciones y 99 borrados
  1. 7 1
      config
  2. 3 2
      radicale/__init__.py
  3. 5 1
      radicale/config.py
  4. 62 92
      radicale/ical.py
  5. 34 0
      radicale/storage/__init__.py
  6. 116 0
      radicale/storage/filesystem.py
  7. 2 2
      radicale/xmlutils.py
  8. 1 1
      setup.py

+ 7 - 1
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]

+ 3 - 2
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
@@ -268,7 +269,7 @@ class Application(object):
         """Manage DELETE request."""
         calendar = calendars[0]
 
-        if calendar.local_path == environ["PATH_INFO"].strip("/"):
+        if calendar.path == environ["PATH_INFO"].strip("/"):
             # Path matching the calendar, the item to delete is the calendar
             item = calendar
         else:

+ 5 - 1
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",

+ 62 - 92
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=()):
@@ -161,7 +145,11 @@ class Timezone(Item):
 
 
 class Calendar(object):
-    """Internal calendar class."""
+    """Internal calendar class.
+
+    This class must be overridden and replaced by a storage backend.
+
+    """
     tag = "VCALENDAR"
 
     def __init__(self, path, principal=False):
@@ -173,9 +161,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 path != '.' else ''
+        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:
@@ -183,7 +170,6 @@ class Calendar(object):
             self.owner = split_path[0]
         else:
             self.owner = None
-        self.local_path = path if path != '.' else ''
         self.is_principal = principal
 
     @classmethod
@@ -195,7 +181,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/../
@@ -203,28 +189,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))
@@ -235,11 +214,52 @@ 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:
-            return 'BEGIN:VCALENDAR' == stream.read(15)
+    def save(self, text):
+        """Save the text into the calendar."""
+        raise NotImplemented
+
+    def delete(self):
+        """Delete the calendar."""
+        raise NotImplemented
+
+    @property
+    def text(self):
+        """Calendar as plain text."""
+        raise NotImplemented
+
+    @classmethod
+    def children(cls, path):
+        """Yield the children of the collection at local ``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
+
+    @property
+    def last_modified(self):
+        """Get the last time the calendar has been modified.
+
+        The date is formatted according to rfc1123-5.2.14.
+
+        """
+        raise NotImplemented
+
+    @property
+    @contextmanager
+    def props(self):
+        """Get the calendar properties."""
+        raise NotImplemented
+
+    def is_vcalendar(self, path):
+        """Return ``True`` if there is a VCALENDAR under relative ``path``."""
+        return self.text.startswith('BEGIN:VCALENDAR')
 
     @staticmethod
     def _parse(text, item_types, name=None):
@@ -303,11 +323,6 @@ class Calendar(object):
 
         self.write(items=items)
 
-    def delete(self):
-        """Delete the calendar."""
-        os.remove(self.path)
-        os.remove(self.props_path)
-
     def remove(self, name):
         """Remove object named ``name`` from calendar."""
         components = [
@@ -329,16 +344,8 @@ 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)
-
-    @staticmethod
-    def _create_dirs(path):
-        """Create folder if absent."""
-        if not os.path.exists(os.path.dirname(path)):
-            os.makedirs(os.path.dirname(path))
+        self.save(text)
 
     @property
     def etag(self):
@@ -352,14 +359,6 @@ class Calendar(object):
             return props.get('D:displayname',
                 self.path.split(os.path.sep)[-1])
 
-    @property
-    def text(self):
-        """Calendar as plain text."""
-        try:
-            return open(self.path).read()
-        except IOError:
-            return ""
-
     @property
     def headers(self):
         """Find headers items in calendar."""
@@ -405,35 +404,6 @@ class Calendar(object):
         """Get list of ``Timezome`` items in calendar."""
         return self._parse(self.text, (Timezone,))
 
-    @property
-    def last_modified(self):
-        """Get the last time the calendar has been modified.
-
-        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)
-
-    @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)
-
     @property
     def owner_url(self):
         """Get the calendar URL according to its owner."""
@@ -445,4 +415,4 @@ class Calendar(object):
     @property
     def url(self):
         """Get the standard calendar URL."""
-        return "/%s/" % self.local_path
+        return "/%s/" % self.path

+ 34 - 0
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 <http://www.gnu.org/licenses/>.
+
+"""
+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)

+ 116 - 0
radicale/storage/filesystem.py

@@ -0,0 +1,116 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+
+"""
+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"))
+
+
+# This function overrides the builtin ``open`` function for this module
+# pylint: disable=W0622
+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"))
+# pylint: enable=W0622
+
+
+class Calendar(ical.Calendar):
+    @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))
+
+    def save(self, text):
+        self._create_dirs()
+        open(self._path, "w").write(text)
+
+    def delete(self):
+        os.remove(self._path)
+        os.remove(self._props_path)
+
+    @property
+    def text(self):
+        try:
+            return open(self._path).read()
+        except IOError:
+            return ""
+
+    @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)
+
+    @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)
+
+    @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
+    @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)
+
+
+ical.Calendar = Calendar

+ 2 - 2
radicale/xmlutils.py

@@ -120,7 +120,7 @@ def _response(code):
 
 def name_from_path(path, calendar):
     """Return Radicale item name from ``path``."""
-    calendar_parts = calendar.local_path.strip("/").split("/")
+    calendar_parts = calendar.path.split("/")
     path_parts = path.strip("/").split("/")
     return path_parts[-1] if (len(path_parts) - len(calendar_parts)) else None
 
@@ -153,7 +153,7 @@ def delete(path, calendar):
 
     """
     # Reading request
-    if calendar.local_path == path.strip("/"):
+    if calendar.path == path.strip("/"):
         # Delete the whole calendar
         calendar.delete()
     else:

+ 1 - 1
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"],