Merge branch 'git'

This commit is contained in:
Guillaume Ayoub 2012-01-23 15:50:17 +01:00
commit 6eb9b21aac
8 changed files with 230 additions and 99 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
@ -268,7 +269,7 @@ class Application(object):
"""Manage DELETE request.""" """Manage DELETE request."""
calendar = calendars[0] 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 # Path matching the calendar, the item to delete is the calendar
item = calendar item = calendar
else: else:

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

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

View File

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

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