From 5028d09f61d1c3281dddab73228dd625a783f6e2 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 12 Jun 2014 13:40:40 +0200 Subject: [PATCH 1/2] Add read_only parameter Just skip any updates when the storage is read-only, write to status anyway. The change will get reverted in the next sync. Fix #54 --- docs/api.rst | 4 +++ docs/changelog.rst | 3 ++ tests/test_sync.py | 24 +++++++++++++++- tests/utils/test_main.py | 2 +- vdirsyncer/storage/base.py | 9 ++++++ vdirsyncer/storage/http.py | 5 ++-- vdirsyncer/sync.py | 56 ++++++++++++++++++++++++++++---------- 7 files changed, 85 insertions(+), 18 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7b8bdb9..727e6bb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -74,6 +74,10 @@ Storage Section - ``type`` defines which kind of storage is defined. See :ref:`storages`. +- ``read_only`` defines whether the storage should be regarded as a read-only + storage, defaulting to ``False``. Setting this to ``True`` effectively means + synchronization will discard any changes made to the other side. + - Any further parameters are passed on to the storage class. .. _storages: diff --git a/docs/changelog.rst b/docs/changelog.rst index 99bc5f3..baf4edd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,9 @@ Version 0.1.6 values ``from a`` and ``from b`` for automatically discovering collections. See :ref:`pair_config`. +- The ``read_only`` parameter was added to storage sections. See + :ref:`storage_config`. + .. _`#48`: https://github.com/untitaker/vdirsyncer/issues/48 Version 0.1.5 diff --git a/tests/test_sync.py b/tests/test_sync.py index 41d0a03..df41ccc 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -12,7 +12,7 @@ import pytest from . import assert_item_equals, normalize_item from vdirsyncer.storage.base import Item from vdirsyncer.storage.memory import MemoryStorage -from vdirsyncer.sync import sync, SyncConflict, StorageEmpty +from vdirsyncer.sync import sync, SyncConflict, StorageEmpty, BothReadOnly def empty_storage(x): @@ -221,3 +221,25 @@ def test_no_uids(): b_items = set(b.get(href)[0].raw for href, etag in b.list()) assert a_items == b_items == {u'ASDF', u'FOOBAR'} + + +def test_both_readonly(): + a = MemoryStorage(read_only=True) + b = MemoryStorage(read_only=True) + assert a.read_only + assert b.read_only + status = {} + with pytest.raises(BothReadOnly): + sync(a, b, status) + + +def test_readonly(): + a = MemoryStorage() + b = MemoryStorage(read_only=True) + status = {} + href_a, _ = a.upload(Item(u'UID:1')) + href_b, _ = b.upload(Item(u'UID:2')) + sync(a, b, status) + assert len(status) == 2 and a.has(href_a) and not b.has(href_a) + sync(a, b, status) + assert len(status) == 1 and not a.has(href_a) and not b.has(href_a) diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index 4785ebf..ca2672e 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -122,5 +122,5 @@ def test_get_class_init_args_on_storage(): from vdirsyncer.storage.memory import MemoryStorage all, required = utils.get_class_init_args(MemoryStorage) - assert not all + assert all == set(['read_only']) assert not required diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index 653a4d4..2232e9f 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -36,8 +36,17 @@ class Storage(object): ''' fileext = '.txt' storage_name = None # the name used in the config file + read_only = None _repr_attributes = () + def __init__(self, read_only=None): + if read_only is None: + read_only = self.read_only + if self.read_only is not None and read_only != self.read_only: + raise ValueError('read_only must be {}' + .format(repr(self.read_only))) + self.read_only = bool(read_only) + @classmethod def discover(cls, **kwargs): ''' diff --git a/vdirsyncer/storage/http.py b/vdirsyncer/storage/http.py index 63a65b7..a3edb52 100644 --- a/vdirsyncer/storage/http.py +++ b/vdirsyncer/storage/http.py @@ -52,8 +52,8 @@ class HttpStorage(Storage): .. note:: This is a read-only storage. If you sync this with - read-and-write-storages (such as CalDAV), make sure not to change - anything on the other side, otherwise vdirsyncer will crash. + read-and-write-storages (such as CalDAV), any changes on the other side + will get reverted. :param url: URL to the ``.ics`` file. :param username: Username for authentication. @@ -82,6 +82,7 @@ class HttpStorage(Storage): ''' storage_name = 'http' + read_only = True _repr_attributes = ('username', 'url') _items = None diff --git a/vdirsyncer/sync.py b/vdirsyncer/sync.py index 2d03549..5b48a3b 100644 --- a/vdirsyncer/sync.py +++ b/vdirsyncer/sync.py @@ -44,6 +44,13 @@ class StorageEmpty(SyncError): ''' +class BothReadOnly(SyncError): + ''' + Both storages are marked as read-only. Synchronization is therefore not + possible. + ''' + + def prepare_list(storage, href_to_status): rv = {} download = [] @@ -88,6 +95,8 @@ def sync(storage_a, storage_b, status, conflict_resolution=None, safety. Setting this parameter to ``True`` disables this safety measure. ''' + if False not in (storage_a.read_only, storage_b.read_only): + raise BothReadOnly() a_href_to_status = dict( (href_a, (ident, etag_a)) for ident, (href_a, etag_a, href_b, etag_b) in iteritems(status) @@ -127,12 +136,18 @@ def action_upload(ident, source, dest): source_href = source_ident_to_href[ident] source_etag = source_list[source_href]['etag'] - - item = source_list[source_href]['item'] - dest_href, dest_etag = dest_storage.upload(item) - source_status = (source_href, source_etag) - dest_status = (dest_href, dest_etag) + + dest_status = (None, None) + + if dest_storage.read_only: + sync_logger.warning('{dest} is read-only. Skipping update...' + .format(dest=dest_storage)) + else: + item = source_list[source_href]['item'] + dest_href, dest_etag = dest_storage.upload(item) + dest_status = (dest_href, dest_etag) + status[ident] = source_status + dest_status if source == 'a' else \ dest_status + source_status @@ -145,17 +160,25 @@ def action_update(ident, source, dest): dest_storage, dest_list, dest_ident_to_href = storages[dest] sync_logger.info('Copying (updating) item {} to {}' .format(ident, dest_storage)) + source_href = source_ident_to_href[ident] source_etag = source_list[source_href]['etag'] + source_status = (source_href, source_etag) dest_href = dest_ident_to_href[ident] - old_etag = dest_list[dest_href]['etag'] - item = source_list[source_href]['item'] - dest_etag = dest_storage.update(dest_href, item, old_etag) - assert isinstance(dest_etag, (bytes, text_type)) - - source_status = (source_href, source_etag) + dest_etag = dest_list[dest_href]['etag'] dest_status = (dest_href, dest_etag) + + if dest_storage.read_only: + sync_logger.info('{dest} is read-only. Skipping update...' + .format(dest=dest_storage)) + else: + item = source_list[source_href]['item'] + dest_etag = dest_storage.update(dest_href, item, dest_etag) + assert isinstance(dest_etag, (bytes, text_type)) + + dest_status = (dest_href, dest_etag) + status[ident] = source_status + dest_status if source == 'a' else \ dest_status + source_status @@ -168,12 +191,17 @@ def action_delete(ident, dest): dest_storage, dest_list, dest_ident_to_href = storages[dest] sync_logger.info('Deleting item {} from {}' .format(ident, dest_storage)) - dest_href = dest_ident_to_href[ident] - dest_etag = dest_list[dest_href]['etag'] - dest_storage.delete(dest_href, dest_etag) + if dest_storage.read_only: + sync_logger.warning('{dest} is read-only, skipping deletion...' + .format(dest=dest_storage)) + else: + dest_href = dest_ident_to_href[ident] + dest_etag = dest_list[dest_href]['etag'] + dest_storage.delete(dest_href, dest_etag) else: sync_logger.info('Deleting status info for nonexisting item {}' .format(ident)) + del status[ident] return inner From fc9ca4177fc8bd5ef64d228f230effdab47f2d9a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 12 Jun 2014 14:31:32 +0200 Subject: [PATCH 2/2] Add a test for HttpStorage.read_only --- tests/storage/test_http.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/storage/test_http.py b/tests/storage/test_http.py index fdc1369..845258b 100644 --- a/tests/storage/test_http.py +++ b/tests/storage/test_http.py @@ -7,6 +7,8 @@ :license: MIT, see LICENSE for more details. ''' +import pytest + from requests import Response from tests import normalize_item @@ -71,3 +73,13 @@ def test_list(monkeypatch): assert item.uid is None assert etag2 == etag assert found_items[normalize_item(item)] == href + + +def test_readonly_param(): + url = u'http://example.com/' + with pytest.raises(ValueError): + HttpStorage(url=url, read_only=False) + + a = HttpStorage(url=url, read_only=True).read_only + b = HttpStorage(url=url, read_only=None).read_only + assert a is b is True