diff --git a/example.cfg b/example.cfg index 950007a10..41807853b 100644 --- a/example.cfg +++ b/example.cfg @@ -58,6 +58,8 @@ collections = ["private", "work"] type = filesystem path = ~/.calendars/ fileext = .ics +# For each new / updated file f, invoke the following script with argument f: +#post_hook = /usr/local/bin/post_process.sh [storage bob_calendar_remote] type = caldav diff --git a/tests/storage/test_filesystem.py b/tests/storage/test_filesystem.py index ce997288c..cfd4fee05 100644 --- a/tests/storage/test_filesystem.py +++ b/tests/storage/test_filesystem.py @@ -2,6 +2,7 @@ import os import sys +import subprocess import pytest @@ -66,3 +67,29 @@ def test_case_sensitive_uids(self, s, get_item): items = list(href for href, etag in s.list()) assert len(items) == 1 assert len(set(items)) == 1 + + def test_post_hook_inactive(self, tmpdir, monkeypatch): + + def check_call_mock(*args, **kwargs): + assert False + + monkeypatch.setattr(subprocess, 'call', check_call_mock) + + s = self.storage_class(str(tmpdir), '.txt', post_hook=None) + s.upload(Item(u'UID:a/b/c')) + + def test_post_hook_active(self, tmpdir, monkeypatch): + + calls = [] + exe = 'foo' + + def check_call_mock(l, *args, **kwargs): + calls.append(True) + assert len(l) == 2 + assert l[0] == exe + + monkeypatch.setattr(subprocess, 'call', check_call_mock) + + s = self.storage_class(str(tmpdir), '.txt', post_hook=exe) + s.upload(Item(u'UID:a/b/c')) + assert calls diff --git a/vdirsyncer/storage/filesystem.py b/vdirsyncer/storage/filesystem.py index c9a4bde67..bce7eec64 100644 --- a/vdirsyncer/storage/filesystem.py +++ b/vdirsyncer/storage/filesystem.py @@ -2,6 +2,7 @@ import errno import os +import subprocess import uuid from atomicwrites import atomic_write @@ -35,13 +36,15 @@ class FilesystemStorage(Storage): storage_name = 'filesystem' _repr_attributes = ('path',) - def __init__(self, path, fileext, encoding='utf-8', **kwargs): + def __init__(self, path, fileext, encoding='utf-8', post_hook=None, + **kwargs): super(FilesystemStorage, self).__init__(**kwargs) path = expand_path(path) checkdir(path, create=False) self.path = path self.encoding = encoding self.fileext = fileext + self.post_hook = post_hook @classmethod def discover(cls, path, **kwargs): @@ -103,7 +106,7 @@ def upload(self, item): try: href = self._deterministic_href(item) - return self._upload_impl(item, href) + fpath, etag = self._upload_impl(item, href) except OSError as e: if e.errno in ( errno.ENAMETOOLONG, # Unix @@ -112,16 +115,20 @@ def upload(self, item): logger.debug('UID as filename rejected, trying with random ' 'one.') href = self._random_href() - return self._upload_impl(item, href) + fpath, etag = self._upload_impl(item, href) else: raise + if self.post_hook: + self._run_post_hook(fpath) + return href, etag + def _upload_impl(self, item, href): fpath = self._get_filepath(href) try: with atomic_write(fpath, mode='wb', overwrite=False) as f: f.write(item.raw.encode(self.encoding)) - return href, get_etag_from_fileobject(f) + return fpath, get_etag_from_fileobject(f) except OSError as e: if e.errno == errno.EEXIST: raise exceptions.AlreadyExistingError(item) @@ -141,7 +148,11 @@ def update(self, href, item, etag): with atomic_write(fpath, mode='wb', overwrite=True) as f: f.write(item.raw.encode(self.encoding)) - return get_etag_from_fileobject(f) + etag = get_etag_from_fileobject(f) + + if self.post_hook: + self._run_post_hook(fpath) + return etag def delete(self, href, etag): fpath = self._get_filepath(href) @@ -151,3 +162,11 @@ def delete(self, href, etag): if etag != actual_etag: raise exceptions.WrongEtagError(etag, actual_etag) os.remove(fpath) + + def _run_post_hook(self, fpath): + logger.info('Calling post_hook={} with argument={}'.format( + self.post_hook, fpath)) + try: + subprocess.call([self.post_hook, fpath]) + except OSError: + logger.exception('Error executing external hook.')