Merge pull request #77 from untitaker/readonly

Add read_only parameter
This commit is contained in:
Markus Unterwaditzer 2014-06-12 15:46:57 +02:00
commit 5788544839
8 changed files with 97 additions and 18 deletions

View file

@ -74,6 +74,10 @@ Storage Section
- ``type`` defines which kind of storage is defined. See :ref:`storages`. - ``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. - Any further parameters are passed on to the storage class.
.. _storages: .. _storages:

View file

@ -27,6 +27,9 @@ Version 0.1.6
values ``from a`` and ``from b`` for automatically discovering collections. values ``from a`` and ``from b`` for automatically discovering collections.
See :ref:`pair_config`. 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 .. _`#48`: https://github.com/untitaker/vdirsyncer/issues/48
Version 0.1.5 Version 0.1.5

View file

@ -7,6 +7,8 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
''' '''
import pytest
from requests import Response from requests import Response
from tests import normalize_item from tests import normalize_item
@ -71,3 +73,13 @@ def test_list(monkeypatch):
assert item.uid is None assert item.uid is None
assert etag2 == etag assert etag2 == etag
assert found_items[normalize_item(item)] == href 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

View file

@ -12,7 +12,7 @@ import pytest
from . import assert_item_equals, normalize_item from . import assert_item_equals, normalize_item
from vdirsyncer.storage.base import Item from vdirsyncer.storage.base import Item
from vdirsyncer.storage.memory import MemoryStorage 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): 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()) b_items = set(b.get(href)[0].raw for href, etag in b.list())
assert a_items == b_items == {u'ASDF', u'FOOBAR'} 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)

View file

@ -122,5 +122,5 @@ def test_get_class_init_args_on_storage():
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage
all, required = utils.get_class_init_args(MemoryStorage) all, required = utils.get_class_init_args(MemoryStorage)
assert not all assert all == set(['read_only'])
assert not required assert not required

View file

@ -36,8 +36,17 @@ class Storage(object):
''' '''
fileext = '.txt' fileext = '.txt'
storage_name = None # the name used in the config file storage_name = None # the name used in the config file
read_only = None
_repr_attributes = () _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 @classmethod
def discover(cls, **kwargs): def discover(cls, **kwargs):
''' '''

View file

@ -52,8 +52,8 @@ class HttpStorage(Storage):
.. note:: .. note::
This is a read-only storage. If you sync this with This is a read-only storage. If you sync this with
read-and-write-storages (such as CalDAV), make sure not to change read-and-write-storages (such as CalDAV), any changes on the other side
anything on the other side, otherwise vdirsyncer will crash. will get reverted.
:param url: URL to the ``.ics`` file. :param url: URL to the ``.ics`` file.
:param username: Username for authentication. :param username: Username for authentication.
@ -82,6 +82,7 @@ class HttpStorage(Storage):
''' '''
storage_name = 'http' storage_name = 'http'
read_only = True
_repr_attributes = ('username', 'url') _repr_attributes = ('username', 'url')
_items = None _items = None

View file

@ -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): def prepare_list(storage, href_to_status):
rv = {} rv = {}
download = [] download = []
@ -88,6 +95,8 @@ def sync(storage_a, storage_b, status, conflict_resolution=None,
safety. Setting this parameter to ``True`` disables this safety safety. Setting this parameter to ``True`` disables this safety
measure. measure.
''' '''
if False not in (storage_a.read_only, storage_b.read_only):
raise BothReadOnly()
a_href_to_status = dict( a_href_to_status = dict(
(href_a, (ident, etag_a)) (href_a, (ident, etag_a))
for ident, (href_a, etag_a, href_b, etag_b) in iteritems(status) 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_href = source_ident_to_href[ident]
source_etag = source_list[source_href]['etag'] 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) 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 \ status[ident] = source_status + dest_status if source == 'a' else \
dest_status + source_status dest_status + source_status
@ -145,17 +160,25 @@ def action_update(ident, source, dest):
dest_storage, dest_list, dest_ident_to_href = storages[dest] dest_storage, dest_list, dest_ident_to_href = storages[dest]
sync_logger.info('Copying (updating) item {} to {}' sync_logger.info('Copying (updating) item {} to {}'
.format(ident, dest_storage)) .format(ident, dest_storage))
source_href = source_ident_to_href[ident] source_href = source_ident_to_href[ident]
source_etag = source_list[source_href]['etag'] source_etag = source_list[source_href]['etag']
source_status = (source_href, source_etag)
dest_href = dest_ident_to_href[ident] dest_href = dest_ident_to_href[ident]
old_etag = dest_list[dest_href]['etag'] dest_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_status = (dest_href, dest_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 \ status[ident] = source_status + dest_status if source == 'a' else \
dest_status + source_status dest_status + source_status
@ -168,12 +191,17 @@ def action_delete(ident, dest):
dest_storage, dest_list, dest_ident_to_href = storages[dest] dest_storage, dest_list, dest_ident_to_href = storages[dest]
sync_logger.info('Deleting item {} from {}' sync_logger.info('Deleting item {} from {}'
.format(ident, dest_storage)) .format(ident, dest_storage))
dest_href = dest_ident_to_href[ident] if dest_storage.read_only:
dest_etag = dest_list[dest_href]['etag'] sync_logger.warning('{dest} is read-only, skipping deletion...'
dest_storage.delete(dest_href, dest_etag) .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: else:
sync_logger.info('Deleting status info for nonexisting item {}' sync_logger.info('Deleting status info for nonexisting item {}'
.format(ident)) .format(ident))
del status[ident] del status[ident]
return inner return inner