mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-11 11:46:02 +00:00
Fix #42
This commit is contained in:
parent
9c45a7852a
commit
d906aa3df6
4 changed files with 59 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue