Parcourir la source

Split the storage filesystem backend into another file

Guillaume Ayoub il y a 14 ans
Parent
commit
b1414c152d
7 fichiers modifiés avec 201 ajouts et 71 suppressions
  1. 7 1
      config
  2. 2 1
      radicale/__init__.py
  3. 5 1
      radicale/config.py
  4. 39 67
      radicale/ical.py
  5. 34 0
      radicale/storage/__init__.py
  6. 113 0
      radicale/storage/filesystem.py
  7. 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]

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

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

+ 39 - 67
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):

+ 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)

+ 113 - 0
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 <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"))
+
+
+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

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