Remove extra auth, rights and storage modules

This commit is contained in:
Guillaume Ayoub
2016-04-07 19:02:52 +02:00
parent 1c4acc44a8
commit 1001bcb676
17 changed files with 134 additions and 1119 deletions

View File

@ -1,42 +0,0 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2012-2016 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.
"""
import sys
from .. import config, ical
def load():
"""Load list of available storage managers."""
storage_type = config.get("storage", "type")
if storage_type == "custom":
storage_module = config.get("storage", "custom_handler")
__import__(storage_module)
module = sys.modules[storage_module]
else:
root_module = __import__(
"storage.%s" % storage_type, globals=globals(), level=2)
module = getattr(root_module, storage_type)
ical.Collection = module.Collection
return module

View File

@ -1,282 +0,0 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2013 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/>.
"""
SQLAlchemy storage backend.
"""
import time
from datetime import datetime
from contextlib import contextmanager
from sqlalchemy import create_engine, Column, Unicode, Integer, ForeignKey
from sqlalchemy import func
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from .. import config, ical
# These are classes, not constants
# pylint: disable=C0103
Base = declarative_base()
Session = sessionmaker()
Session.configure(bind=create_engine(config.get("storage", "database_url")))
# pylint: enable=C0103
class DBCollection(Base):
"""Table of collections."""
__tablename__ = "collection"
path = Column(Unicode, primary_key=True)
parent_path = Column(Unicode, ForeignKey("collection.path"))
parent = relationship(
"DBCollection", backref="children", remote_side=[path])
class DBItem(Base):
"""Table of collection's items."""
__tablename__ = "item"
name = Column(Unicode, primary_key=True)
tag = Column(Unicode)
collection_path = Column(Unicode, ForeignKey("collection.path"))
collection = relationship("DBCollection", backref="items")
class DBHeader(Base):
"""Table of item's headers."""
__tablename__ = "header"
name = Column(Unicode, primary_key=True)
value = Column(Unicode)
collection_path = Column(
Unicode, ForeignKey("collection.path"), primary_key=True)
collection = relationship("DBCollection", backref="headers")
class DBLine(Base):
"""Table of item's lines."""
__tablename__ = "line"
name = Column(Unicode)
value = Column(Unicode)
item_name = Column(Unicode, ForeignKey("item.name"))
timestamp = Column(
Integer, default=lambda: time.time() * 10 ** 6, primary_key=True)
item = relationship("DBItem", backref="lines", order_by=timestamp)
class DBProperty(Base):
"""Table of collection's properties."""
__tablename__ = "property"
name = Column(Unicode, primary_key=True)
value = Column(Unicode)
collection_path = Column(
Unicode, ForeignKey("collection.path"), primary_key=True)
collection = relationship(
"DBCollection", backref="properties", cascade="delete")
class Collection(ical.Collection):
"""Collection stored in a database."""
def __init__(self, path, principal=False):
self.session = Session()
super().__init__(path, principal)
def __del__(self):
self.session.commit()
def _query(self, item_types):
"""Get collection's items matching ``item_types``."""
item_objects = []
for item_type in item_types:
items = (
self.session.query(DBItem)
.filter_by(collection_path=self.path, tag=item_type.tag)
.order_by(DBItem.name).all())
for item in items:
text = "\n".join(
"%s:%s" % (line.name, line.value) for line in item.lines)
item_objects.append(item_type(text, item.name))
return item_objects
@property
def _modification_time(self):
"""Collection's last modification time."""
timestamp = (
self.session.query(func.max(DBLine.timestamp))
.join(DBItem).filter_by(collection_path=self.path).first()[0])
if timestamp:
return datetime.fromtimestamp(float(timestamp) / 10 ** 6)
else:
return datetime.now()
@property
def _db_collection(self):
"""Collection's object mapped to the table line."""
return self.session.query(DBCollection).get(self.path)
def write(self):
if self._db_collection:
for item in self._db_collection.items:
for line in item.lines:
self.session.delete(line)
self.session.delete(item)
for header in self._db_collection.headers:
self.session.delete(header)
else:
db_collection = DBCollection()
db_collection.path = self.path
db_collection.parent_path = "/".join(self.path.split("/")[:-1])
self.session.add(db_collection)
for header in self.headers:
db_header = DBHeader()
db_header.name, db_header.value = header.text.split(":", 1)
db_header.collection_path = self.path
self.session.add(db_header)
for item in self.items.values():
db_item = DBItem()
db_item.name = item.name
db_item.tag = item.tag
db_item.collection_path = self.path
self.session.add(db_item)
for line in ical.unfold(item.text):
db_line = DBLine()
db_line.name, db_line.value = line.split(":", 1)
db_line.item_name = item.name
self.session.add(db_line)
def delete(self):
self.session.delete(self._db_collection)
@property
def text(self):
return ical.serialize(self.tag, self.headers, self.components)
@property
def etag(self):
return '"%s"' % hash(self._modification_time)
@property
def headers(self):
headers = (
self.session.query(DBHeader)
.filter_by(collection_path=self.path)
.order_by(DBHeader.name).all())
return [
ical.Header("%s:%s" % (header.name, header.value))
for header in headers]
@classmethod
def children(cls, path):
session = Session()
children = (
session.query(DBCollection)
.filter_by(parent_path=path or "").all())
collections = [cls(child.path) for child in children]
session.close()
return collections
@classmethod
def is_node(cls, path):
if not path:
return True
session = Session()
result = (
session.query(DBCollection)
.filter_by(parent_path=path or "").count() > 0)
session.close()
return result
@classmethod
def is_leaf(cls, path):
if not path:
return False
session = Session()
result = (
session.query(DBItem)
.filter_by(collection_path=path or "").count() > 0)
session.close()
return result
@property
def last_modified(self):
return time.strftime(
"%a, %d %b %Y %H:%M:%S +0000", self._modification_time.timetuple())
@property
@contextmanager
def props(self):
# On enter
properties = {}
db_properties = (
self.session.query(DBProperty)
.filter_by(collection_path=self.path).all())
for prop in db_properties:
properties[prop.name] = prop.value
old_properties = properties.copy()
yield properties
# On exit
if old_properties != properties:
for prop in db_properties:
self.session.delete(prop)
for name, value in properties.items():
prop = DBProperty(name=name, value=value or '',
collection_path=self.path)
self.session.add(prop)
@property
def components(self):
return self._query((ical.Event, ical.Todo, ical.Journal, ical.Card))
@property
def events(self):
return self._query((ical.Event,))
@property
def todos(self):
return self._query((ical.Todo,))
@property
def journals(self):
return self._query((ical.Journal,))
@property
def timezones(self):
return self._query((ical.Timezone,))
@property
def cards(self):
return self._query((ical.Card,))
def save(self):
"""Save the text into the collection.
This method is not used for databases.
"""

View File

@ -1,142 +0,0 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2012-2016 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 posixpath
import json
import time
import sys
from contextlib import contextmanager
from .. import config, ical, log, pathutils
FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
FILESYSTEM_ENCODING = sys.getfilesystemencoding()
try:
from dulwich.repo import Repo
GIT_REPOSITORY = Repo(FOLDER)
except:
GIT_REPOSITORY = None
# This function overrides the builtin ``open`` function for this module
# pylint: disable=W0622
@contextmanager
def open(path, mode="r"):
"""Open a file at ``path`` with encoding set in the configuration."""
# On enter
abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd:
yield fd
# On exit
if GIT_REPOSITORY and mode == "w":
path = os.path.relpath(abs_path, FOLDER)
GIT_REPOSITORY.stage([path])
committer = config.get("git", "committer")
GIT_REPOSITORY.do_commit(
path.encode("utf-8"), committer=committer.encode("utf-8"))
# pylint: enable=W0622
class Collection(ical.Collection):
"""Collection stored in a flat ical file."""
@property
def _filesystem_path(self):
"""Absolute path of the file at local ``path``."""
return pathutils.path_to_filesystem(self.path, FOLDER)
@property
def _props_path(self):
"""Absolute path of the file storing the collection properties."""
return self._filesystem_path + ".props"
def _create_dirs(self):
"""Create folder storing the collection if absent."""
if not os.path.exists(os.path.dirname(self._filesystem_path)):
os.makedirs(os.path.dirname(self._filesystem_path))
def save(self, text):
self._create_dirs()
with open(self._filesystem_path, "w") as fd:
fd.write(text)
def delete(self):
os.remove(self._filesystem_path)
os.remove(self._props_path)
@property
def text(self):
try:
with open(self._filesystem_path) as fd:
return fd.read()
except IOError:
return ""
@classmethod
def children(cls, path):
filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
_, directories, files = next(os.walk(filesystem_path))
for filename in directories + files:
# make sure that the local filename can be translated
# into an internal path
if not pathutils.is_safe_path_component(filename):
log.LOGGER.debug("Skipping unsupported filename: %s", filename)
continue
rel_filename = posixpath.join(path, filename)
if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
yield cls(rel_filename)
@classmethod
def is_node(cls, path):
filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
return os.path.isdir(filesystem_path)
@classmethod
def is_leaf(cls, path):
filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
return (
os.path.isfile(filesystem_path) and not
filesystem_path.endswith(".props"))
@property
def last_modified(self):
modification_time = time.gmtime(
os.path.getmtime(self._filesystem_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))
old_properties = properties.copy()
yield properties
# On exit
self._create_dirs()
if old_properties != properties:
with open(self._props_path, "w") as prop_file:
json.dump(properties, prop_file)

View File

@ -1,139 +0,0 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2014-2016 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/>.
"""
Multi files per calendar filesystem storage backend.
"""
import os
import json
import shutil
import time
import sys
from contextlib import contextmanager
from . import filesystem
from .. import ical
from .. import log
from .. import pathutils
class Collection(filesystem.Collection):
"""Collection stored in several files per calendar."""
def _create_dirs(self):
if not os.path.exists(self._filesystem_path):
os.makedirs(self._filesystem_path)
@property
def headers(self):
return (
ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
ical.Header("VERSION:%s" % self.version))
def write(self):
self._create_dirs()
for component in self.components:
text = ical.serialize(
self.tag, self.headers, [component] + self.timezones)
name = component.name
if not pathutils.is_safe_filesystem_path_component(name):
log.LOGGER.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", name)
continue
filesystem_path = os.path.join(self._filesystem_path, name)
with filesystem.open(filesystem_path, "w") as fd:
fd.write(text)
def delete(self):
shutil.rmtree(self._filesystem_path)
os.remove(self._props_path)
def remove(self, name):
if not pathutils.is_safe_filesystem_path_component(name):
log.LOGGER.debug(
"Can't tranlate name safely to filesystem, "
"skipping component: %s", name)
return
if name in self.items:
del self.items[name]
filesystem_path = os.path.join(self._filesystem_path, name)
if os.path.exists(filesystem_path):
os.remove(filesystem_path)
@property
def text(self):
components = (
ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card)
items = {}
try:
filenames = os.listdir(self._filesystem_path)
except (OSError, IOError) as e:
log.LOGGER.info(
'Error while reading collection %r: %r' % (
self._filesystem_path, e))
return ""
for filename in filenames:
path = os.path.join(self._filesystem_path, filename)
try:
with filesystem.open(path) as fd:
items.update(self._parse(fd.read(), components))
except (OSError, IOError) as e:
log.LOGGER.warning(
'Error while reading item %r: %r' % (path, e))
return ical.serialize(
self.tag, self.headers, sorted(items.values(), key=lambda x: x.name))
@classmethod
def is_node(cls, path):
filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
return (
os.path.isdir(filesystem_path) and
not os.path.exists(filesystem_path + ".props"))
@classmethod
def is_leaf(cls, path):
filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
return (
os.path.isdir(filesystem_path) and os.path.exists(path + ".props"))
@property
def last_modified(self):
last = max([
os.path.getmtime(os.path.join(self._filesystem_path, filename))
for filename in os.listdir(self._filesystem_path)] or [0])
return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(last))
@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))
old_properties = properties.copy()
yield properties
# On exit
if os.path.exists(self._props_path):
self._create_dirs()
if old_properties != properties:
with open(self._props_path, "w") as prop_file:
json.dump(properties, prop_file)