Skip to content

Commit

Permalink
Add read_only parameter
Browse files Browse the repository at this point in the history
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
  • Loading branch information
untitaker committed Jun 12, 2014
1 parent 967540f commit 5028d09
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 17 deletions.
4 changes: 4 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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):
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/utils/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions vdirsyncer/storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
'''
Expand Down
5 changes: 3 additions & 2 deletions vdirsyncer/storage/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -82,6 +82,7 @@ class HttpStorage(Storage):
'''

storage_name = 'http'
read_only = True
_repr_attributes = ('username', 'url')
_items = None

Expand Down
54 changes: 41 additions & 13 deletions vdirsyncer/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -127,12 +136,18 @@ def inner(storages, status, conflict_resolution):

source_href = source_ident_to_href[ident]
source_etag = source_list[source_href]['etag']
source_status = (source_href, source_etag)

item = source_list[source_href]['item']
dest_href, dest_etag = dest_storage.upload(item)
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)

source_status = (source_href, source_etag)
dest_status = (dest_href, dest_etag)
status[ident] = source_status + dest_status if source == 'a' else \
dest_status + source_status

Expand All @@ -145,17 +160,25 @@ def inner(storages, status, conflict_resolution):
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

Expand All @@ -168,12 +191,17 @@ def inner(storages, status, conflict_resolution):
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
Expand Down

0 comments on commit 5028d09

Please sign in to comment.