Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection sharing (WIP) #885

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@
#hook =


[share]

# Share plugins
#type = read, write, birthday


[web]

# Web interface backend
Expand Down
5 changes: 3 additions & 2 deletions radicale/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import signal
import socket

from radicale import VERSION, config, log, server, storage
from radicale import VERSION, config, log, server, share, storage
from radicale.log import logger


Expand Down Expand Up @@ -115,7 +115,8 @@ def run():
if args.verify_storage:
logger.info("Verifying storage")
try:
Collection = storage.load(configuration)
shares = share.load(configuration)
Collection = storage.load(configuration, shares)
with Collection.acquire_lock("r"):
if not Collection.verify():
logger.fatal("Storage verifcation failed")
Expand Down
5 changes: 3 additions & 2 deletions radicale/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from xml.etree import ElementTree as ET

from radicale import (
auth, httputils, log, pathutils, rights, storage, web, xmlutils)
auth, httputils, log, pathutils, rights, share, storage, web, xmlutils)
from radicale.app.delete import ApplicationDeleteMixin
from radicale.app.get import ApplicationGetMixin
from radicale.app.head import ApplicationHeadMixin
Expand Down Expand Up @@ -70,7 +70,8 @@ def __init__(self, configuration):
super().__init__()
self.configuration = configuration
self.Auth = auth.load(configuration)
self.Collection = storage.load(configuration)
self.shares = share.load(configuration)
self.Collection = storage.load(configuration, self.shares)
self.Rights = rights.load(configuration)
self.Web = web.load(configuration)
self.encoding = configuration.get("encoding", "request")
Expand Down
8 changes: 7 additions & 1 deletion radicale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from collections import OrderedDict
from configparser import RawConfigParser as ConfigParser

from radicale import auth, rights, storage, web
from radicale import auth, rights, share, storage, web


def positive_int(value):
Expand Down Expand Up @@ -169,6 +169,12 @@ def logging_level(value):
"value": "",
"help": "command that is run after changes to storage",
"type": str})])),
("share", OrderedDict([
("type", {
"value": "read, write, birthday",
"help": "set share plugins",
"type": str,
"internal": share.INTERNAL_TYPES})])),
("web", OrderedDict([
("type", {
"value": "internal",
Expand Down
25 changes: 25 additions & 0 deletions radicale/pathutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,28 @@ def name_from_path(path, collection):
raise ValueError("%r is not a component in collection %r" %
(name, collection.path))
return name


def escape_shared_path(path):
return path.replace(".", "..").replace("_", "._").replace("/", "_")


def unescape_shared_path(escaped_path):
path = ""
while escaped_path:
if escaped_path[0] == ".":
if len(escaped_path) <= 1:
raise ValueError("EOF")
if escaped_path[1] in (".", "_"):
path += escaped_path[1]
else:
raise ValueError("Illegal escape sequence: %r" %
escaped_path[0:2])
escaped_path = escaped_path[2:]
elif escaped_path[0] == "_":
path += "/"
escaped_path = escaped_path[1:]
else:
path += escaped_path[0]
escaped_path = escaped_path[1:]
return path
62 changes: 62 additions & 0 deletions radicale/share/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2018 Unrud<[email protected]>
#
# 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/>.

from importlib import import_module

from radicale.log import logger

INTERNAL_TYPES = ("read", "write", "birthday")


def load(configuration):
"""Load the share plugins chosen in configuration."""
share_types = tuple([
s.strip() for s in configuration.get("share", "type").split(",")])
classes = []
for share_type in share_types:
if share_type in INTERNAL_TYPES:
module = "radicale.share.%s" % share_type
else:
module = share_type
try:
classes.append(import_module(module).Share)
except Exception as e:
raise RuntimeError("Failed to load share module %r: %s" %
(module, e)) from e
logger.info("Share types are %r", share_types)
return tuple([class_(configuration) for class_ in classes])


class BaseShare:

name = ""
uuid = ""
group = ""

tags = ()
item_writethrough = False

def __init__(self, configuration):
self.configuration = configuration

def get(self, item):
raise NotImplementedError

def get_meta(self, props, base_props):
raise NotImplementedError

def set_meta(self, props, old_props, old_base_props):
raise NotImplementedError
37 changes: 37 additions & 0 deletions radicale/share/birthday.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2018 Unrud<[email protected]>
#
# 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/>.

from radicale.share import BaseShare


class Share(BaseShare):

name = "Birthday"
uuid = "a5ee648a-2240-4400-af49-a2f064ec5678"
group = "birthday"
tags = ("VCALENDAR",)
item_writethrough = False

def get(self, item):
return None

def get_meta(self, props, base_props):
return {
"tag": "VCALENDAR",
}

def set_meta(self, props, old_props, old_base_props):
return old_props, old_base_props
36 changes: 36 additions & 0 deletions radicale/share/read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2018 Unrud<[email protected]>
#
# 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/>.

from radicale.share import BaseShare


class Share(BaseShare):

name = "Read"
uuid = "0d41f7f1-4d93-41e7-98ce-0f2069d6773a"
group = ""

tags = ("VADDRESSBOOK", "VCALENDAR")
item_writethrough = False

def get(self, item):
return item

def get_meta(self, props, base_props):
return base_props

def set_meta(self, props, old_props, old_base_props):
return old_props, old_base_props
36 changes: 36 additions & 0 deletions radicale/share/write.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This file is part of Radicale Server - Calendar Server
# Copyright © 2018 Unrud<[email protected]>
#
# 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/>.

from radicale.share import BaseShare


class Share(BaseShare):

name = "Write"
uuid = "d3027b80-9624-4d88-9aa0-382e9bcd92ad"
group = ""

tags = ("VADDRESSBOOK", "VCALENDAR")
item_writethrough = True

def get(self, item):
return item

def get_meta(self, props, base_props):
return base_props

def set_meta(self, props, old_props, old_base_props):
return old_props, props
6 changes: 4 additions & 2 deletions radicale/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
for pkg in CACHE_DEPS) + ";").encode()


def load(configuration):
def load(configuration, shares):
"""Load the storage manager chosen in configuration."""
storage_type = configuration.get("storage", "type")
if storage_type in INTERNAL_TYPES:
Expand All @@ -61,6 +61,7 @@ def load(configuration):
class CollectionCopy(class_):
"""Collection copy, avoids overriding the original class attributes."""
CollectionCopy.configuration = configuration
CollectionCopy.shares = shares
CollectionCopy.static_init()
return CollectionCopy

Expand All @@ -81,6 +82,7 @@ class BaseCollection:

# Overriden on copy by the "load" function
configuration = None
share_types = None

# Properties of instance
"""The sanitized path of the collection without leading or trailing ``/``.
Expand All @@ -99,7 +101,7 @@ def owner(self):
@property
def is_principal(self):
"""Collection is a principal."""
return bool(self.path) and "/" not in self.path
return self.path and "/" not in self.path and not self.get_meta("tag")

@classmethod
def discover(cls, path, depth="0"):
Expand Down
40 changes: 30 additions & 10 deletions radicale/storage/multifilesystem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from radicale.storage.multifilesystem.lock import CollectionLockMixin
from radicale.storage.multifilesystem.meta import CollectionMetaMixin
from radicale.storage.multifilesystem.move import CollectionMoveMixin
from radicale.storage.multifilesystem.share import CollectionShareMixin
from radicale.storage.multifilesystem.sync import CollectionSyncMixin
from radicale.storage.multifilesystem.upload import CollectionUploadMixin
from radicale.storage.multifilesystem.verify import CollectionVerifyMixin
Expand All @@ -42,26 +43,36 @@ class Collection(
CollectionCacheMixin, CollectionCreateCollectionMixin,
CollectionDeleteMixin, CollectionDiscoverMixin, CollectionGetMixin,
CollectionHistoryMixin, CollectionLockMixin, CollectionMetaMixin,
CollectionMoveMixin, CollectionSyncMixin, CollectionUploadMixin,
CollectionVerifyMixin, storage.BaseCollection):
CollectionMoveMixin, CollectionShareMixin, CollectionSyncMixin,
CollectionUploadMixin, CollectionVerifyMixin, storage.BaseCollection):
"""Collection stored in several files per calendar."""

@classmethod
def static_init(cls):
folder = os.path.expanduser(cls.configuration.get(
"storage", "filesystem_folder"))
cls._makedirs_synced(folder)
cls._encoding = cls.configuration.get("encoding", "stock")
super().static_init()

def __init__(self, path, filesystem_path=None):
@property
def owner(self):
if self._share:
return self._base_collection.owner
return super().owner

def __init__(self, sane_path, filesystem_path=None,
share=None, base_collection=None):
assert not ((share is None) ^ (base_collection is None))
folder = self._get_collection_root_folder()
# Path should already be sanitized
self.path = pathutils.strip_path(path)
self._encoding = self.configuration.get("encoding", "stock")
assert sane_path == pathutils.sanitize_path(sane_path).strip("/")
self.path = sane_path
if filesystem_path is None:
filesystem_path = pathutils.path_to_filesystem(folder, self.path)
self._filesystem_path = filesystem_path
self._etag_cache = None
self._share = share
self._base_collection = base_collection
super().__init__()

@classmethod
Expand Down Expand Up @@ -137,12 +148,21 @@ def _makedirs_synced(cls, filesystem_path):
os.makedirs(filesystem_path, exist_ok=True)
cls._sync_directory(parent_filesystem_path)

def _last_modified_relevant_files(self):
yield self._filesystem_path
if os.path.exists(self._props_path):
yield self._props_path
if not self._share:
for href in self._list():
yield os.path.join(self._filesystem_path, href)

@property
def last_modified(self):
relevant_files = chain(
(self._filesystem_path,),
(self._props_path,) if os.path.exists(self._props_path) else (),
(os.path.join(self._filesystem_path, h) for h in self._list()))
relevant_files = self._last_modified_relevant_files()
if self._share:
relevant_files = chain(
relevant_files,
self._base_collection._last_modified_relevant_files())
last = max(map(os.path.getmtime, relevant_files))
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last))

Expand Down
Loading