From 208453408febdbd7e465004020f0716a4938f5c0 Mon Sep 17 00:00:00 2001 From: Michael Adler Date: Sun, 26 Oct 2014 10:04:46 +0100 Subject: [PATCH] Introduce post_hook for filesystem storage --- example.cfg | 2 ++ tests/storage/test_filesystem.py | 27 +++++++++++++++++++++++++++ vdirsyncer/storage/filesystem.py | 29 ++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/example.cfg b/example.cfg index 950007a..4180785 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 ce99728..cfd4fee 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 @@ class TestFilesystemStorage(StorageTests): 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 c9a4bde..bce7eec 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 @@ class FilesystemStorage(Storage): 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 @@ class FilesystemStorage(Storage): 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 @@ class FilesystemStorage(Storage): 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 @@ class FilesystemStorage(Storage): 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.')