mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-03-25 08:55:50 +00:00
vdirsyncer can now be somewhat used
This commit is contained in:
parent
dc12b74805
commit
46fa1d7c47
8 changed files with 124 additions and 45 deletions
|
|
@ -15,9 +15,13 @@ from vdirsyncer.sync import sync
|
|||
from vdirsyncer.storage.caldav import CaldavStorage
|
||||
from vdirsyncer.storage.filesystem import FilesystemStorage
|
||||
from vdirsyncer.utils import expand_path
|
||||
import vdirsyncer.log as log
|
||||
import argvard
|
||||
|
||||
|
||||
cli_logger = log.get('cli')
|
||||
|
||||
|
||||
storage_names = {
|
||||
'caldav': CaldavStorage,
|
||||
'filesystem': FilesystemStorage
|
||||
|
|
@ -43,7 +47,8 @@ def get_config_parser(env):
|
|||
elif section == 'general':
|
||||
general = dict(c.items(section))
|
||||
else:
|
||||
raise RuntimeError('Unknown section: {}'.format(section))
|
||||
cli_logger.error(
|
||||
'Unknown section in {}: {}'.format(fname, section))
|
||||
|
||||
return general, pairs, storages
|
||||
|
||||
|
|
@ -61,16 +66,31 @@ def save_status(basepath, pair_name, status):
|
|||
with open(full_path, 'w+') as f:
|
||||
for k, v in status.items():
|
||||
json.dump((k, v), f)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def storage_instance_from_config(config):
|
||||
config = dict(config)
|
||||
cls = storage_names[config.pop('type')]
|
||||
storage_name = config.pop('type')
|
||||
cls = storage_names[storage_name]
|
||||
try:
|
||||
return cls(**config)
|
||||
except TypeError:
|
||||
print(config)
|
||||
raise
|
||||
except TypeError as e:
|
||||
import inspect
|
||||
x = cli_logger.critical
|
||||
spec = inspect.getargspec(cls.__init__)
|
||||
required_args = set(spec.args[:-len(spec.defaults)])
|
||||
|
||||
x(str(e))
|
||||
x('')
|
||||
x('Unable to initialize storage {}.'.format(storage_name))
|
||||
x('Here are the required arguments for the storage:')
|
||||
x(list(required_args - {'self'}))
|
||||
x('Here are the optional arguments:')
|
||||
x(list(set(spec.args) - required_args))
|
||||
x('And here are the ones you gave: ')
|
||||
x(list(config))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -86,11 +106,15 @@ def _main(env, file_cfg):
|
|||
|
||||
@app.main()
|
||||
def app_main(context):
|
||||
print("heY")
|
||||
print("Hello.")
|
||||
|
||||
@app.option('--debug|-v')
|
||||
def debug_option(context):
|
||||
log.get('cli').setLevel(log.logging.DEBUG)
|
||||
log.get('sync').setLevel(log.logging.DEBUG)
|
||||
|
||||
sync_command = argvard.Command()
|
||||
|
||||
|
||||
@sync_command.main('[pairs...]')
|
||||
def sync_main(context, pairs=None):
|
||||
if pairs is None:
|
||||
|
|
@ -100,13 +124,15 @@ def _main(env, file_cfg):
|
|||
try:
|
||||
a, b = all_pairs[pair_name]
|
||||
except KeyError:
|
||||
print('Pair not found: {}'.format(pair_name))
|
||||
print(file_cfg)
|
||||
cli_logger.critical('Pair not found: {}'.format(pair_name))
|
||||
cli_logger.critical('These are the pairs found: ')
|
||||
cli_logger.critical(list(all_pairs))
|
||||
sys.exit(1)
|
||||
a = storage_instance_from_config(all_storages[a])
|
||||
b = storage_instance_from_config(all_storages[b])
|
||||
|
||||
def x(a=a, b=b, pair_name=pair_name):
|
||||
cli_logger.debug('Syncing {}'.format(pair_name))
|
||||
status = load_status(general['status_path'], pair_name)
|
||||
sync(a, b, status)
|
||||
save_status(general['status_path'], pair_name, status)
|
||||
|
|
|
|||
30
vdirsyncer/log.py
Normal file
30
vdirsyncer/log.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
vdirsyncer.log
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
:copyright: (c) 2014 Markus Unterwaditzer
|
||||
:license: MIT, see LICENSE for more details.
|
||||
'''
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
|
||||
|
||||
def create_logger(name):
|
||||
x = logging.getLogger(name)
|
||||
x.setLevel(logging.WARNING)
|
||||
x.addHandler(stdout_handler)
|
||||
return x
|
||||
|
||||
|
||||
loggers = {}
|
||||
|
||||
|
||||
def get(name):
|
||||
name = 'watdo.' + name
|
||||
if name not in loggers:
|
||||
loggers[name] = create_logger(name)
|
||||
return loggers[name]
|
||||
|
|
@ -13,16 +13,14 @@ class Item(object):
|
|||
'''should-be-immutable wrapper class for VCALENDAR and VCARD'''
|
||||
|
||||
def __init__(self, raw):
|
||||
self.raw = raw
|
||||
self._uid = None
|
||||
assert type(raw) is unicode
|
||||
raw = raw.splitlines()
|
||||
self.uid = None
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
if self._uid is None:
|
||||
for line in self.raw.splitlines():
|
||||
if line.startswith(b'UID:'):
|
||||
self._uid = line[4:].strip()
|
||||
return self._uid
|
||||
for line in raw:
|
||||
if line.startswith(u'UID:'):
|
||||
self.uid = line[4:].strip()
|
||||
self.raw = '\n'.join(raw)
|
||||
|
||||
|
||||
class Storage(object):
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from .base import Storage, Item
|
|||
import vdirsyncer.exceptions as exceptions
|
||||
from lxml import etree
|
||||
import requests
|
||||
import urlparse
|
||||
import datetime
|
||||
|
||||
CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ'
|
||||
|
|
@ -53,6 +54,7 @@ class CaldavStorage(Storage):
|
|||
|
||||
self.useragent = useragent
|
||||
self.url = url.rstrip('/') + '/'
|
||||
self.parsed_url = urlparse.urlparse(self.url)
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
|
||||
|
|
@ -74,8 +76,10 @@ class CaldavStorage(Storage):
|
|||
}
|
||||
|
||||
def _simplify_href(self, href):
|
||||
if href.startswith(self.url):
|
||||
return href[len(self.url):]
|
||||
href = urlparse.urlparse(href).path
|
||||
if href.startswith(self.parsed_url.path):
|
||||
href = href[len(self.parsed_url.path):]
|
||||
assert '/' not in href, href
|
||||
return href
|
||||
|
||||
def _request(self, method, item, data=None, headers=None):
|
||||
|
|
@ -144,7 +148,7 @@ class CaldavStorage(Storage):
|
|||
</C:calendar-multiget>'''
|
||||
href_xml = []
|
||||
for href in hrefs:
|
||||
assert '/' not in href
|
||||
assert '/' not in href, href
|
||||
href_xml.append('<D:href>{}</D:href>'.format(self.url + href))
|
||||
data = data.format(hrefs='\n'.join(href_xml))
|
||||
response = self._request(
|
||||
|
|
@ -154,19 +158,24 @@ class CaldavStorage(Storage):
|
|||
headers=self._default_headers()
|
||||
)
|
||||
response.raise_for_status()
|
||||
root = etree.XML(response.content)
|
||||
root = etree.XML(response.content) # etree only can handle bytes
|
||||
rv = []
|
||||
hrefs_left = set(hrefs)
|
||||
for element in root.iter('{DAV:}response'):
|
||||
href = self._simplify_href(element.find('{DAV:}href').text)
|
||||
href = self._simplify_href(
|
||||
element.find('{DAV:}href').text.decode(response.encoding))
|
||||
obj = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{urn:ietf:params:xml:ns:caldav}calendar-data').text
|
||||
etag = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{DAV:}getetag').text
|
||||
.find('{DAV:}prop') \
|
||||
.find('{DAV:}getetag').text
|
||||
if isinstance(obj, bytes):
|
||||
obj = obj.decode(response.encoding)
|
||||
if isinstance(etag, bytes):
|
||||
etag = etag.decode(response.encoding)
|
||||
rv.append((href, Item(obj), etag))
|
||||
hrefs_left.remove(href)
|
||||
for href in hrefs_left:
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ class FilesystemStorage(Storage):
|
|||
mtime is etag
|
||||
filename without path is href'''
|
||||
|
||||
def __init__(self, path, fileext, **kwargs):
|
||||
def __init__(self, path, fileext, encoding='utf-8', **kwargs):
|
||||
'''
|
||||
:param path: Absolute path to a *collection* inside a vdir.
|
||||
'''
|
||||
self.path = expand_path(path)
|
||||
self.encoding = encoding
|
||||
self.fileext = fileext
|
||||
super(FilesystemStorage, self).__init__(**kwargs)
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ class FilesystemStorage(Storage):
|
|||
def get(self, href):
|
||||
fpath = self._get_filepath(href)
|
||||
with open(fpath, 'rb') as f:
|
||||
return Item(f.read()), os.path.getmtime(fpath)
|
||||
return Item(f.read().decode(self.encoding)), os.path.getmtime(fpath)
|
||||
|
||||
def has(self, href):
|
||||
return os.path.isfile(self._get_filepath(href))
|
||||
|
|
@ -53,7 +54,7 @@ class FilesystemStorage(Storage):
|
|||
if os.path.exists(fpath):
|
||||
raise exceptions.AlreadyExistingError(obj.uid)
|
||||
with open(fpath, 'wb+') as f:
|
||||
f.write(obj.raw)
|
||||
f.write(obj.raw.encode(self.encoding))
|
||||
return href, os.path.getmtime(fpath)
|
||||
|
||||
def update(self, href, obj, etag):
|
||||
|
|
@ -67,7 +68,7 @@ class FilesystemStorage(Storage):
|
|||
raise exceptions.WrongEtagError(etag, actual_etag)
|
||||
|
||||
with open(fpath, 'wb') as f:
|
||||
f.write(obj.raw)
|
||||
f.write(obj.raw.encode('utf-8'))
|
||||
return os.path.getmtime(fpath)
|
||||
|
||||
def delete(self, href, etag):
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
:copyright: (c) 2014 Markus Unterwaditzer
|
||||
:license: MIT, see LICENSE for more details.
|
||||
'''
|
||||
import vdirsyncer.exceptions as exceptions
|
||||
import vdirsyncer.log
|
||||
sync_logger = vdirsyncer.log.get('sync')
|
||||
|
||||
|
||||
def prepare_list(storage, href_to_uid):
|
||||
|
|
@ -49,10 +52,14 @@ def sync(storage_a, storage_b, status):
|
|||
modified by the function and should be passed to it at the next sync.
|
||||
If this is the first sync, an empty dictionary should be provided.
|
||||
'''
|
||||
a_href_to_uid = dict((href_a, uid)
|
||||
for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems())
|
||||
b_href_to_uid = dict((href_b, uid)
|
||||
for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems())
|
||||
a_href_to_uid = dict(
|
||||
(href_a, uid)
|
||||
for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems()
|
||||
)
|
||||
b_href_to_uid = dict(
|
||||
(href_b, uid)
|
||||
for uid, (href_a, etag_a, href_b, etag_b) in status.iteritems()
|
||||
)
|
||||
# href => {'etag': etag, 'obj': optional object, 'uid': uid}
|
||||
list_a = dict(prepare_list(storage_a, a_href_to_uid))
|
||||
list_b = dict(prepare_list(storage_b, b_href_to_uid))
|
||||
|
|
@ -73,6 +80,7 @@ def sync(storage_a, storage_b, status):
|
|||
}
|
||||
|
||||
for action, uid, source, dest in actions:
|
||||
sync_logger.debug((action, uid, source, dest))
|
||||
source_storage, source_list, source_uid_to_href = storages[source]
|
||||
dest_storage, dest_list, dest_uid_to_href = storages[dest]
|
||||
|
||||
|
|
@ -114,8 +122,13 @@ def get_actions(list_a, list_b, status, a_uid_to_href, b_uid_to_href):
|
|||
if uid not in status:
|
||||
if uid in uids_a and uid in uids_b: # missing status
|
||||
# TODO: might need some kind of diffing too?
|
||||
assert type(a['obj'].raw) is unicode, repr(a['obj'].raw)
|
||||
assert type(b['obj'].raw) is unicode, repr(b['obj'].raw)
|
||||
if a['obj'].raw != b['obj'].raw:
|
||||
1 / 0
|
||||
raise NotImplementedError(
|
||||
'Conflict. No status and '
|
||||
'different content on both sides.'
|
||||
)
|
||||
status[uid] = (href_a, a['etag'], href_b, b['etag'])
|
||||
# new item was created in a
|
||||
elif uid in uids_a and uid not in uids_b:
|
||||
|
|
@ -129,7 +142,9 @@ def get_actions(list_a, list_b, status, a_uid_to_href, b_uid_to_href):
|
|||
_, status_etag_a, _, status_etag_b = status[uid]
|
||||
if uid in uids_a and uid in uids_b:
|
||||
if a['etag'] != status_etag_a and b['etag'] != status_etag_b:
|
||||
1 / 0 # conflict resolution TODO
|
||||
# conflict resolution TODO
|
||||
raise NotImplementedError('Conflict. '
|
||||
'New etags on both sides.')
|
||||
elif a['etag'] != status_etag_a: # item was updated in a
|
||||
prefetch_from_a.append(href_a)
|
||||
actions.append(('update', uid, 'a', 'b'))
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import vdirsyncer.exceptions as exceptions
|
|||
class StorageTests(object):
|
||||
|
||||
def _create_bogus_item(self, uid):
|
||||
return Item('''BEGIN:VCALENDAR
|
||||
return Item(u'''BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//dmfs.org//mimedir.icalendar//EN
|
||||
BEGIN:VTODO
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class SyncTests(TestCase):
|
|||
a = MemoryStorage()
|
||||
b = MemoryStorage()
|
||||
status = {}
|
||||
item = Item('UID:1')
|
||||
item = Item(u'UID:1')
|
||||
a.upload(item)
|
||||
b.upload(item)
|
||||
sync(a, b, status)
|
||||
|
|
@ -46,8 +46,8 @@ class SyncTests(TestCase):
|
|||
a = MemoryStorage()
|
||||
b = MemoryStorage()
|
||||
status = {}
|
||||
item1 = Item('UID:1\nhaha')
|
||||
item2 = Item('UID:1\nhoho')
|
||||
item1 = Item(u'UID:1\nhaha')
|
||||
item2 = Item(u'UID:1\nhoho')
|
||||
a.upload(item1)
|
||||
b.upload(item2)
|
||||
sync(a, b, status)
|
||||
|
|
@ -59,22 +59,22 @@ class SyncTests(TestCase):
|
|||
b = MemoryStorage()
|
||||
status = {}
|
||||
|
||||
item = Item('UID:1') # new item 1 in a
|
||||
item = Item(u'UID:1') # new item 1 in a
|
||||
a.upload(item)
|
||||
sync(a, b, status)
|
||||
assert b.get('1.txt')[0].raw == item.raw
|
||||
|
||||
item = Item('UID:1\nASDF:YES') # update of item 1 in b
|
||||
item = Item(u'UID:1\nASDF:YES') # update of item 1 in b
|
||||
b.update('1.txt', item, b.get('1.txt')[1])
|
||||
sync(a, b, status)
|
||||
assert a.get('1.txt')[0].raw == item.raw
|
||||
|
||||
item2 = Item('UID:2') # new item 2 in b
|
||||
item2 = Item(u'UID:2') # new item 2 in b
|
||||
b.upload(item2)
|
||||
sync(a, b, status)
|
||||
assert a.get('2.txt')[0].raw == item2.raw
|
||||
|
||||
item2 = Item('UID:2\nASDF:YES') # update of item 2 in a
|
||||
item2 = Item(u'UID:2\nASDF:YES') # update of item 2 in a
|
||||
a.update('2.txt', item2, a.get('2.txt')[1])
|
||||
sync(a, b, status)
|
||||
assert b.get('2.txt')[0].raw == item2.raw
|
||||
|
|
@ -84,7 +84,7 @@ class SyncTests(TestCase):
|
|||
b = MemoryStorage()
|
||||
status = {}
|
||||
|
||||
item = Item('UID:1')
|
||||
item = Item(u'UID:1')
|
||||
a.upload(item)
|
||||
sync(a, b, status)
|
||||
b.delete('1.txt', b.get('1.txt')[1])
|
||||
|
|
@ -101,7 +101,7 @@ class SyncTests(TestCase):
|
|||
def test_already_synced(self):
|
||||
a = MemoryStorage()
|
||||
b = MemoryStorage()
|
||||
item = Item('UID:1')
|
||||
item = Item(u'UID:1')
|
||||
a.upload(item)
|
||||
b.upload(item)
|
||||
status = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue