Split the storage filesystem backend into another file

This commit is contained in:
Guillaume Ayoub 2012-01-12 02:18:06 +01:00
parent f2d491ea61
commit b1414c152d
7 changed files with 201 additions and 71 deletions

8
config
View File

@ -72,8 +72,14 @@ courier_socket =
[storage] [storage]
# Storage backend
type = filesystem
# Folder for storing local calendars, created if not present # 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] [logging]

View File

@ -46,7 +46,7 @@ except ImportError:
from urlparse import urlparse from urlparse import urlparse
# pylint: enable=F0401,E0611 # pylint: enable=F0401,E0611
from radicale import acl, config, ical, log, xmlutils from radicale import acl, config, ical, log, storage, xmlutils
VERSION = "git" VERSION = "git"
@ -112,6 +112,7 @@ class Application(object):
"""Initialize application.""" """Initialize application."""
super(Application, self).__init__() super(Application, self).__init__()
self.acl = acl.load() self.acl = acl.load()
storage.load()
self.encoding = config.get("encoding", "request") self.encoding = config.get("encoding", "request")
if config.getboolean('logging', 'full_environment'): if config.getboolean('logging', 'full_environment'):
self.headers_log = lambda environ: environ self.headers_log = lambda environ: environ

View File

@ -63,7 +63,11 @@ INITIAL_CONFIG = {
"pam_group_membership": "", "pam_group_membership": "",
"courier_socket": ""}, "courier_socket": ""},
"storage": { "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": { "logging": {
"config": "/etc/radicale/logging", "config": "/etc/radicale/logging",
"debug": "False", "debug": "False",

View File

@ -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 os
import posixpath import posixpath
import time
import uuid import uuid
from contextlib import contextmanager
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
def serialize(headers=(), items=()): def serialize(headers=(), items=()):
@ -171,9 +155,8 @@ class Calendar(object):
""" """
self.encoding = "utf-8" self.encoding = "utf-8"
split_path = path.split("/") split_path = path.split("/")
self.path = os.path.join(FOLDER, path.replace("/", os.sep)) self.path = path
self.props_path = self.path + '.props' if principal and split_path and self.is_collection(self.path):
if principal and split_path and os.path.isdir(self.path):
# Already existing principal calendar # Already existing principal calendar
self.owner = split_path[0] self.owner = split_path[0]
elif len(split_path) > 1: elif len(split_path) > 1:
@ -193,7 +176,7 @@ class Calendar(object):
``include_container`` is ``True`` (the default), the containing object ``include_container`` is ``True`` (the default), the containing object
is included in the result. 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/../ # First do normpath and then strip, to prevent access to FOLDER/../
@ -201,28 +184,21 @@ class Calendar(object):
attributes = sane_path.split("/") attributes = sane_path.split("/")
if not attributes: if not attributes:
return None return None
if not (os.path.isfile(os.path.join(FOLDER, *attributes)) or if not (cls.is_item("/".join(attributes)) or path.endswith("/")):
path.endswith("/")):
attributes.pop() attributes.pop()
result = [] result = []
path = "/".join(attributes) path = "/".join(attributes)
abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
principal = len(attributes) <= 1 principal = len(attributes) <= 1
if os.path.isdir(abs_path): if cls.is_collection(path):
if depth == "0": if depth == "0":
result.append(cls(path, principal)) result.append(cls(path, principal))
else: else:
if include_container: if include_container:
result.append(cls(path, principal)) result.append(cls(path, principal))
try: for child in cls.children(path):
for filename in next(os.walk(abs_path))[2]: result.append(child)
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
else: else:
if depth == "0": if depth == "0":
result.append(cls(path)) result.append(cls(path))
@ -233,12 +209,30 @@ class Calendar(object):
result.extend(calendar.components) result.extend(calendar.components)
return result return result
@staticmethod def open(self, path):
def is_vcalendar(path): """Return the content of the calendar under ``path``."""
"""Return ``True`` if there is a VCALENDAR file under ``path``.""" raise NotImplemented
with open(path) as stream:
@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) return 'BEGIN:VCALENDAR' == stream.read(15)
@classmethod
def children(cls, path):
"""Yield the children of the collection at local ``path``."""
raise NotImplemented
@staticmethod @staticmethod
def _parse(text, item_types, name=None): def _parse(text, item_types, name=None):
"""Find items with type in ``item_types`` in ``text``. """Find items with type in ``item_types`` in ``text``.
@ -303,8 +297,7 @@ class Calendar(object):
def delete(self): def delete(self):
"""Delete the calendar.""" """Delete the calendar."""
os.remove(self.path) raise NotImplemented
os.remove(self.props_path)
def remove(self, name): def remove(self, name):
"""Remove object named ``name`` from calendar.""" """Remove object named ``name`` from calendar."""
@ -327,16 +320,12 @@ class Calendar(object):
Header("VERSION:2.0")) Header("VERSION:2.0"))
items = items if items is not None else self.items items = items if items is not None else self.items
self._create_dirs(self.path)
text = serialize(headers, items) text = serialize(headers, items)
return open(self.path, "w").write(text) self.save(text)
@staticmethod def save(self, text):
def _create_dirs(path): """Save the text into the calendar."""
"""Create folder if absent.""" raise NotImplemented
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
@property @property
def etag(self): def etag(self):
@ -353,10 +342,7 @@ class Calendar(object):
@property @property
def text(self): def text(self):
"""Calendar as plain text.""" """Calendar as plain text."""
try: raise NotImplemented
return open(self.path).read()
except IOError:
return ""
@property @property
def headers(self): def headers(self):
@ -410,27 +396,13 @@ class Calendar(object):
The date is formatted according to rfc1123-5.2.14. The date is formatted according to rfc1123-5.2.14.
""" """
# Create calendar if needed raise NotImplemented
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 @property
@contextmanager @contextmanager
def props(self): def props(self):
"""Get the calendar properties.""" """Get the calendar properties."""
# On enter raise NotImplemented
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 @property
def owner_url(self): def owner_url(self):

View File

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

View File

@ -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

View File

@ -55,7 +55,7 @@ setup(
"Radicale-%s.tar.gz" % radicale.VERSION, "Radicale-%s.tar.gz" % radicale.VERSION,
license="GNU GPL v3", license="GNU GPL v3",
platforms="Any", platforms="Any",
packages=["radicale", "radicale.acl"], packages=["radicale", "radicale.acl", "radicale.storage"],
provides=["radicale"], provides=["radicale"],
scripts=["bin/radicale"], scripts=["bin/radicale"],
keywords=["calendar", "CalDAV"], keywords=["calendar", "CalDAV"],