Browse Source

only write to disk when creating or updating item

Fabian Peter Hammerle 4 years ago
parent
commit
6a24a4759c
3 changed files with 29 additions and 30 deletions
  1. 1 0
      CHANGELOG.md
  2. 20 22
      ical2vdir/__init__.py
  3. 8 8
      tests/vdir_test.py

+ 1 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [0.1.1] - 2020-02-06
 ### Fixed
+- only write to disk when creating or updating item
 - cleanup temporary files
 
 ## [0.1.0] - 2020-02-06

+ 20 - 22
ical2vdir/__init__.py

@@ -16,14 +16,12 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import argparse
-import contextlib
 import datetime
 import logging
 import os
 import pathlib
 import sys
 import tempfile
-import typing
 
 import icalendar
 
@@ -88,37 +86,37 @@ def _event_vdir_filename(event: icalendar.cal.Event) -> str:
     return output_filename + _VDIR_EVENT_FILE_EXTENSION
 
 
-@contextlib.contextmanager
-def _temp_vdir_item() -> typing.Iterator[typing.Tuple[int, str]]:
+def _write_event(event: icalendar.cal.Event, path: pathlib.Path):
+    # > Creating and modifying items or metadata files should happen atomically.
+    # https://vdirsyncer.readthedocs.io/en/stable/vdir.html#writing-to-vdirs
     temp_fd, temp_path = tempfile.mkstemp(
         prefix="ical2vdir-", suffix=_VDIR_EVENT_FILE_EXTENSION
     )
     try:
-        yield (temp_fd, temp_path)
+        os.write(temp_fd, event.to_ical())
+        os.close(temp_fd)
+        os.rename(temp_path, path)
     finally:
         if os.path.exists(temp_path):
             os.unlink(temp_path)
 
 
-def _export_event(
+def _sync_event(
     event: icalendar.cal.Event, output_dir_path: pathlib.Path
 ) -> pathlib.Path:
-    with _temp_vdir_item() as (temp_fd, temp_path):
-        os.write(temp_fd, event.to_ical())
-        os.close(temp_fd)
-        output_path = output_dir_path.joinpath(_event_vdir_filename(event))
-        if not output_path.exists():
-            _LOGGER.info("creating %s", output_path)
-            os.rename(temp_path, output_path)
+    output_path = output_dir_path.joinpath(_event_vdir_filename(event))
+    if not output_path.exists():
+        _LOGGER.info("creating %s", output_path)
+        _write_event(event, output_path)
+    else:
+        with open(output_path, "rb") as current_file:
+            current_event = icalendar.Event.from_ical(current_file.read())
+        if _events_equal(event, current_event):
+            _LOGGER.debug("%s is up to date", output_path)
         else:
-            with open(output_path, "rb") as current_file:
-                current_event = icalendar.Event.from_ical(current_file.read())
-            if _events_equal(event, current_event):
-                _LOGGER.debug("%s is up to date", output_path)
-            else:
-                _LOGGER.info("updating %s", output_path)
-                os.rename(temp_path, output_path)
-        return output_path
+            _LOGGER.info("updating %s", output_path)
+            _write_event(event, output_path)
+    return output_path
 
 
 def _main():
@@ -173,7 +171,7 @@ def _main():
     for component in calendar.subcomponents:
         if isinstance(component, icalendar.cal.Event):
             extra_paths.discard(
-                _export_event(event=component, output_dir_path=args.output_dir_path)
+                _sync_event(event=component, output_dir_path=args.output_dir_path)
             )
         else:
             _LOGGER.debug("%s", component)

+ 8 - 8
tests/vdir_test.py

@@ -80,22 +80,22 @@ def test__event_vdir_filename(event_ical, expected_filename):
 
 
 @pytest.mark.parametrize("event_ical", [_SINGLE_EVENT_ICAL])
-def test__export_event_create(tmpdir, event_ical):
+def test__sync_event_create(tmpdir, event_ical):
     temp_path = pathlib.Path(tmpdir)
     event = icalendar.cal.Event.from_ical(event_ical)
-    ical2vdir._export_event(event, temp_path)
+    ical2vdir._sync_event(event, temp_path)
     (ics_path,) = temp_path.iterdir()
     assert ics_path.name == "1qa2ws3ed4rf5tg@google.com.ics"
     assert ics_path.read_bytes() == _SINGLE_EVENT_ICAL
 
 
 @pytest.mark.parametrize("event_ical", [_SINGLE_EVENT_ICAL])
-def test__export_event_update(tmpdir, event_ical):
+def test__sync_event_update(tmpdir, event_ical):
     temp_path = pathlib.Path(tmpdir)
     event = icalendar.cal.Event.from_ical(event_ical)
-    ical2vdir._export_event(event, temp_path)
+    ical2vdir._sync_event(event, temp_path)
     event["SUMMARY"] += " suffix"
-    ical2vdir._export_event(event, temp_path)
+    ical2vdir._sync_event(event, temp_path)
     (ics_path,) = temp_path.iterdir()
     assert ics_path.name == event["UID"] + ".ics"
     assert ics_path.read_bytes() == _SINGLE_EVENT_ICAL.replace(
@@ -104,12 +104,12 @@ def test__export_event_update(tmpdir, event_ical):
 
 
 @pytest.mark.parametrize("event_ical", [_SINGLE_EVENT_ICAL])
-def test__export_event_unchanged(tmpdir, event_ical):
+def test__sync_event_unchanged(tmpdir, event_ical):
     temp_path = pathlib.Path(tmpdir)
     event = icalendar.cal.Event.from_ical(event_ical)
-    ical2vdir._export_event(event, temp_path)
+    ical2vdir._sync_event(event, temp_path)
     (ics_path,) = temp_path.iterdir()
     old_stat = copy.deepcopy(ics_path.stat())
-    ical2vdir._export_event(event, temp_path)
+    ical2vdir._sync_event(event, temp_path)
     assert ics_path.stat() == old_stat
     assert ics_path.read_bytes() == _SINGLE_EVENT_ICAL