mirror of
https://github.com/Kozea/Radicale.git
synced 2025-09-12 20:30:57 +00:00
- Capture previous version of event pre-overwrite for use in notification hooks
- Use previous version of event in email hooks to determine added/deleted/updated email type
This commit is contained in:
parent
e4b337d3ff
commit
80dc4995cf
10 changed files with 274 additions and 110 deletions
|
@ -28,7 +28,7 @@ import json
|
|||
import xml.etree.ElementTree as ET
|
||||
from hashlib import sha256
|
||||
from typing import (Callable, ContextManager, Iterable, Iterator, Mapping,
|
||||
Optional, Sequence, Set, Tuple, Union, overload)
|
||||
Optional, Sequence, Set, Tuple, Union, overload, Dict, List)
|
||||
|
||||
import vobject
|
||||
|
||||
|
@ -175,8 +175,11 @@ class BaseCollection:
|
|||
return False
|
||||
|
||||
def upload(self, href: str, item: "radicale_item.Item") -> (
|
||||
"radicale_item.Item"):
|
||||
"""Upload a new or replace an existing item."""
|
||||
"radicale_item.Item", Optional["radicale_item.Item"]):
|
||||
"""Upload a new or replace an existing item.
|
||||
|
||||
Return the uploaded item and the old item if it was replaced.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, href: Optional[str] = None) -> None:
|
||||
|
@ -328,7 +331,8 @@ class BaseStorage:
|
|||
def create_collection(
|
||||
self, href: str,
|
||||
items: Optional[Iterable["radicale_item.Item"]] = None,
|
||||
props: Optional[Mapping[str, str]] = None) -> BaseCollection:
|
||||
props: Optional[Mapping[str, str]] = None) -> (
|
||||
Tuple)[BaseCollection, Dict[str, "radicale_item.Item"], List[str]]:
|
||||
"""Create a collection.
|
||||
|
||||
``href`` is the sanitized path.
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import os
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Iterable, Optional, cast
|
||||
from typing import Iterable, Optional, cast, List, Tuple, Dict
|
||||
|
||||
import radicale.item as radicale_item
|
||||
from radicale import pathutils
|
||||
|
@ -30,9 +30,37 @@ from radicale.storage.multifilesystem.base import StorageBase
|
|||
|
||||
class StoragePartCreateCollection(StorageBase):
|
||||
|
||||
def _discover_existing_items_pre_overwrite(self,
|
||||
tmp_collection: "multifilesystem.Collection",
|
||||
dst_path: str) -> Tuple[Dict[str, radicale_item.Item], List[str]]:
|
||||
"""Discover existing items in the collection before overwriting them."""
|
||||
existing_items = {}
|
||||
new_item_hrefs = []
|
||||
|
||||
existing_collection = self._collection_class(
|
||||
cast(multifilesystem.Storage, self),
|
||||
pathutils.unstrip_path(dst_path, True))
|
||||
existing_item_hrefs = set(existing_collection._list())
|
||||
tmp_collection_hrefs = set(tmp_collection._list())
|
||||
for item_href in tmp_collection_hrefs:
|
||||
if item_href not in existing_item_hrefs:
|
||||
# Item in temporary collection does not exist in the existing collection (is new)
|
||||
new_item_hrefs.append(item_href)
|
||||
continue
|
||||
# Item exists in both collections, grab the existing item for reference
|
||||
try:
|
||||
item = existing_collection._get(item_href, verify_href=False)
|
||||
if item is not None:
|
||||
existing_items[item_href] = item
|
||||
except Exception:
|
||||
# TODO: Log exception?
|
||||
continue
|
||||
|
||||
return existing_items, new_item_hrefs
|
||||
|
||||
def create_collection(self, href: str,
|
||||
items: Optional[Iterable[radicale_item.Item]] = None,
|
||||
props=None) -> "multifilesystem.Collection":
|
||||
props=None) -> Tuple["multifilesystem.Collection", Dict[str, radicale_item.Item], List[str]]:
|
||||
folder = self._get_collection_root_folder()
|
||||
|
||||
# Path should already be sanitized
|
||||
|
@ -44,11 +72,14 @@ class StoragePartCreateCollection(StorageBase):
|
|||
self._makedirs_synced(filesystem_path)
|
||||
return self._collection_class(
|
||||
cast(multifilesystem.Storage, self),
|
||||
pathutils.unstrip_path(sane_path, True))
|
||||
pathutils.unstrip_path(sane_path, True)), {}, []
|
||||
|
||||
parent_dir = os.path.dirname(filesystem_path)
|
||||
self._makedirs_synced(parent_dir)
|
||||
|
||||
replaced_items: Dict[str, radicale_item.Item] = {}
|
||||
new_item_hrefs: List[str] = []
|
||||
|
||||
# Create a temporary directory with an unsafe name
|
||||
try:
|
||||
with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir
|
||||
|
@ -68,14 +99,20 @@ class StoragePartCreateCollection(StorageBase):
|
|||
col._upload_all_nonatomic(items, suffix=".vcf")
|
||||
|
||||
if os.path.lexists(filesystem_path):
|
||||
replaced_items, new_item_hrefs = self._discover_existing_items_pre_overwrite(
|
||||
tmp_collection=col,
|
||||
dst_path=sane_path)
|
||||
pathutils.rename_exchange(tmp_filesystem_path, filesystem_path)
|
||||
else:
|
||||
# If the destination path does not exist, obviously all items are new
|
||||
new_item_hrefs = list(col._list())
|
||||
os.rename(tmp_filesystem_path, filesystem_path)
|
||||
self._sync_directory(parent_dir)
|
||||
except Exception as e:
|
||||
raise ValueError("Failed to create collection %r as %r %s" %
|
||||
(href, filesystem_path, e)) from e
|
||||
|
||||
# TODO: Return new-old pairs and just-new items (new vs updated)
|
||||
return self._collection_class(
|
||||
cast(multifilesystem.Storage, self),
|
||||
pathutils.unstrip_path(sane_path, True))
|
||||
pathutils.unstrip_path(sane_path, True)), replaced_items, new_item_hrefs
|
||||
|
|
|
@ -21,7 +21,7 @@ import errno
|
|||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from typing import Iterable, Iterator, TextIO, cast
|
||||
from typing import Iterable, Iterator, TextIO, cast, Optional, Tuple
|
||||
|
||||
import radicale.item as radicale_item
|
||||
from radicale import pathutils
|
||||
|
@ -36,10 +36,11 @@ class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
|
|||
CollectionPartHistory, CollectionBase):
|
||||
|
||||
def upload(self, href: str, item: radicale_item.Item
|
||||
) -> radicale_item.Item:
|
||||
) -> Tuple[radicale_item.Item, Optional[radicale_item.Item]]:
|
||||
if not pathutils.is_safe_filesystem_path_component(href):
|
||||
raise pathutils.UnsafePathError(href)
|
||||
path = pathutils.path_to_filesystem(self._filesystem_path, href)
|
||||
old_item = self._get(href, verify_href=False)
|
||||
try:
|
||||
with self._atomic_write(path, newline="") as fo: # type: ignore
|
||||
f = cast(TextIO, fo)
|
||||
|
@ -67,7 +68,7 @@ class CollectionPartUpload(CollectionPartGet, CollectionPartCache,
|
|||
uploaded_item = self._get(href, verify_href=False)
|
||||
if uploaded_item is None:
|
||||
raise RuntimeError("Storage modified externally")
|
||||
return uploaded_item
|
||||
return uploaded_item, old_item
|
||||
|
||||
def _upload_all_nonatomic(self, items: Iterable[radicale_item.Item],
|
||||
suffix: str = "") -> None:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue