mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-03-25 08:55:50 +00:00
Simple metadata interface
This commit is contained in:
parent
f087ec599e
commit
a007828f87
9 changed files with 155 additions and 1 deletions
|
|
@ -43,10 +43,28 @@ the client, which are free to choose a different scheme for filenames instead.
|
|||
.. _CardDAV: http://tools.ietf.org/html/rfc6352
|
||||
.. _CalDAV: http://tools.ietf.org/search/rfc4791
|
||||
|
||||
Metadata
|
||||
========
|
||||
|
||||
Any of the below metadata files may be absent. None of the files listed below
|
||||
have any file extensions.
|
||||
|
||||
- A file called ``color`` inside the vdir indicates the vdir's color, a
|
||||
property that is only relevant in UI design.
|
||||
|
||||
Its content is an ASCII-encoded hex-RGB value of the form ``#RRGGBB``. For
|
||||
example, a file content of ``#FF0000`` indicates that the vdir has a red
|
||||
(user-visible) color. No short forms or informal values such as ``red`` (as
|
||||
known from CSS, for example) are allowed. The prefixing ``#`` must be
|
||||
present.
|
||||
|
||||
- A file called ``displayname`` contains a UTF-8 encoded label that may be used
|
||||
to represent the vdir in UIs.
|
||||
|
||||
Writing to vdirs
|
||||
================
|
||||
|
||||
Creating and modifying items *should* happen atomically_.
|
||||
Creating and modifying items or metadata files *should* happen atomically_.
|
||||
|
||||
Writing to a temporary file on the same physical device, and then moving it to
|
||||
the appropriate location is usually a very effective solution. For this
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ def format_item(item_template, uid=None):
|
|||
class StorageTests(object):
|
||||
storage_class = None
|
||||
supports_collections = True
|
||||
supports_metadata = True
|
||||
|
||||
@pytest.fixture(params=['VEVENT', 'VTODO', 'VCARD'])
|
||||
def item_type(self, request):
|
||||
|
|
@ -55,6 +56,11 @@ class StorageTests(object):
|
|||
if not self.supports_collections:
|
||||
pytest.skip('This storage does not support collections.')
|
||||
|
||||
@pytest.fixture
|
||||
def requires_metadata(self):
|
||||
if not self.supports_metadata:
|
||||
pytest.skip('This storage does not support metadata.')
|
||||
|
||||
def test_generic(self, s, get_item):
|
||||
items = [get_item() for i in range(1, 10)]
|
||||
hrefs = []
|
||||
|
|
@ -227,3 +233,13 @@ class StorageTests(object):
|
|||
s = self.storage_class(**get_storage_args(collection=collname))
|
||||
href, etag = s.upload(get_item())
|
||||
s.get(href)
|
||||
|
||||
def test_metadata(self, requires_metadata, s):
|
||||
try:
|
||||
s.set_meta('color', u'#ff0000')
|
||||
assert s.get_meta('color') == u'#ff0000'
|
||||
except exceptions.UnsupportedMetadataError:
|
||||
pass
|
||||
|
||||
s.set_meta('displayname', u'hello world')
|
||||
assert s.get_meta('displayname') == u'hello world'
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class CombinedStorage(Storage):
|
|||
class TestHttpStorage(StorageTests):
|
||||
storage_class = CombinedStorage
|
||||
supports_collections = False
|
||||
supports_metadata = False
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_tmpdir(self, tmpdir, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class TestSingleFileStorage(StorageTests):
|
|||
|
||||
storage_class = SingleFileStorage
|
||||
supports_collections = False
|
||||
supports_metadata = False
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, tmpdir):
|
||||
|
|
|
|||
|
|
@ -56,3 +56,7 @@ class ReadOnlyError(Error):
|
|||
|
||||
class InvalidResponse(Error, ValueError):
|
||||
'''The backend returned an invalid result.'''
|
||||
|
||||
|
||||
class UnsupportedMetadataError(Error, NotImplementedError):
|
||||
'''The storage doesn't support this type of metadata.'''
|
||||
|
|
|
|||
|
|
@ -208,3 +208,25 @@ class Storage(with_metaclass(StorageMeta)):
|
|||
when.
|
||||
'''
|
||||
yield
|
||||
|
||||
def get_meta(self, key):
|
||||
'''Get metadata value for collection/storage.
|
||||
|
||||
See the vdir specification for the keys that *have* to be accepted.
|
||||
|
||||
:param key: The metadata key.
|
||||
:type key: unicode
|
||||
'''
|
||||
|
||||
raise NotImplementedError('This storage does not support metadata.')
|
||||
|
||||
def set_meta(self, key, value):
|
||||
'''Get metadata value for collection/storage.
|
||||
|
||||
:param key: The metadata key.
|
||||
:type key: unicode
|
||||
:param value: The value.
|
||||
:type value: unicode
|
||||
'''
|
||||
|
||||
raise NotImplementedError('This storage does not support metadata.')
|
||||
|
|
|
|||
|
|
@ -336,6 +336,10 @@ class DavStorage(Storage):
|
|||
_session = None
|
||||
_repr_attributes = ('username', 'url')
|
||||
|
||||
_property_table = {
|
||||
'displayname': ('displayname', 'DAV:'),
|
||||
}
|
||||
|
||||
def __init__(self, url, username='', password='', verify=True, auth=None,
|
||||
useragent=USERAGENT, unsafe_href_chars='@',
|
||||
verify_fingerprint=None, auth_cert=None, **kwargs):
|
||||
|
|
@ -546,6 +550,65 @@ class DavStorage(Storage):
|
|||
for href, etag, prop in rv:
|
||||
yield utils.compat.urlunquote(href), etag
|
||||
|
||||
def get_meta(self, key):
|
||||
try:
|
||||
tagname, namespace = self._property_table[key]
|
||||
except KeyError:
|
||||
raise exceptions.UnsupportedMetadataError()
|
||||
|
||||
lxml_selector = '{%s}%s' % (namespace, tagname)
|
||||
data = '''<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
{}
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
'''.format(
|
||||
to_native(etree.tostring(etree.Element(lxml_selector)))
|
||||
)
|
||||
|
||||
headers = self.session.get_default_headers()
|
||||
headers['Depth'] = 0
|
||||
|
||||
response = self.session.request(
|
||||
'PROPFIND', '',
|
||||
data=data, headers=headers
|
||||
)
|
||||
|
||||
root = _parse_xml(response.content)
|
||||
|
||||
for prop in root.findall('.//' + lxml_selector):
|
||||
text = getattr(prop, 'text', None)
|
||||
if text:
|
||||
return text
|
||||
|
||||
def set_meta(self, key, value):
|
||||
try:
|
||||
tagname, namespace = self._property_table[key]
|
||||
except KeyError:
|
||||
raise exceptions.UnsupportedMetadataError()
|
||||
|
||||
lxml_selector = '{%s}%s' % (namespace, tagname)
|
||||
element = etree.Element(lxml_selector)
|
||||
element.text = value
|
||||
|
||||
data = '''<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propertyupdate xmlns:D="DAV:">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
{}
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</D:propertyupdate>
|
||||
'''.format(to_native(etree.tostring(element)))
|
||||
|
||||
self.session.request(
|
||||
'PROPPATCH', '',
|
||||
data=data, headers=self.session.get_default_headers()
|
||||
)
|
||||
|
||||
# FIXME: Deal with response
|
||||
|
||||
|
||||
class CaldavStorage(DavStorage):
|
||||
|
||||
|
|
@ -598,6 +661,11 @@ class CaldavStorage(DavStorage):
|
|||
|
||||
get_multi_data_query = '{urn:ietf:params:xml:ns:caldav}calendar-data'
|
||||
|
||||
_property_table = dict(DavStorage._property_table)
|
||||
_property_table.update({
|
||||
'color': ('calendar-color', 'http://apple.com/ns/ical/'),
|
||||
})
|
||||
|
||||
def __init__(self, start_date=None, end_date=None,
|
||||
item_types=(), **kwargs):
|
||||
super(CaldavStorage, self).__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -180,3 +180,20 @@ class FilesystemStorage(Storage):
|
|||
subprocess.call([self.post_hook, fpath])
|
||||
except OSError as e:
|
||||
logger.warning('Error executing external hook: {}'.format(str(e)))
|
||||
|
||||
def get_meta(self, key):
|
||||
fpath = os.path.join(self.path, key)
|
||||
try:
|
||||
with open(fpath, 'rb') as f:
|
||||
return f.read().decode(self.encoding)
|
||||
except IOError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
def set_meta(self, key, value):
|
||||
assert isinstance(value, text_type)
|
||||
fpath = os.path.join(self.path, key)
|
||||
with atomic_write(fpath, mode='wb', overwrite=True) as f:
|
||||
f.write(value.encode(self.encoding))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class MemoryStorage(Storage):
|
|||
raise exceptions.UserError('MemoryStorage does not support '
|
||||
'collections.')
|
||||
self.items = {} # href => (etag, item)
|
||||
self.metadata = {}
|
||||
self.fileext = fileext
|
||||
super(MemoryStorage, self).__init__(**kwargs)
|
||||
|
||||
|
|
@ -63,3 +64,9 @@ class MemoryStorage(Storage):
|
|||
if etag != self.items[href][0]:
|
||||
raise exceptions.WrongEtagError(etag)
|
||||
del self.items[href]
|
||||
|
||||
def get_meta(self, key):
|
||||
return self.metadata[key]
|
||||
|
||||
def set_meta(self, key, value):
|
||||
self.metadata[key] = value
|
||||
|
|
|
|||
Loading…
Reference in a new issue