318 lines
10 KiB
Python
318 lines
10 KiB
Python
from radicale.storage import BaseCollection
|
|
import urllib.request
|
|
import json
|
|
import hashlib
|
|
import couchdb
|
|
|
|
|
|
class Collection(BaseCollection):
|
|
|
|
# Overriden on copy by the "load" function
|
|
configuration = None
|
|
logger = None
|
|
|
|
# Properties of instance
|
|
"""The sanitized path of the collection without leading or trailing ``/``.
|
|
"""
|
|
path = ""
|
|
|
|
@property
|
|
def owner(self):
|
|
"""The owner of the collection."""
|
|
return self.path.split("/", maxsplit=1)[0]
|
|
|
|
@property
|
|
def is_principal(self):
|
|
"""Collection is a principal."""
|
|
return bool(self.path) and "/" not in self.path
|
|
|
|
@owner.setter
|
|
def owner(self, value):
|
|
# DEPRECATED: Included for compatibility reasons
|
|
pass
|
|
|
|
@is_principal.setter
|
|
def is_principal(self, value):
|
|
# DEPRECATED: Included for compatibility reasons
|
|
pass
|
|
|
|
@classmethod
|
|
def discover(cls, path, depth="0"):
|
|
"""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
|
|
|
|
@classmethod
|
|
def move(cls, item, to_collection, to_href):
|
|
"""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.
|
|
|
|
"""
|
|
if item.collection.path == to_collection.path and item.href == to_href:
|
|
return
|
|
to_collection.upload(to_href, item.item)
|
|
item.collection.delete(item.href)
|
|
|
|
@property
|
|
def etag(self):
|
|
"""Encoded as quoted-string (see RFC 2616)."""
|
|
etag = md5()
|
|
for item in self.get_all():
|
|
etag.update((item.href + "/" + item.etag).encode("utf-8"))
|
|
etag.update(json.dumps(self.get_meta(), sort_keys=True).encode())
|
|
return '"%s"' % etag.hexdigest()
|
|
|
|
@classmethod
|
|
def create_collection(cls, href, collection=None, props=None):
|
|
"""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.
|
|
|
|
``props["tag"]`` is the type of collection (VCALENDAR or
|
|
VADDRESSBOOK). If the key ``tag`` is missing, it is guessed from the
|
|
collection.
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def sync(self, old_token=None):
|
|
"""Get the current sync token and changed items for synchronization.
|
|
|
|
``old_token`` an old sync token which is used as the base of the
|
|
delta update. If sync token is missing, all items are returned.
|
|
ValueError is raised for invalid or old tokens.
|
|
|
|
WARNING: This simple default implementation treats all sync-token as
|
|
invalid. It adheres to the specification but some clients
|
|
(e.g. InfCloud) don't like it. Subclasses should provide a
|
|
more sophisticated implementation.
|
|
|
|
"""
|
|
token = "http://radicale.org/ns/sync/%s" % self.etag.strip("\"")
|
|
if old_token:
|
|
raise ValueError("Sync token are not supported")
|
|
return token, self.list()
|
|
|
|
def list(self):
|
|
"""List collection items."""
|
|
raise NotImplementedError
|
|
|
|
def get(self, href):
|
|
"""Fetch a single item."""
|
|
raise NotImplementedError
|
|
|
|
def get_multi(self, hrefs):
|
|
"""Fetch multiple items. Duplicate hrefs must be ignored.
|
|
|
|
DEPRECATED: use ``get_multi2`` instead
|
|
|
|
"""
|
|
return (self.get(href) for href in set(hrefs))
|
|
|
|
def get_multi2(self, hrefs):
|
|
"""Fetch multiple items.
|
|
|
|
Functionally similar to ``get``, but might bring performance benefits
|
|
on some storages when used cleverly. It's not required to return the
|
|
requested items in the correct order. Duplicated hrefs can be ignored.
|
|
|
|
Returns tuples with the href and the item or None if the item doesn't
|
|
exist.
|
|
|
|
"""
|
|
return ((href, self.get(href)) for href in hrefs)
|
|
|
|
def get_all(self):
|
|
"""Fetch all items.
|
|
|
|
Functionally similar to ``get``, but might bring performance benefits
|
|
on some storages when used cleverly.
|
|
|
|
"""
|
|
return map(self.get, self.list())
|
|
|
|
def get_all_filtered(self, filters):
|
|
"""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.
|
|
|
|
This returns all events by default
|
|
"""
|
|
return ((item, False) for item in self.get_all())
|
|
|
|
def pre_filtered_list(self, filters):
|
|
"""List collection items with optional pre filtering.
|
|
|
|
DEPRECATED: use ``get_all_filtered`` instead
|
|
|
|
"""
|
|
return self.get_all()
|
|
|
|
def has(self, href):
|
|
"""Check if an item exists by its href.
|
|
|
|
Functionally similar to ``get``, but might bring performance benefits
|
|
on some storages when used cleverly.
|
|
|
|
"""
|
|
return self.get(href) is not None
|
|
|
|
def upload(self, href, vobject_item):
|
|
"""Upload a new or replace an existing item."""
|
|
raise NotImplementedError
|
|
|
|
def delete(self, href=None):
|
|
"""Delete an item.
|
|
|
|
When ``href`` is ``None``, delete the collection.
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def get_meta(self, key=None):
|
|
"""Get metadata value for collection.
|
|
|
|
Return the value of the property ``key``. If ``key`` is ``None`` return
|
|
a dict with all properties
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def set_meta(self, props):
|
|
"""Set metadata values for collection.
|
|
|
|
``props`` a dict with updates for properties. If a value is empty, the
|
|
property must be deleted.
|
|
|
|
DEPRECATED: use ``set_meta_all`` instead
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def set_meta_all(self, props):
|
|
"""Set metadata values for collection.
|
|
|
|
``props`` a dict with values for properties.
|
|
|
|
"""
|
|
delta_props = self.get_meta()
|
|
for key in delta_props.keys():
|
|
if key not in props:
|
|
delta_props[key] = None
|
|
delta_props.update(props)
|
|
self.set_meta(self, delta_props)
|
|
|
|
@property
|
|
def last_modified(self):
|
|
"""Get the HTTP-datetime of when the collection was modified."""
|
|
raise NotImplementedError
|
|
|
|
def serialize(self):
|
|
"""Get the unicode string representing the whole collection."""
|
|
if self.get_meta("tag") == "VCALENDAR":
|
|
in_vcalendar = False
|
|
vtimezones = ""
|
|
included_tzids = set()
|
|
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 errornous ouput.
|
|
# 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)
|
|
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:])
|
|
elif self.get_meta("tag") == "VADDRESSBOOK":
|
|
return "".join((item.serialize() for item in self.get_all()))
|
|
return ""
|
|
|
|
@classmethod
|
|
@contextmanager
|
|
def acquire_lock(cls, mode, user=None):
|
|
"""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
|
|
|
|
@classmethod
|
|
def verify(cls):
|
|
"""Check the storage for errors."""
|
|
return True
|