mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
First working sync
This commit is contained in:
parent
5f1b0d190f
commit
cc362e7e8f
5 changed files with 110 additions and 29 deletions
|
|
@ -42,7 +42,7 @@ class Storage(object):
|
||||||
'''
|
'''
|
||||||
Upload a new object, raise
|
Upload a new object, raise
|
||||||
:exc:`vdirsyncer.exceptions.AlreadyExistingError` if it already exists.
|
:exc:`vdirsyncer.exceptions.AlreadyExistingError` if it already exists.
|
||||||
:returns: (uid, etag)
|
:returns: etag on the server
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
@ -55,3 +55,9 @@ class Storage(object):
|
||||||
:returns: etag on the server
|
:returns: etag on the server
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def delete(self, uid):
|
||||||
|
'''
|
||||||
|
Delete the object, raise exceptions on error, no return value
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class FilesystemStorage(Storage):
|
||||||
raise exceptions.AlreadyExistingError(obj.uid)
|
raise exceptions.AlreadyExistingError(obj.uid)
|
||||||
with open(fpath, 'wb+') as f:
|
with open(fpath, 'wb+') as f:
|
||||||
f.write(obj.raw)
|
f.write(obj.raw)
|
||||||
return obj.uid, os.path.getmtime(fpath)
|
return os.path.getmtime(fpath)
|
||||||
|
|
||||||
def update(self, obj, etag):
|
def update(self, obj, etag):
|
||||||
fpath = self._get_filepath(obj)
|
fpath = self._get_filepath(obj)
|
||||||
|
|
@ -44,3 +44,6 @@ class FilesystemStorage(Storage):
|
||||||
with open(fpath, 'wb') as f:
|
with open(fpath, 'wb') as f:
|
||||||
f.write(obj.raw)
|
f.write(obj.raw)
|
||||||
return os.path.getmtime(fpath)
|
return os.path.getmtime(fpath)
|
||||||
|
|
||||||
|
def delete(self, uid):
|
||||||
|
os.remove(self._get_filepath(uid))
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class MemoryStorage(Storage):
|
||||||
def get_items(self, uids):
|
def get_items(self, uids):
|
||||||
for uid in uids:
|
for uid in uids:
|
||||||
etag, obj = self.items[uid]
|
etag, obj = self.items[uid]
|
||||||
return obj, uid, etag
|
yield obj, uid, etag
|
||||||
|
|
||||||
def item_exists(self, uid):
|
def item_exists(self, uid):
|
||||||
return uid in self.items
|
return uid in self.items
|
||||||
|
|
@ -24,11 +24,14 @@ class MemoryStorage(Storage):
|
||||||
raise exceptions.AlreadyExistingError(obj)
|
raise exceptions.AlreadyExistingError(obj)
|
||||||
etag = datetime.datetime.now()
|
etag = datetime.datetime.now()
|
||||||
self.items[obj.uid] = (etag, obj)
|
self.items[obj.uid] = (etag, obj)
|
||||||
return obj.uid, etag
|
return etag
|
||||||
|
|
||||||
def update(self, obj, etag):
|
def update(self, obj, etag):
|
||||||
if obj.uid not in self.items:
|
if obj.uid not in self.items:
|
||||||
raise exceptions.NotFoundError(obj)
|
raise exceptions.NotFoundError(obj)
|
||||||
etag = datetime.datetime.now()
|
etag = datetime.datetime.now()
|
||||||
self.items[obj.uid] = (etag, obj)
|
self.items[obj.uid] = (etag, obj)
|
||||||
return obj.uid, etag
|
return etag
|
||||||
|
|
||||||
|
def delete(self, uid):
|
||||||
|
del self.items[uid]
|
||||||
|
|
|
||||||
|
|
@ -5,32 +5,58 @@ def sync(storage_a, storage_b, status):
|
||||||
:param storage_b: The second storage
|
:param storage_b: The second storage
|
||||||
:param status: {uid: (etag_a, etag_b)}
|
:param status: {uid: (etag_a, etag_b)}
|
||||||
'''
|
'''
|
||||||
items_a = dict(storage_a.list_items())
|
list_a = dict(storage_a.list_items())
|
||||||
items_b = dict(storage_b.list_items())
|
list_b = dict(storage_b.list_items())
|
||||||
downloads_a = set() # uids which to copy from a to b
|
|
||||||
downloads_b = set() # uids which to copy from b to a
|
|
||||||
deletes_a = set()
|
|
||||||
deletes_b = set()
|
|
||||||
|
|
||||||
for uid in set(items_a) + set(items_b):
|
prefetch_items_from_a = []
|
||||||
|
prefetch_items_from_b = []
|
||||||
|
actions = [] # list(tuple(action, uid, source, dest))
|
||||||
|
|
||||||
|
for uid in set(list_a).union(set(list_b)):
|
||||||
if uid not in status:
|
if uid not in status:
|
||||||
if uid in items_a and uid in items_b: # missing status
|
if uid in list_a and uid in list_b: # missing status
|
||||||
status[uid] = (items_a[uid], items_b[uid]) # TODO: might need etag diffing too?
|
status[uid] = (list_a[uid], list_b[uid]) # TODO: might need etag diffing too?
|
||||||
elif uid in items_a and uid not in items_b: # new item in a
|
elif uid in list_a and uid not in list_b: # new item in a
|
||||||
downloads_a.add(uid)
|
prefetch_items_from_a.append(uid)
|
||||||
elif uid not in items_a and uid in items_b: # new item in b
|
actions.append(('upload', uid, 'a', 'b'))
|
||||||
downloads_b.add(uid)
|
elif uid not in list_a and uid in list_b: # new item in b
|
||||||
|
prefetch_items_from_b.append(uid)
|
||||||
|
actions.append(('upload', uid, 'b', 'a'))
|
||||||
else:
|
else:
|
||||||
if uid in items_a and uid in items_b:
|
if uid in list_a and uid in list_b:
|
||||||
if items_a[uid] != status[uid][0] and items_a[uid] != status[uid][1]:
|
if list_a[uid] != status[uid][0] and list_b[uid] != status[uid][1]:
|
||||||
1/0 # conflict resolution
|
1/0 # conflict resolution TODO
|
||||||
elif items_a[uid] != status[uid][0]: # item update in a
|
elif list_a[uid] != status[uid][0]: # item update in a
|
||||||
downloads_a.add(uid)
|
prefetch_items_from_a.append(uid)
|
||||||
elif items_b[uid] != status[uid][1]: # item update in b
|
actions.append(('update', uid, 'a', 'b'))
|
||||||
downloads_b.add(uid)
|
elif list_b[uid] != status[uid][1]: # item update in b
|
||||||
|
prefetch_items_from_b.append(uid)
|
||||||
|
actions.append(('update', uid, 'b', 'a'))
|
||||||
else: # completely in sync!
|
else: # completely in sync!
|
||||||
pass
|
pass
|
||||||
elif uid in items_a and uid not in items_b: # deleted from b
|
elif uid in list_a and uid not in list_b: # deleted from b
|
||||||
deletes_a.add(uid)
|
actions.append(('delete', uid, 'b', 'a'))
|
||||||
elif uid not in items_a and uid in items_b: # deleted from a
|
elif uid not in list_a and uid in list_b: # deleted from a
|
||||||
deletes_b.add(uid)
|
actions.append(('delete', uid, 'a', 'b'))
|
||||||
|
|
||||||
|
items_a = {}
|
||||||
|
items_b = {}
|
||||||
|
for item, uid, etag in storage_a.get_items(prefetch_items_from_a):
|
||||||
|
items_a[uid] = (item, etag)
|
||||||
|
for item, uid, etag in storage_b.get_items(prefetch_items_from_b):
|
||||||
|
items_b[uid] = (item, etag)
|
||||||
|
|
||||||
|
for action, uid, source, dest in actions:
|
||||||
|
source_storage = storage_a if source == 'a' else storage_b
|
||||||
|
dest_storage = storage_a if dest == 'a' else storage_b
|
||||||
|
source_items = items_a if source == 'a' else items_b
|
||||||
|
if action in ('upload', 'update'):
|
||||||
|
item, source_etag = source_items[uid]
|
||||||
|
if action == 'upload':
|
||||||
|
dest_etag = dest_storage.upload(item)
|
||||||
|
else:
|
||||||
|
dest_etag = dest_storage.update(item, etag)
|
||||||
|
status[uid] = (source_etag, dest_etag) if source == 'a' else (dest_etag, source_etag)
|
||||||
|
elif action == 'delete':
|
||||||
|
dest_storage.delete(uid)
|
||||||
|
del status[uid]
|
||||||
|
|
|
||||||
43
vdirsyncer/tests/test_sync.py
Normal file
43
vdirsyncer/tests/test_sync.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from unittest import TestCase
|
||||||
|
from vdirsyncer.storage.base import Item
|
||||||
|
from vdirsyncer.storage.memory import MemoryStorage
|
||||||
|
from vdirsyncer.sync import sync
|
||||||
|
import vdirsyncer.exceptions as exceptions
|
||||||
|
|
||||||
|
class SyncTests(TestCase):
|
||||||
|
def test_basic(self):
|
||||||
|
a = MemoryStorage()
|
||||||
|
b = MemoryStorage()
|
||||||
|
status = {}
|
||||||
|
sync(a, b, status)
|
||||||
|
assert len(status) == 0
|
||||||
|
assert list(a.list_items()) == []
|
||||||
|
assert list(b.list_items()) == []
|
||||||
|
|
||||||
|
item = Item('UID:1')
|
||||||
|
a.upload(item)
|
||||||
|
sync(a, b, status)
|
||||||
|
assert list(status) == ['1']
|
||||||
|
obj, uid, etag = next(b.get_items(['1']))
|
||||||
|
assert obj.raw == item.raw
|
||||||
|
|
||||||
|
item2 = Item('UID:2')
|
||||||
|
b.upload(item2)
|
||||||
|
b.delete('1')
|
||||||
|
sync(a, b, status)
|
||||||
|
assert list(status) == ['2']
|
||||||
|
assert next(a.list_items())[0] == '2'
|
||||||
|
assert next(b.list_items())[0] == '2'
|
||||||
|
obj, uid, etag = next(a.get_items(['2']))
|
||||||
|
assert obj.raw == item2.raw
|
||||||
|
|
||||||
|
new_item2 = Item('UID:2\nHUEHUEHUE:PRECISELY')
|
||||||
|
old_status = status.copy()
|
||||||
|
a.update(new_item2, next(a.list_items())[1])
|
||||||
|
sync(a, b, status)
|
||||||
|
assert status != old_status
|
||||||
|
assert list(status) == list(old_status)
|
||||||
|
assert next(a.list_items())[0] == '2'
|
||||||
|
assert next(b.list_items())[0] == '2'
|
||||||
|
obj, uid, etag = next(b.get_items(['2']))
|
||||||
|
assert obj.raw == new_item2.raw
|
||||||
Loading…
Reference in a new issue