1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00
Radicale/radicale/storage/__init__.py

356 lines
12 KiB
Python
Raw Normal View History

2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2018-08-28 16:19:36 +02:00
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
2019-06-17 04:13:24 +02:00
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
2018-08-28 16:19:36 +02:00
#
# 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/>.
"""
2020-01-12 23:32:28 +01:00
The storage module that stores calendars and address books.
2018-08-28 16:19:36 +02:00
2020-01-12 23:32:28 +01:00
Take a look at the class ``BaseCollection`` if you want to implement your own.
2018-08-28 16:19:36 +02:00
"""
import json
2021-07-26 20:56:46 +02:00
import xml.etree.ElementTree as ET
from hashlib import sha256
2024-09-22 18:57:48 +02:00
from typing import (Callable, ContextManager, Iterable, Iterator, Mapping,
Optional, Sequence, Set, Tuple, Union, overload)
2018-08-28 16:19:36 +02:00
import vobject
2021-07-26 20:56:46 +02:00
from radicale import config
from radicale import item as radicale_item
from radicale import types, utils
2018-09-04 03:33:48 +02:00
from radicale.item import filter as radicale_filter
2018-08-28 16:19:36 +02:00
2021-12-08 21:41:12 +01:00
INTERNAL_TYPES: Sequence[str] = ("multifilesystem", "multifilesystem_nolock",)
2018-08-28 16:19:36 +02:00
CACHE_DEPS: Sequence[str] = ("radicale", "vobject")
2021-07-26 20:56:46 +02:00
CACHE_VERSION: bytes = "".join(
"%s=%s;" % (pkg, utils.package_version(pkg))
2021-07-26 20:56:46 +02:00
for pkg in CACHE_DEPS).encode()
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def load(configuration: "config.Configuration") -> "BaseStorage":
2020-01-14 22:43:48 +01:00
"""Load the storage module chosen in configuration."""
2021-07-26 20:56:46 +02:00
return utils.load_plugin(INTERNAL_TYPES, "storage", "Storage", BaseStorage,
configuration)
2018-08-28 16:19:36 +02:00
class ComponentExistsError(ValueError):
2021-07-26 20:56:46 +02:00
def __init__(self, path: str) -> None:
2018-08-28 16:19:36 +02:00
message = "Component already exists: %r" % path
super().__init__(message)
class ComponentNotFoundError(ValueError):
2021-07-26 20:56:46 +02:00
def __init__(self, path: str) -> None:
2018-08-28 16:19:36 +02:00
message = "Component doesn't exist: %r" % path
super().__init__(message)
class BaseCollection:
@property
2021-07-26 20:56:46 +02:00
def path(self) -> str:
"""The sanitized path of the collection without leading or
trailing ``/``."""
raise NotImplementedError
2018-08-28 16:19:36 +02:00
@property
2021-07-26 20:56:46 +02:00
def owner(self) -> str:
2018-08-28 16:19:36 +02:00
"""The owner of the collection."""
return self.path.split("/", maxsplit=1)[0]
@property
2021-07-26 20:56:46 +02:00
def is_principal(self) -> bool:
2018-08-28 16:19:36 +02:00
"""Collection is a principal."""
return bool(self.path) and "/" not in self.path
@property
2021-07-26 20:56:46 +02:00
def etag(self) -> str:
2018-08-28 16:19:36 +02:00
"""Encoded as quoted-string (see RFC 2616)."""
etag = sha256()
2018-08-28 16:19:36 +02:00
for item in self.get_all():
2021-07-26 20:56:46 +02:00
assert item.href
2020-01-19 18:13:05 +01:00
etag.update((item.href + "/" + item.etag).encode())
2018-08-28 16:19:36 +02:00
etag.update(json.dumps(self.get_meta(), sort_keys=True).encode())
return '"%s"' % etag.hexdigest()
2021-07-26 20:56:46 +02:00
@property
def tag(self) -> str:
"""The tag of the collection."""
return self.get_meta("tag") or ""
def sync(self, old_token: str = "") -> Tuple[str, Iterable[str]]:
2018-08-28 16:19:36 +02:00
"""Get the current sync token and changed items for synchronization.
``old_token`` an old sync token which is used as the base of the
2021-07-26 20:56:46 +02:00
delta update. If sync token is empty, all items are returned.
2018-08-28 16:19:36 +02:00
ValueError is raised for invalid or old tokens.
WARNING: This simple default implementation treats all sync-token as
invalid.
2018-08-28 16:19:36 +02:00
"""
2021-07-26 20:56:46 +02:00
def hrefs_iter() -> Iterator[str]:
for item in self.get_all():
assert item.href
yield item.href
2018-08-28 16:19:36 +02:00
token = "http://radicale.org/ns/sync/%s" % self.etag.strip("\"")
if old_token:
raise ValueError("Sync token are not supported")
2021-07-26 20:56:46 +02:00
return token, hrefs_iter()
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def get_multi(self, hrefs: Iterable[str]
) -> Iterable[Tuple[str, Optional["radicale_item.Item"]]]:
2018-08-28 16:19:36 +02:00
"""Fetch multiple items.
It's not required to return the requested items in the correct order.
Duplicated hrefs can be ignored.
2018-08-28 16:19:36 +02:00
Returns tuples with the href and the item or None if the item doesn't
exist.
"""
raise NotImplementedError
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def get_all(self) -> Iterable["radicale_item.Item"]:
"""Fetch all items."""
raise NotImplementedError
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def get_filtered(self, filters: Iterable[ET.Element]
) -> Iterable[Tuple["radicale_item.Item", bool]]:
2018-08-28 16:19:36 +02:00
"""Fetch all items with optional filtering.
This can largely improve performance of reports depending on
the filters and this implementation.
Returns tuples in the form ``(item, filters_matched)``.
``filters_matched`` is a bool that indicates if ``filters`` are fully
matched.
"""
2021-07-26 20:56:46 +02:00
if not self.tag:
return
2018-09-04 03:33:48 +02:00
tag, start, end, simple = radicale_filter.simplify_prefilters(
2021-07-26 20:56:46 +02:00
filters, self.tag)
2018-09-04 03:33:48 +02:00
for item in self.get_all():
2021-07-26 20:56:46 +02:00
if tag is not None and tag != item.component_name:
continue
istart, iend = item.time_range
if istart >= end or iend <= start:
continue
yield item, simple and (start <= istart or iend <= end)
def has_uid(self, uid: str) -> bool:
2018-08-28 16:19:36 +02:00
"""Check if a UID exists in the collection."""
for item in self.get_all():
if item.uid == uid:
return True
return False
2021-07-26 20:56:46 +02:00
def upload(self, href: str, item: "radicale_item.Item") -> (
"radicale_item.Item"):
2018-08-28 16:19:36 +02:00
"""Upload a new or replace an existing item."""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def delete(self, href: Optional[str] = None) -> None:
2018-08-28 16:19:36 +02:00
"""Delete an item.
When ``href`` is ``None``, delete the collection.
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
@overload
def get_meta(self, key: None = None) -> Mapping[str, str]: ...
@overload
def get_meta(self, key: str) -> Optional[str]: ...
def get_meta(self, key: Optional[str] = None
) -> Union[Mapping[str, str], Optional[str]]:
2018-08-28 16:19:36 +02:00
"""Get metadata value for collection.
Return the value of the property ``key``. If ``key`` is ``None`` return
a dict with all properties
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def set_meta(self, props: Mapping[str, str]) -> None:
2018-08-28 16:19:36 +02:00
"""Set metadata values for collection.
``props`` a dict with values for properties.
"""
raise NotImplementedError
@property
2021-07-26 20:56:46 +02:00
def last_modified(self) -> str:
2018-08-28 16:19:36 +02:00
"""Get the HTTP-datetime of when the collection was modified."""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def serialize(self) -> str:
2018-08-28 16:19:36 +02:00
"""Get the unicode string representing the whole collection."""
2021-07-26 20:56:46 +02:00
if self.tag == "VCALENDAR":
2018-08-28 16:19:36 +02:00
in_vcalendar = False
vtimezones = ""
2021-07-26 20:56:46 +02:00
included_tzids: Set[str] = set()
2018-08-28 16:19:36 +02:00
vtimezone = []
tzid = None
components = ""
# Concatenate all child elements of VCALENDAR from all items
# together, while preventing duplicated VTIMEZONE entries.
# VTIMEZONEs are only distinguished by their TZID, if different
# timezones share the same TZID this produces erroneous output.
2018-08-28 16:19:36 +02:00
# VObject fails at this too.
for item in self.get_all():
depth = 0
for line in item.serialize().split("\r\n"):
if line.startswith("BEGIN:"):
depth += 1
if depth == 1 and line == "BEGIN:VCALENDAR":
in_vcalendar = True
elif in_vcalendar:
if depth == 1 and line.startswith("END:"):
in_vcalendar = False
if depth == 2 and line == "BEGIN:VTIMEZONE":
vtimezone.append(line + "\r\n")
elif vtimezone:
vtimezone.append(line + "\r\n")
if depth == 2 and line.startswith("TZID:"):
tzid = line[len("TZID:"):]
elif depth == 2 and line.startswith("END:"):
if tzid is None or tzid not in included_tzids:
vtimezones += "".join(vtimezone)
2021-07-26 20:56:46 +02:00
if tzid is not None:
2018-08-28 16:19:36 +02:00
included_tzids.add(tzid)
vtimezone.clear()
tzid = None
elif depth >= 2:
components += line + "\r\n"
if line.startswith("END:"):
depth -= 1
template = vobject.iCalendar()
displayname = self.get_meta("D:displayname")
if displayname:
template.add("X-WR-CALNAME")
template.x_wr_calname.value_param = "TEXT"
template.x_wr_calname.value = displayname
description = self.get_meta("C:calendar-description")
if description:
template.add("X-WR-CALDESC")
template.x_wr_caldesc.value_param = "TEXT"
template.x_wr_caldesc.value = description
template = template.serialize()
template_insert_pos = template.find("\r\nEND:VCALENDAR\r\n") + 2
assert template_insert_pos != -1
return (template[:template_insert_pos] +
vtimezones + components +
template[template_insert_pos:])
2021-07-26 20:56:46 +02:00
if self.tag == "VADDRESSBOOK":
2018-08-28 16:19:36 +02:00
return "".join((item.serialize() for item in self.get_all()))
return ""
class BaseStorage:
2021-07-26 20:56:46 +02:00
def __init__(self, configuration: "config.Configuration") -> None:
"""Initialize BaseStorage.
``configuration`` see ``radicale.config`` module.
The ``configuration`` must not change during the lifetime of
this object, it is kept as an internal reference.
"""
self.configuration = configuration
def discover(
self, path: str, depth: str = "0",
child_context_manager: Optional[
Callable[[str, Optional[str]], ContextManager[None]]] = None,
user_groups: Set[str] = set([])) -> Iterable["types.CollectionOrItem"]:
"""Discover a list of collections under the given ``path``.
``path`` is sanitized.
If ``depth`` is "0", only the actual object under ``path`` is
returned.
If ``depth`` is anything but "0", it is considered as "1" and direct
children are included in the result.
The root collection "/" must always exist.
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def move(self, item: "radicale_item.Item", to_collection: BaseCollection,
to_href: str) -> None:
"""Move an object.
``item`` is the item to move.
``to_collection`` is the target collection.
``to_href`` is the target name in ``to_collection``. An item with the
same name might already exist.
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def create_collection(
self, href: str,
items: Optional[Iterable["radicale_item.Item"]] = None,
props: Optional[Mapping[str, str]] = None) -> BaseCollection:
"""Create a collection.
``href`` is the sanitized path.
If the collection already exists and neither ``collection`` nor
``props`` are set, this method shouldn't do anything. Otherwise the
existing collection must be replaced.
``collection`` is a list of vobject components.
``props`` are metadata values for the collection.
2021-07-26 20:56:46 +02:00
``props["tag"]`` is the type of collection (VCALENDAR or VADDRESSBOOK).
If the key ``tag`` is missing, ``items`` is ignored.
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
@types.contextmanager
def acquire_lock(self, mode: str, user: str = "") -> Iterator[None]:
2018-08-28 16:19:36 +02:00
"""Set a context manager to lock the whole storage.
``mode`` must either be "r" for shared access or "w" for exclusive
access.
``user`` is the name of the logged in user or empty.
"""
raise NotImplementedError
2021-07-26 20:56:46 +02:00
def verify(self) -> bool:
2018-08-28 16:19:36 +02:00
"""Check the storage for errors."""
raise NotImplementedError