mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
commit
5788544839
8 changed files with 97 additions and 18 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue