This commit is contained in:
Markus Unterwaditzer 2014-05-01 21:55:34 +02:00
parent 9c45a7852a
commit d906aa3df6
4 changed files with 59 additions and 5 deletions

View file

@ -91,6 +91,7 @@ def test_deletion():
item = Item(u'UID:1')
a.upload(item)
a.upload(Item(u'UID:2'))
sync(a, b, status)
b.delete('1.txt', b.get('1.txt')[1])
sync(a, b, status)
@ -192,3 +193,17 @@ def test_uses_get_multi(monkeypatch):
sync(a, b, {})
assert get_multi_calls == [[expected_href]]
def test_empty_storage_dataloss():
a = MemoryStorage()
b = MemoryStorage()
a.upload(Item(u'UID:1'))
a.upload(Item(u'UID:2'))
status = {}
sync(a, b, status)
with pytest.raises(exceptions.StorageEmpty):
sync(MemoryStorage(), b, status)
with pytest.raises(exceptions.StorageEmpty):
sync(a, MemoryStorage(), status)

View file

@ -14,6 +14,7 @@ from vdirsyncer.sync import sync
from vdirsyncer.utils import expand_path, split_dict, parse_options
from vdirsyncer.storage import storage_names
import vdirsyncer.log as log
import vdirsyncer.exceptions as exceptions
import argvard
@ -196,6 +197,11 @@ def _main(env, file_cfg):
sync_command = argvard.Command()
@sync_command.option('--force-delete status_name')
def force_delete(context, status_name):
'''Pretty please delete all my data.'''
context.setdefault('force_delete', set()).add(status_name)
@sync_command.main('[pairs...]')
def sync_main(context, pairs=None):
'''
@ -209,6 +215,7 @@ def _main(env, file_cfg):
from the pair "bob".
'''
actions = []
force_delete = context.get('force_delete', set())
for pair_name, collection in parse_pairs_args(pairs, all_pairs):
a_name, b_name, pair_options, storage_defaults = \
all_pairs[pair_name]
@ -225,7 +232,8 @@ def _main(env, file_cfg):
'pair_name': pair_name,
'collection': collection,
'pair_options': pair_options,
'general': general
'general': general,
'force_delete': force_delete
})
processes = general.get('processes', 0) or len(actions)
@ -249,7 +257,7 @@ def _sync_collection(x):
def sync_collection(config_a, config_b, pair_name, collection, pair_options,
general):
general, force_delete):
status_name = '_'.join(filter(bool, (pair_name, collection)))
pair_description = ' from '.join(filter(bool, (collection, pair_name)))
@ -258,6 +266,20 @@ def sync_collection(config_a, config_b, pair_name, collection, pair_options,
cli_logger.info('Syncing {}'.format(pair_description))
status = load_status(general['status_path'], status_name)
sync(a, b, status,
pair_options.get('conflict_resolution', None))
try:
sync(
a, b, status,
conflict_resolution=pair_options.get('conflict_resolution', None),
force_delete=status_name in force_delete
)
except exceptions.StorageEmpty as e:
side = 'a' if e.empty_storage is a else 'b'
storage = e.empty_storage
cli_logger.error('{pair_description}: Storage "{side}" ({storage}) '
'was completely emptied. Use "--force-delete '
'{status_name}" to synchronize that emptyness to '
'the other side, or delete the status by yourself to '
'restore the empty side from the other one.'
.format(**locals()))
sys.exit(1)
save_status(general['status_path'], status_name, status)

View file

@ -51,3 +51,12 @@ class SyncError(Error):
class SyncConflict(SyncError):
pass
class StorageEmpty(SyncError):
'''One storage unexpectedly got completely empty between two
synchronizations. The first argument is the empty storage.'''
@property
def empty_storage(self):
return self.args[0]

View file

@ -42,7 +42,8 @@ def prepare_list(storage, href_to_status):
yield href, props
def sync(storage_a, storage_b, status, conflict_resolution=None):
def sync(storage_a, storage_b, status, conflict_resolution=None,
force_delete=False):
'''Syncronizes two storages.
:param storage_a: The first storage
@ -56,6 +57,10 @@ def sync(storage_a, storage_b, status, conflict_resolution=None):
:param conflict_resolution: Either 'a wins' or 'b wins'. If none is
provided, the sync function will raise
:py:exc:`vdirsyncer.exceptions.SyncConflict`.
:param force_delete: When one storage got completely emptied between two
syncs, :py:exc:`vdirsyncer.exceptions.StorageEmpty` is raised for
safety. Setting this parameter to ``True`` disables this safety
measure.
'''
a_href_to_status = dict(
(href_a, (uid, etag_a))
@ -69,6 +74,9 @@ def sync(storage_a, storage_b, status, conflict_resolution=None):
list_a = dict(prepare_list(storage_a, a_href_to_status))
list_b = dict(prepare_list(storage_b, b_href_to_status))
if bool(list_a) != bool(list_b) and status and not force_delete:
raise exceptions.StorageEmpty(storage_b if list_a else storage_a)
a_uid_to_href = dict((x['uid'], href) for href, x in iteritems(list_a))
b_uid_to_href = dict((x['uid'], href) for href, x in iteritems(list_b))
del a_href_to_status, b_href_to_status