Skip to content

Commit 7b49341

Browse files
author
Hugo Osvaldo Barrera
authored
Merge pull request pimutils#920 from pimutils/meta_delete
metasync: use None as no-value and delete missing values on syncing
2 parents 5b8f00e + 7379a96 commit 7b49341

File tree

7 files changed

+63
-29
lines changed

7 files changed

+63
-29
lines changed

tests/storage/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,20 +312,29 @@ async def test_specialchars(
312312
if self.storage_class.storage_name.endswith("dav"):
313313
assert urlquote(uid, "/@:") in href
314314

315+
@pytest.mark.asyncio
316+
async def test_empty_metadata(self, requires_metadata, s):
317+
if getattr(self, "dav_server", ""):
318+
pytest.skip()
319+
320+
assert await s.get_meta("color") is None
321+
assert await s.get_meta("displayname") is None
322+
315323
@pytest.mark.asyncio
316324
async def test_metadata(self, requires_metadata, s):
317-
if not getattr(self, "dav_server", ""):
318-
assert not await s.get_meta("color")
319-
assert not await s.get_meta("displayname")
325+
if getattr(self, "dav_server", "") == "xandikos":
326+
pytest.skip("xandikos does not support removing metadata.")
320327

321328
try:
322329
await s.set_meta("color", None)
323-
assert not await s.get_meta("color")
330+
assert await s.get_meta("color") is None
324331
await s.set_meta("color", "#ff0000")
325332
assert await s.get_meta("color") == "#ff0000"
326333
except exceptions.UnsupportedMetadataError:
327334
pass
328335

336+
@pytest.mark.asyncio
337+
async def test_encoding_metadata(self, requires_metadata, s):
329338
for x in ("hello world", "hello wörld"):
330339
await s.set_meta("displayname", x)
331340
rv = await s.get_meta("displayname")

tests/unit/test_metasync.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ async def test_basic(monkeypatch):
2828
b = MemoryStorage()
2929
status = {}
3030

31+
await a.set_meta("foo", None)
32+
await metasync(a, b, status, keys=["foo"])
33+
assert await a.get_meta("foo") is None and await b.get_meta("foo") is None
34+
3135
await a.set_meta("foo", "bar")
3236
await metasync(a, b, status, keys=["foo"])
3337
assert await a.get_meta("foo") == await b.get_meta("foo") == "bar"
@@ -183,7 +187,7 @@ def _get_storage(m, instance_name):
183187
await metasync(a, b, status, keys=keys, conflict_resolution=conflict_resolution)
184188

185189
for key in keys:
186-
s = status.get(key, "")
190+
s = status.get(key)
187191
assert await a.get_meta(key) == await b.get_meta(key) == s
188-
if expected_values.get(key, "") and s:
192+
if expected_values.get(key) and s:
189193
assert s == expected_values[key]

vdirsyncer/metasync.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@ class MetaSyncConflict(MetaSyncError):
1414
key = None
1515

1616

17+
def status_set_key(status, key, value):
18+
if value is None:
19+
status.pop(key, None)
20+
else:
21+
status[key] = value
22+
23+
1724
async def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
1825
async def _a_to_b():
1926
logger.info(f"Copying {key} to {storage_b}")
2027
await storage_b.set_meta(key, a)
21-
status[key] = a
28+
status_set_key(status, key, a)
2229

2330
async def _b_to_a():
2431
logger.info(f"Copying {key} to {storage_a}")
2532
await storage_a.set_meta(key, b)
26-
status[key] = b
33+
status_set_key(status, key, b)
2734

2835
async def _resolve_conflict():
2936
if a == b:
30-
status[key] = a
37+
status_set_key(status, key, a)
3138
elif conflict_resolution == "a wins":
3239
await _a_to_b()
3340
elif conflict_resolution == "b wins":

vdirsyncer/storage/base.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import functools
3+
from typing import Optional
34

45
from .. import exceptions
56
from ..utils import uniq
@@ -219,31 +220,29 @@ async def at_once(self):
219220
"""
220221
yield
221222

222-
async def get_meta(self, key):
223+
async def get_meta(self, key: str) -> Optional[str]:
223224
"""Get metadata value for collection/storage.
224225
225226
See the vdir specification for the keys that *have* to be accepted.
226227
227228
:param key: The metadata key.
228-
:type key: unicode
229+
:return: The metadata or None, if metadata is missing.
229230
"""
230231

231232
raise NotImplementedError("This storage does not support metadata.")
232233

233-
async def set_meta(self, key, value):
234+
async def set_meta(self, key: str, value: Optional[str]):
234235
"""Get metadata value for collection/storage.
235236
236237
:param key: The metadata key.
237-
:type key: unicode
238-
:param value: The value.
239-
:type value: unicode
238+
:param value: The value. Use None to delete the data.
240239
"""
241240

242241
raise NotImplementedError("This storage does not support metadata.")
243242

244243

245-
def normalize_meta_value(value):
244+
def normalize_meta_value(value) -> Optional[str]:
246245
# `None` is returned by iCloud for empty properties.
247-
if not value or value == "None":
248-
value = ""
249-
return value.strip()
246+
if value is None or value == "None":
247+
return
248+
return value.strip() if value else ""

vdirsyncer/storage/dav.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import xml.etree.ElementTree as etree
55
from inspect import getfullargspec
66
from inspect import signature
7+
from typing import Optional
78

89
import aiohttp
910
import aiostream
@@ -679,7 +680,7 @@ async def list(self):
679680
for href, etag, _prop in rv:
680681
yield href, etag
681682

682-
async def get_meta(self, key):
683+
async def get_meta(self, key) -> Optional[str]:
683684
try:
684685
tagname, namespace = self._property_table[key]
685686
except KeyError:
@@ -714,7 +715,7 @@ async def get_meta(self, key):
714715
text = normalize_meta_value(getattr(prop, "text", None))
715716
if text:
716717
return text
717-
return ""
718+
return None
718719

719720
async def set_meta(self, key, value):
720721
try:
@@ -724,18 +725,23 @@ async def set_meta(self, key, value):
724725

725726
lxml_selector = f"{{{namespace}}}{tagname}"
726727
element = etree.Element(lxml_selector)
727-
element.text = normalize_meta_value(value)
728+
if value is None:
729+
action = "remove"
730+
else:
731+
element.text = normalize_meta_value(value)
732+
action = "set"
728733

729734
data = """<?xml version="1.0" encoding="utf-8" ?>
730735
<propertyupdate xmlns="DAV:">
731-
<set>
736+
<{action}>
732737
<prop>
733738
{}
734739
</prop>
735-
</set>
740+
</{action}>
736741
</propertyupdate>
737742
""".format(
738-
etree.tostring(element, encoding="unicode")
743+
etree.tostring(element, encoding="unicode"),
744+
action=action,
739745
).encode(
740746
"utf-8"
741747
)

vdirsyncer/storage/filesystem.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,19 @@ async def get_meta(self, key):
183183
return normalize_meta_value(f.read().decode(self.encoding))
184184
except OSError as e:
185185
if e.errno == errno.ENOENT:
186-
return ""
186+
return None
187187
else:
188188
raise
189189

190190
async def set_meta(self, key, value):
191191
value = normalize_meta_value(value)
192192

193193
fpath = os.path.join(self.path, key)
194-
with atomic_write(fpath, mode="wb", overwrite=True) as f:
195-
f.write(value.encode(self.encoding))
194+
if value is None:
195+
try:
196+
os.remove(fpath)
197+
except OSError:
198+
pass
199+
else:
200+
with atomic_write(fpath, mode="wb", overwrite=True) as f:
201+
f.write(value.encode(self.encoding))

vdirsyncer/storage/memory.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,7 @@ async def get_meta(self, key):
6969
return normalize_meta_value(self.metadata.get(key))
7070

7171
async def set_meta(self, key, value):
72-
self.metadata[key] = normalize_meta_value(value)
72+
if value is None:
73+
self.metadata.pop(key, None)
74+
else:
75+
self.metadata[key] = normalize_meta_value(value)

0 commit comments

Comments
 (0)