Skip to content

Commit 2084534

Browse files
Michael Adleruntitaker
Michael Adler
authored andcommitted
Introduce post_hook for filesystem storage
1 parent ba4407a commit 2084534

File tree

3 files changed

+53
-5
lines changed

3 files changed

+53
-5
lines changed

example.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ collections = ["private", "work"]
5858
type = filesystem
5959
path = ~/.calendars/
6060
fileext = .ics
61+
# For each new / updated file f, invoke the following script with argument f:
62+
#post_hook = /usr/local/bin/post_process.sh
6163

6264
[storage bob_calendar_remote]
6365
type = caldav

tests/storage/test_filesystem.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import sys
5+
import subprocess
56

67
import pytest
78

@@ -66,3 +67,29 @@ def test_case_sensitive_uids(self, s, get_item):
6667
items = list(href for href, etag in s.list())
6768
assert len(items) == 1
6869
assert len(set(items)) == 1
70+
71+
def test_post_hook_inactive(self, tmpdir, monkeypatch):
72+
73+
def check_call_mock(*args, **kwargs):
74+
assert False
75+
76+
monkeypatch.setattr(subprocess, 'call', check_call_mock)
77+
78+
s = self.storage_class(str(tmpdir), '.txt', post_hook=None)
79+
s.upload(Item(u'UID:a/b/c'))
80+
81+
def test_post_hook_active(self, tmpdir, monkeypatch):
82+
83+
calls = []
84+
exe = 'foo'
85+
86+
def check_call_mock(l, *args, **kwargs):
87+
calls.append(True)
88+
assert len(l) == 2
89+
assert l[0] == exe
90+
91+
monkeypatch.setattr(subprocess, 'call', check_call_mock)
92+
93+
s = self.storage_class(str(tmpdir), '.txt', post_hook=exe)
94+
s.upload(Item(u'UID:a/b/c'))
95+
assert calls

vdirsyncer/storage/filesystem.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import errno
44
import os
5+
import subprocess
56
import uuid
67

78
from atomicwrites import atomic_write
@@ -35,13 +36,15 @@ class FilesystemStorage(Storage):
3536
storage_name = 'filesystem'
3637
_repr_attributes = ('path',)
3738

38-
def __init__(self, path, fileext, encoding='utf-8', **kwargs):
39+
def __init__(self, path, fileext, encoding='utf-8', post_hook=None,
40+
**kwargs):
3941
super(FilesystemStorage, self).__init__(**kwargs)
4042
path = expand_path(path)
4143
checkdir(path, create=False)
4244
self.path = path
4345
self.encoding = encoding
4446
self.fileext = fileext
47+
self.post_hook = post_hook
4548

4649
@classmethod
4750
def discover(cls, path, **kwargs):
@@ -103,7 +106,7 @@ def upload(self, item):
103106

104107
try:
105108
href = self._deterministic_href(item)
106-
return self._upload_impl(item, href)
109+
fpath, etag = self._upload_impl(item, href)
107110
except OSError as e:
108111
if e.errno in (
109112
errno.ENAMETOOLONG, # Unix
@@ -112,16 +115,20 @@ def upload(self, item):
112115
logger.debug('UID as filename rejected, trying with random '
113116
'one.')
114117
href = self._random_href()
115-
return self._upload_impl(item, href)
118+
fpath, etag = self._upload_impl(item, href)
116119
else:
117120
raise
118121

122+
if self.post_hook:
123+
self._run_post_hook(fpath)
124+
return href, etag
125+
119126
def _upload_impl(self, item, href):
120127
fpath = self._get_filepath(href)
121128
try:
122129
with atomic_write(fpath, mode='wb', overwrite=False) as f:
123130
f.write(item.raw.encode(self.encoding))
124-
return href, get_etag_from_fileobject(f)
131+
return fpath, get_etag_from_fileobject(f)
125132
except OSError as e:
126133
if e.errno == errno.EEXIST:
127134
raise exceptions.AlreadyExistingError(item)
@@ -141,7 +148,11 @@ def update(self, href, item, etag):
141148

142149
with atomic_write(fpath, mode='wb', overwrite=True) as f:
143150
f.write(item.raw.encode(self.encoding))
144-
return get_etag_from_fileobject(f)
151+
etag = get_etag_from_fileobject(f)
152+
153+
if self.post_hook:
154+
self._run_post_hook(fpath)
155+
return etag
145156

146157
def delete(self, href, etag):
147158
fpath = self._get_filepath(href)
@@ -151,3 +162,11 @@ def delete(self, href, etag):
151162
if etag != actual_etag:
152163
raise exceptions.WrongEtagError(etag, actual_etag)
153164
os.remove(fpath)
165+
166+
def _run_post_hook(self, fpath):
167+
logger.info('Calling post_hook={} with argument={}'.format(
168+
self.post_hook, fpath))
169+
try:
170+
subprocess.call([self.post_hook, fpath])
171+
except OSError:
172+
logger.exception('Error executing external hook.')

0 commit comments

Comments
 (0)