Use vobject

This commit is contained in:
Guillaume Ayoub 2016-04-10 01:36:45 +02:00
parent b495bfa59f
commit 73d39ea572
3 changed files with 42 additions and 54 deletions

View File

@ -31,6 +31,8 @@ from contextlib import contextmanager
from random import randint from random import randint
from uuid import uuid4 from uuid import uuid4
import vobject
def serialize(tag, headers=(), items=()): def serialize(tag, headers=(), items=()):
"""Return a text corresponding to given collection ``tag``. """Return a text corresponding to given collection ``tag``.
@ -41,14 +43,15 @@ def serialize(tag, headers=(), items=()):
""" """
items = sorted(items, key=lambda x: x.name) items = sorted(items, key=lambda x: x.name)
if tag == "VADDRESSBOOK": if tag == "VADDRESSBOOK":
lines = [item.text for item in items] lines = [item.text.strip() for item in items]
else: else:
lines = ["BEGIN:%s" % tag] lines = ["BEGIN:%s" % tag]
for part in (headers, items): for part in (headers, items):
if part: if part:
lines.append("\n".join(item.text for item in part)) lines.append("\r\n".join(item.text.strip() for item in part))
lines.append("END:%s\n" % tag) lines.append("END:%s" % tag)
return "\n".join(lines) lines.append("")
return "\r\n".join(lines)
def sanitize_path(path): def sanitize_path(path):
@ -90,9 +93,14 @@ class Item(object):
"""Internal iCal item.""" """Internal iCal item."""
def __init__(self, text, name=None): def __init__(self, text, name=None):
"""Initialize object from ``text`` and different ``kwargs``.""" """Initialize object from ``text`` and different ``kwargs``."""
self.text = text self.component = vobject.readOne(text)
self._name = name self._name = name
if not self.component.name:
# Header
self._name = next(self.component.lines()).name.lower()
return
# We must synchronize the name in the text and in the object. # We must synchronize the name in the text and in the object.
# An item must have a name, determined in order by: # An item must have a name, determined in order by:
# #
@ -101,31 +109,20 @@ class Item(object):
# - the ``UID`` iCal property (for Events, Todos, Journals) # - the ``UID`` iCal property (for Events, Todos, Journals)
# - the ``TZID`` iCal property (for Timezones) # - the ``TZID`` iCal property (for Timezones)
if not self._name: if not self._name:
for line in unfold(self.text): for line in self.component.lines():
if line.startswith("X-RADICALE-NAME:"): if line.name in ("X-RADICALE-NAME", "UID", "TZID"):
self._name = line.replace("X-RADICALE-NAME:", "").strip() self._name = line.value
if line.name == "X-RADICALE-NAME":
break break
elif line.startswith("UID:"):
self._name = line.replace("UID:", "").strip()
# Do not break, a ``X-RADICALE-NAME`` can appear next
elif line.startswith("TZID:"):
self._name = line.replace("TZID:", "").strip()
# Do not break, a ``X-RADICALE-NAME`` can appear next
if self._name: if self._name:
self._name = clean_name(self._name) self._name = clean_name(self._name)
if "\nX-RADICALE-NAME:" in text:
for line in unfold(self.text):
if line.startswith("X-RADICALE-NAME:"):
self.text = self.text.replace(
line, "X-RADICALE-NAME:%s" % self._name)
else:
self.text = self.text.replace(
"\nEND:V", "\nX-RADICALE-NAME:%s\nEND:V" % self._name)
else: else:
self._name = uuid4().hex self._name = uuid4().hex
self.text = self.text.replace(
"\nEND:V", "\nX-RADICALE-NAME:%s\nEND:V" % self._name) if not hasattr(self.component, "x_radicale_name"):
self.component.add("X-RADICALE-NAME")
self.component.x_radicale_name.value = self._name
def __hash__(self): def __hash__(self):
return hash(self.text) return hash(self.text)
@ -153,6 +150,11 @@ class Item(object):
""" """
return self._name return self._name
@property
def text(self):
"""Item serialized text."""
return self.component.serialize()
class Header(Item): class Header(Item):
"""Internal header class.""" """Internal header class."""
@ -335,32 +337,18 @@ class Collection(object):
Return a dict of items. Return a dict of items.
""" """
item_tags = {} item_tags = {item_type.tag: item_type for item_type in item_types}
for item_type in item_types:
item_tags[item_type.tag] = item_type
items = {} items = {}
root = next(vobject.readComponents(text))
lines = unfold(text) components = (
in_item = False root.components() if root.name in ("VADDRESSBOOK", "VCALENDAR")
else (root,))
for line in lines: for component in components:
if line.startswith("BEGIN:") and not in_item: item_name = None if component.name == "VTIMEZONE" else name
item_tag = line.replace("BEGIN:", "").strip() item_type = item_tags[component.name]
if item_tag in item_tags: item = item_type(component.serialize(), item_name)
in_item = True
item_lines = []
if in_item:
item_lines.append(line)
if line.startswith("END:%s" % item_tag):
in_item = False
item_type = item_tags[item_tag]
item_text = "\n".join(item_lines)
item_name = None if item_tag == "VTIMEZONE" else name
item = item_type(item_text, item_name)
if item.name in items: if item.name in items:
text = "\n".join((item.text, items[item.name].text)) text = "\r\n".join((item.text, items[item.name].text))
items[item.name] = item_type(text, item.name) items[item.name] = item_type(text, item.name)
else: else:
items[item.name] = item items[item.name] = item

View File

@ -140,7 +140,6 @@ class Collection(ical.Collection):
ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card) ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card)
for name, component in self._parse(text, item_types).items(): for name, component in self._parse(text, item_types).items():
if not is_safe_filesystem_path_component(name): if not is_safe_filesystem_path_component(name):
# TODO: Timezones with slashes can't be saved
log.LOGGER.debug( log.LOGGER.debug(
"Can't tranlate name safely to filesystem, " "Can't tranlate name safely to filesystem, "
"skipping component: %s", name) "skipping component: %s", name)

View File

@ -56,6 +56,7 @@ setup(
packages=["radicale"], packages=["radicale"],
provides=["radicale"], provides=["radicale"],
scripts=["bin/radicale"], scripts=["bin/radicale"],
install_requires=["vobject"],
keywords=["calendar", "addressbook", "CalDAV", "CardDAV"], keywords=["calendar", "addressbook", "CalDAV", "CardDAV"],
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",