diff --git a/radicale/__main__.py b/radicale/__main__.py
index 2f76f56a4..6ac272135 100644
--- a/radicale/__main__.py
+++ b/radicale/__main__.py
@@ -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
@@ -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")
diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py
index 2f87efa1e..adbbb8b02 100644
--- a/radicale/app/__init__.py
+++ b/radicale/app/__init__.py
@@ -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
@@ -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")
diff --git a/radicale/pathutils.py b/radicale/pathutils.py
index eee3e6d8f..de8e1069b 100644
--- a/radicale/pathutils.py
+++ b/radicale/pathutils.py
@@ -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
diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py
index 13c93b58f..427c88fe7 100644
--- a/radicale/storage/__init__.py
+++ b/radicale/storage/__init__.py
@@ -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:
@@ -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
@@ -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 ``/``.
@@ -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"):
diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py
index a7ab37826..fd7e030a9 100644
--- a/radicale/storage/multifilesystem/__init__.py
+++ b/radicale/storage/multifilesystem/__init__.py
@@ -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
@@ -42,8 +43,8 @@ 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
@@ -51,17 +52,27 @@ 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
@@ -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))
diff --git a/radicale/storage/multifilesystem/create_collection.py b/radicale/storage/multifilesystem/create_collection.py
index 9edbba426..6e2ec894a 100644
--- a/radicale/storage/multifilesystem/create_collection.py
+++ b/radicale/storage/multifilesystem/create_collection.py
@@ -33,7 +33,7 @@ def create_collection(cls, href, items=None, props=None):
if not props:
cls._makedirs_synced(filesystem_path)
- return cls(pathutils.unstrip_path(sane_path, True))
+ return cls(sane_path)
parent_dir = os.path.dirname(filesystem_path)
cls._makedirs_synced(parent_dir)
@@ -44,8 +44,7 @@ def create_collection(cls, href, items=None, props=None):
# The temporary directory itself can't be renamed
tmp_filesystem_path = os.path.join(tmp_dir, "collection")
os.makedirs(tmp_filesystem_path)
- self = cls(pathutils.unstrip_path(sane_path, True),
- filesystem_path=tmp_filesystem_path)
+ self = cls(sane_path, filesystem_path=tmp_filesystem_path)
self.set_meta(props)
if items is not None:
if props.get("tag") == "VCALENDAR":
@@ -61,4 +60,4 @@ def create_collection(cls, href, items=None, props=None):
os.rename(tmp_filesystem_path, filesystem_path)
cls._sync_directory(parent_dir)
- return cls(pathutils.unstrip_path(sane_path, True))
+ return cls(sane_path)
diff --git a/radicale/storage/multifilesystem/delete.py b/radicale/storage/multifilesystem/delete.py
index 08aa13f44..f680307f6 100644
--- a/radicale/storage/multifilesystem/delete.py
+++ b/radicale/storage/multifilesystem/delete.py
@@ -41,7 +41,13 @@ def delete(self, href=None):
# Delete an item
if not pathutils.is_safe_filesystem_path_component(href):
raise pathutils.UnsafePathError(href)
- path = pathutils.path_to_filesystem(self._filesystem_path, href)
+ if self._share:
+ assert self._share.item_writethrough
+ path = pathutils.path_to_filesystem(
+ self._base_collection._filesystem_path, href)
+ else:
+ path = pathutils.path_to_filesystem(
+ self._filesystem_path, href)
if not os.path.isfile(path):
raise storage.ComponentNotFoundError(href)
os.remove(path)
diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py
index f41cf3588..84a33660f 100644
--- a/radicale/storage/multifilesystem/discover.py
+++ b/radicale/storage/multifilesystem/discover.py
@@ -17,6 +17,7 @@
# along with Radicale. If not, see .
import contextlib
+import json
import os
from radicale import pathutils
@@ -36,6 +37,83 @@ def discover(cls, path, depth="0", child_context_manager=(
folder = cls._get_collection_root_folder()
# Create the root collection
cls._makedirs_synced(folder)
+
+ if (len(attributes) >= 1 and
+ attributes[-1].startswith(".share_") or
+ len(attributes) >= 2 and
+ attributes[-1].startswith(".share_")):
+ if attributes[-1].startswith(".share_"):
+ href = None
+ else:
+ href = attributes.pop()
+ parent_sane_path = "/".join(attributes[:-1])
+ share_path = pathutils.unescape_shared_path(
+ attributes[-1][len(".share"):])
+ base_path, *share_group = share_path.rsplit("//", 1)
+ if share_group:
+ base_path += "/"
+ share_group = share_group[0]
+ else:
+ share_group = ""
+ if (base_path != pathutils.sanitize_path(base_path) or
+ not base_path.endswith("/")):
+ return
+ base_sane_path = pathutils.strip_path(base_path)
+ for share in cls.shares:
+ if share.group == share_group:
+ break
+ else:
+ return
+ try:
+ base_filesystem_path = pathutils.path_to_filesystem(
+ folder, base_sane_path)
+ filesystem_path = os.path.join(pathutils.path_to_filesystem(
+ os.path.join(
+ pathutils.path_to_filesystem(folder, parent_sane_path),
+ ".Radicale.shares"),
+ base_sane_path), ".Radicale.share_group_%s" % share_group
+ if share_group else ".Radicale.share_group")
+ except ValueError as e:
+ # Path is unsafe
+ logger.debug("Unsafe path %r requested from storage: %s",
+ sane_path, e, exc_info=True)
+ return
+ share_uuid_path = os.path.join(filesystem_path, ".Radicale.share")
+ try:
+ with open(share_uuid_path, encoding=cls._encoding) as f:
+ share_uuid = json.load(f)
+ except FileNotFoundError:
+ return
+ except ValueError as e:
+ raise RuntimeError(
+ "Invalid share of collection %r to %r: %s" %
+ (base_sane_path, parent_sane_path, e)) from e
+ if not os.path.isdir(base_filesystem_path):
+ return
+ for share in cls.shares:
+ if share.uuid == share_uuid and share.group == share_group:
+ break
+ else:
+ return
+ base_collection = cls(base_sane_path)
+ if base_collection.get_meta("tag") not in share.tags:
+ return
+ collection = cls(
+ sane_path, filesystem_path=filesystem_path,
+ share=share, base_collection=base_collection)
+ if href:
+ yield collection._get(href)
+ return
+ yield collection
+ if depth == "0":
+ return
+ for href in collection._list():
+ with child_context_manager(sane_path, href):
+ child_item = collection._get(href)
+ if child_item:
+ yield child_item
+ return
+
try:
filesystem_path = pathutils.path_to_filesystem(folder, sane_path)
except ValueError as e:
@@ -54,7 +132,7 @@ def discover(cls, path, depth="0", child_context_manager=(
href = None
sane_path = "/".join(attributes)
- collection = cls(pathutils.unstrip_path(sane_path, True))
+ collection = cls(sane_path)
if href:
yield collection._get(href)
@@ -67,7 +145,9 @@ def discover(cls, path, depth="0", child_context_manager=(
for href in collection._list():
with child_context_manager(sane_path, href):
- yield collection._get(href)
+ child_item = collection._get(href)
+ if child_item:
+ yield child_item
for entry in os.scandir(filesystem_path):
if not entry.is_dir():
@@ -79,6 +159,63 @@ def discover(cls, path, depth="0", child_context_manager=(
href, sane_path)
continue
sane_child_path = posixpath.join(sane_path, href)
- child_path = pathutils.unstrip_path(sane_child_path, True)
with child_context_manager(sane_child_path):
- yield cls(child_path)
+ yield cls(sane_child_path)
+
+ def scan_shares(shares_folder, parent_sane_path=""):
+ for entry in os.scandir(
+ os.path.join(shares_folder, parent_sane_path)):
+ if not entry.is_dir():
+ continue
+ if pathutils.is_safe_filesystem_path_component(entry.name):
+ base_sane_path = os.path.join(parent_sane_path, entry.name)
+ yield from scan_shares(shares_folder, base_sane_path)
+ continue
+ if (entry.name != ".Radicale.share_group" and
+ not entry.name.startswith(".Radicale.share_group_") or
+ entry.name == ".Radicale.share_group_"):
+ continue
+ share_group = entry.name[len(".Radicale.share_group_"):]
+ base_sane_path = parent_sane_path
+ child_filesystem_path = os.path.join(
+ shares_folder, base_sane_path, entry.name)
+ share_uuid_path = os.path.join(
+ child_filesystem_path, ".Radicale.share")
+ try:
+ with open(share_uuid_path, encoding=cls._encoding) as f:
+ share_uuid = json.load(f)
+ except FileNotFoundError:
+ continue
+ except ValueError as e:
+ raise RuntimeError(
+ "Invalid share of collection %r to %r: %s" %
+ (base_sane_path, sane_path, e)) from e
+ for share in cls.shares:
+ if (share.uuid == share_uuid and
+ share.group == share_group):
+ break
+ else:
+ continue
+ try:
+ base_filesystem_path = pathutils.path_to_filesystem(
+ folder, base_sane_path)
+ except ValueError:
+ continue
+ if not os.path.isdir(base_filesystem_path):
+ continue
+ base_collection = cls(base_sane_path)
+ if base_collection.get_meta("tag") not in share.tags:
+ continue
+ child_sane_path = os.path.join(
+ sane_path,
+ ".share%s" % pathutils.escape_shared_path(
+ pathutils.unstrip_path(base_sane_path, True) +
+ ("/%s" % share.group if share.group else "")))
+ child_collection = cls(
+ child_sane_path, filesystem_path=child_filesystem_path,
+ share=share, base_collection=base_collection)
+ yield child_collection
+
+ shares_folder = os.path.join(filesystem_path, ".Radicale.shares")
+ if os.path.isdir(shares_folder):
+ yield from scan_shares(shares_folder)
diff --git a/radicale/storage/multifilesystem/get.py b/radicale/storage/multifilesystem/get.py
index 609fa8200..f1efe5565 100644
--- a/radicale/storage/multifilesystem/get.py
+++ b/radicale/storage/multifilesystem/get.py
@@ -32,6 +32,9 @@ def __init__(self):
self._item_cache_cleaned = False
def _list(self):
+ if self._share:
+ yield from self._base_collection._list()
+ return
for entry in os.scandir(self._filesystem_path):
if not entry.is_file():
continue
@@ -43,6 +46,8 @@ def _list(self):
yield href
def _get(self, href, verify_href=True):
+ if self._share:
+ return self._base_collection._get(href, verify_href)
if verify_href:
try:
if not pathutils.is_safe_filesystem_path_component(href):
@@ -132,4 +137,7 @@ def get_multi(self, hrefs):
def get_all(self):
# We don't need to check for collissions, because the the file names
# are from os.listdir.
- return (self._get(href, verify_href=False) for href in self._list())
+ for href in self._list():
+ item = self._get(href, verify_href=False)
+ if item:
+ yield item
diff --git a/radicale/storage/multifilesystem/meta.py b/radicale/storage/multifilesystem/meta.py
index e15229518..4ac1640af 100644
--- a/radicale/storage/multifilesystem/meta.py
+++ b/radicale/storage/multifilesystem/meta.py
@@ -38,6 +38,9 @@ def get_meta(self, key=None):
self._meta_cache = json.load(f)
except FileNotFoundError:
self._meta_cache = {}
+ if self._share:
+ self._meta_cache = self._share.get_meta(
+ self._meta_cache, self._base_collection.get_meta())
radicale_item.check_and_sanitize_props(self._meta_cache)
except ValueError as e:
raise RuntimeError("Failed to load properties of collection "
@@ -45,5 +48,14 @@ def get_meta(self, key=None):
return self._meta_cache.get(key) if key else self._meta_cache
def set_meta(self, props):
- with self._atomic_write(self._props_path, "w") as f:
- json.dump(props, f, sort_keys=True)
+ if self._share:
+ props, base_props = self._share.set_meta(
+ props, self.get_meta(), self._base_collection.get_meta())
+ with self._atomic_write(self._props_path, "w") as f1,\
+ self._atomic_write(
+ self._base_collection._props_path, "w") as f2:
+ json.dump(props, f1, sort_keys=True)
+ json.dump(base_props, f2, sort_keys=True)
+ else:
+ with self._atomic_write(self._props_path, "w") as f:
+ json.dump(props, f, sort_keys=True)
diff --git a/radicale/storage/multifilesystem/move.py b/radicale/storage/multifilesystem/move.py
index e2f587e11..e1a9077b2 100644
--- a/radicale/storage/multifilesystem/move.py
+++ b/radicale/storage/multifilesystem/move.py
@@ -26,18 +26,30 @@ class CollectionMoveMixin:
def move(cls, item, to_collection, to_href):
if not pathutils.is_safe_filesystem_path_component(to_href):
raise pathutils.UnsafePathError(to_href)
+ collection = item.collection
+ if collection._share:
+ assert collection._share.item_writethrough
+ base_collection = collection._base_collection
+ else:
+ base_collection = collection
+ if to_collection._share:
+ assert to_collection._share.item_writethrough
+ base_to_collection = to_collection._base_collection
+ else:
+ base_to_collection = to_collection
os.replace(
pathutils.path_to_filesystem(
- item.collection._filesystem_path, item.href),
+ base_collection._filesystem_path, item.href),
pathutils.path_to_filesystem(
- to_collection._filesystem_path, to_href))
- cls._sync_directory(to_collection._filesystem_path)
- if item.collection._filesystem_path != to_collection._filesystem_path:
- cls._sync_directory(item.collection._filesystem_path)
+ base_to_collection._filesystem_path, to_href))
+ cls._sync_directory(base_to_collection._filesystem_path)
+ if (base_collection._filesystem_path !=
+ base_to_collection._filesystem_path):
+ cls._sync_directory(base_collection._filesystem_path)
# Move the item cache entry
- cache_folder = os.path.join(item.collection._filesystem_path,
+ cache_folder = os.path.join(base_collection._filesystem_path,
".Radicale.cache", "item")
- to_cache_folder = os.path.join(to_collection._filesystem_path,
+ to_cache_folder = os.path.join(base_to_collection._filesystem_path,
".Radicale.cache", "item")
cls._makedirs_synced(to_cache_folder)
try:
@@ -51,7 +63,7 @@ def move(cls, item, to_collection, to_href):
cls._makedirs_synced(cache_folder)
# Track the change
to_collection._update_history_etag(to_href, item)
- item.collection._update_history_etag(item.href, None)
+ collection._update_history_etag(item.href, None)
to_collection._clean_history()
- if item.collection._filesystem_path != to_collection._filesystem_path:
- item.collection._clean_history()
+ if collection._filesystem_path != to_collection._filesystem_path:
+ collection._clean_history()
diff --git a/radicale/storage/multifilesystem/share.py b/radicale/storage/multifilesystem/share.py
new file mode 100644
index 000000000..5eee9383b
--- /dev/null
+++ b/radicale/storage/multifilesystem/share.py
@@ -0,0 +1,74 @@
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2018 Unrud
+#
+# 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 .
+
+import json
+import os
+
+from radicale import pathutils
+
+
+class CollectionShareMixin:
+
+ def list_shares(self):
+ if self._share:
+ yield from self._base_collection.list_shares()
+ return
+ folder = self._get_collection_root_folder()
+ for entry in os.scandir(folder):
+ if (not entry.is_dir() or
+ not pathutils.is_safe_filesystem_path_component(
+ entry.name)):
+ continue
+ user = entry.name
+ shares_path = os.path.join(folder, user, ".Radicale.shares")
+ try:
+ share_path = pathutils.path_to_filesystem(
+ shares_path, self.path)
+ except ValueError:
+ continue
+ try:
+ group_scanner = os.scandir(share_path)
+ except FileNotFoundError:
+ continue
+ for group_entry in group_scanner:
+ if (group_entry.name != ".Radicale.share_group" and
+ not group_entry.name.startswith(
+ ".Radicale.share_group_") or
+ entry.name == ".Radicale.share_group_"):
+ continue
+ share_group = group_entry.name[len(".Radicale.share_group_"):]
+ child_filesystem_path = os.path.join(
+ share_path, group_entry.name)
+ share_uuid_path = os.path.join(
+ child_filesystem_path, ".Radicale.share")
+ try:
+ with open(share_uuid_path, encoding=self._encoding) as f:
+ share_uuid = json.load(f)
+ except FileNotFoundError:
+ continue
+ except ValueError as e:
+ raise RuntimeError(
+ "Invalid share of collection %r to %r: %s" %
+ (self.path, user, e)) from e
+ for share in self.shares:
+ if (share.uuid == share_uuid and
+ share.group == share_group):
+ break
+ else:
+ continue
+ if self.get_meta("tag") not in share.tags:
+ continue
+ yield (user, share)
diff --git a/radicale/storage/multifilesystem/upload.py b/radicale/storage/multifilesystem/upload.py
index 837381d2a..257419e01 100644
--- a/radicale/storage/multifilesystem/upload.py
+++ b/radicale/storage/multifilesystem/upload.py
@@ -25,19 +25,26 @@
class CollectionUploadMixin:
def upload(self, href, item):
+ if self._share:
+ assert self._share.item_writethrough
+ return self._base_collection.upload(href, item)
if not pathutils.is_safe_filesystem_path_component(href):
raise pathutils.UnsafePathError(href)
+ if self._share:
+ collection = self._base_collection
+ else:
+ collection = self
try:
- self._store_item_cache(href, item)
+ collection._store_item_cache(href, item)
except Exception as e:
raise ValueError("Failed to store item %r in collection %r: %s" %
(href, self.path, e)) from e
- path = pathutils.path_to_filesystem(self._filesystem_path, href)
+ path = pathutils.path_to_filesystem(collection._filesystem_path, href)
with self._atomic_write(path, newline="") as fd:
fd.write(item.serialize())
# Clean the cache after the actual item is stored, or the cache entry
# will be removed again.
- self._clean_item_cache()
+ collection._clean_item_cache()
# Track the change
self._update_history_etag(href, item)
self._clean_history()
@@ -50,6 +57,7 @@ def _upload_all_nonatomic(self, items, suffix=""):
uploads them nonatomic and without existence checks.
"""
+ assert not self._share
cache_folder = os.path.join(self._filesystem_path,
".Radicale.cache", "item")
self._makedirs_synced(cache_folder)
diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py
index 087afafc4..b804b481e 100644
--- a/radicale/tests/test_base.py
+++ b/radicale/tests/test_base.py
@@ -28,7 +28,7 @@
import xml.etree.ElementTree as ET
from functools import partial
-from radicale import Application, config, storage
+from radicale import Application, config, share, storage
from . import BaseTest
from .helpers import get_file_content
@@ -258,7 +258,8 @@ def test_verify(self):
events = get_file_content("event_multiple.ics")
status, _, _ = self.request("PUT", "/calendar.ics/", events)
assert status == 201
- s = storage.load(self.configuration)
+ shares = share.load(self.configuration)
+ s = storage.load(self.configuration, shares)
assert s.verify()
def test_delete(self):