From eec7d5bbbffac540dbf542420029baccb5ec81ac Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:09:11 +0200 Subject: [PATCH 1/9] add support for placeholders 'request' and 'to_path' --- radicale/storage/multifilesystem/lock.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/radicale/storage/multifilesystem/lock.py b/radicale/storage/multifilesystem/lock.py index 0f7b56d9..cce125f0 100644 --- a/radicale/storage/multifilesystem/lock.py +++ b/radicale/storage/multifilesystem/lock.py @@ -78,10 +78,16 @@ class StoragePartLock(StorageBase): preexec_fn = os.setpgrp # optional argument path = kwargs.get('path', "") + request = kwargs.get('request', "NONE") + to_path = kwargs.get('to_path', "") + if to_path != "": + to_path = shlex.quote(self._get_collection_root_folder() + to_path) try: command = self._hook % { "path": shlex.quote(self._get_collection_root_folder() + path), + "to_path": to_path, "cwd": shlex.quote(self._filesystem_folder), + "request": shlex.quote(request), "user": shlex.quote(user or "Anonymous")} except KeyError as e: logger.error("Storage hook contains not supported placeholder %s (skip execution of: %r)" % (e, self._hook)) From eae5daae54627af94645b8e55e7b87a0429c48db Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:10:11 +0200 Subject: [PATCH 2/9] doc new placeholders --- DOCUMENTATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 2c6c0e92..6527fb14 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1373,6 +1373,8 @@ Supported placeholders: - `%(user)s`: logged-in user - `%(cwd)s`: current working directory _(>= 3.5.1)_ - `%(path)s`: full path of item _(>= 3.5.1)_ + - `%(to_path)s`: full path of destination item (only set on MOVE request) _(>= 3.5.5)_ + - `%(request)s`: request method _(>= 3.5.5)_ Command will be executed with base directory defined in `filesystem_folder` (see above) From 0c16f86bd4f6607a0e1e35d7121e05f43903bf8a Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:10:32 +0200 Subject: [PATCH 3/9] default config: new placeholders --- config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config b/config index 9b6c577b..d3e2283f 100644 --- a/config +++ b/config @@ -234,9 +234,12 @@ # %(user)s: logged-in user # %(cwd)s : current working directory # %(path)s: full path of item +# %(to_path)s: full path of destination item (only set on MOVE request) +# %(request)s: request method # Command will be executed with base directory defined in filesystem_folder # For "git" check DOCUMENTATION.md for bootstrap instructions # Example(test): echo \"user=%(user)s path=%(path)s cwd=%(cwd)s\" +# Example(test/json): echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\" # Example(git): git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"") #hook = From 19def4d5613a9645d57aa62351e991cc2f28bf0c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:11:24 +0200 Subject: [PATCH 4/9] add for write requests "path" where missing and "request" --- radicale/app/delete.py | 2 +- radicale/app/mkcalendar.py | 2 +- radicale/app/mkcol.py | 2 +- radicale/app/move.py | 2 +- radicale/app/proppatch.py | 2 +- radicale/app/put.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/radicale/app/delete.py b/radicale/app/delete.py index 13ab0d31..a8c7f0a6 100644 --- a/radicale/app/delete.py +++ b/radicale/app/delete.py @@ -60,7 +60,7 @@ class ApplicationPartDelete(ApplicationBase): access = Access(self._rights, user, path) if not access.check("w"): return httputils.NOT_ALLOWED - with self._storage.acquire_lock("w", user): + with self._storage.acquire_lock("w", user, path=path, request="DELETE"): item = next(iter(self._storage.discover(path)), None) if not item: return httputils.NOT_FOUND diff --git a/radicale/app/mkcalendar.py b/radicale/app/mkcalendar.py index b9f2063a..632d3c38 100644 --- a/radicale/app/mkcalendar.py +++ b/radicale/app/mkcalendar.py @@ -57,7 +57,7 @@ class ApplicationPartMkcalendar(ApplicationBase): return httputils.BAD_REQUEST # TODO: use this? # timezone = props.get("C:calendar-timezone") - with self._storage.acquire_lock("w", user): + with self._storage.acquire_lock("w", user, path=path, request="MKCALENDAR"): item = next(iter(self._storage.discover(path)), None) if item: return self._webdav_error_response( diff --git a/radicale/app/mkcol.py b/radicale/app/mkcol.py index 953508ad..169cb62c 100644 --- a/radicale/app/mkcol.py +++ b/radicale/app/mkcol.py @@ -62,7 +62,7 @@ class ApplicationPartMkcol(ApplicationBase): if not props.get("tag") and "W" not in permissions: logger.warning("MKCOL request %r (type:%s): %s", path, collection_type, "rejected because of missing rights 'W'") return httputils.NOT_ALLOWED - with self._storage.acquire_lock("w", user): + with self._storage.acquire_lock("w", user, path=path, request="MKCOL"): item = next(iter(self._storage.discover(path)), None) if item: return httputils.METHOD_NOT_ALLOWED diff --git a/radicale/app/move.py b/radicale/app/move.py index f555e871..77e56f3e 100644 --- a/radicale/app/move.py +++ b/radicale/app/move.py @@ -73,7 +73,7 @@ class ApplicationPartMove(ApplicationBase): if not to_access.check("w"): return httputils.NOT_ALLOWED - with self._storage.acquire_lock("w", user): + with self._storage.acquire_lock("w", user, path=path, request="MOVE", to_path=to_path): item = next(iter(self._storage.discover(path)), None) if not item: return httputils.NOT_FOUND diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 76b4a1a1..d2c32811 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -87,7 +87,7 @@ class ApplicationPartProppatch(ApplicationBase): except socket.timeout: logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT - with self._storage.acquire_lock("w", user): + with self._storage.acquire_lock("w", user, path=path, request="PROPPATCH"): item = next(iter(self._storage.discover(path)), None) if not item: return httputils.NOT_FOUND diff --git a/radicale/app/put.py b/radicale/app/put.py index 9a08e561..343f3324 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -174,7 +174,7 @@ class ApplicationPartPut(ApplicationBase): bool(rights.intersect(access.permissions, "Ww")), bool(rights.intersect(access.parent_permissions, "w"))) - with self._storage.acquire_lock("w", user, path=path): + with self._storage.acquire_lock("w", user, path=path, request="PUT"): item = next(iter(self._storage.discover(path)), None) parent_item = next(iter( self._storage.discover(access.parent_path)), None) From 6a8f5f4a5b3014e9fc2dc9f9bbf551d65d7072ea Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:12:15 +0200 Subject: [PATCH 5/9] extend/adjust copyright --- radicale/app/delete.py | 4 ++-- radicale/tests/test_storage.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/radicale/app/delete.py b/radicale/app/delete.py index a8c7f0a6..a111df00 100644 --- a/radicale/app/delete.py +++ b/radicale/app/delete.py @@ -2,8 +2,8 @@ # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub -# Copyright © 2017-2018 Unrud -# Copyright © 2024-2024 Peter Bieringer +# Copyright © 2017-2020 Unrud +# Copyright © 2024-2025 Peter Bieringer # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 89572bca..91ff8110 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -1,7 +1,7 @@ # This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2017-2022 Unrud -# Copyright © 2024-2024 Peter Bieringer +# Copyright © 2024-2025 Peter Bieringer # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 97d30478c731a9ca3e63ceb6ebd13a29ed207d5c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:12:38 +0200 Subject: [PATCH 6/9] add tests for storage hook placeholders --- radicale/tests/test_storage.py | 214 +++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 91ff8110..d6ecefbb 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -21,7 +21,10 @@ Tests for storage backends. """ +import json +import logging import os +import re import shutil from typing import ClassVar, cast @@ -186,6 +189,217 @@ class TestMultiFileSystem(BaseTest): assert answer is not None assert "\r\nUID:%s\r\n" % uid in answer + def test_hook_placeholders_PUT(self, caplog) -> None: + """Run hook and check placeholders: PUT""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event) + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "PUT": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == "": + found = found | 256 + else: + found = found | 256 | 32 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + + def test_hook_placeholders_DELETE(self, caplog) -> None: + """Run hook and check placeholders: DELETE""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + path = "/calendar.ics/event1.ics" + self.put(path, event) + self.delete(path) + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "DELETE": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == "": + found = found | 256 + else: + found = found | 256 | 32 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, s) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + + def test_hook_placeholders_MKCALENDAR(self, caplog) -> None: + """Run hook and check placeholders: MKCALENDAR""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcalendar("/calendar.ics/") + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/calendar.ics/": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "MKCALENDAR": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == "": + found = found | 256 + else: + found = found | 256 | 32 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + + def test_hook_placeholders_MKCOL(self, caplog) -> None: + """Run hook and check placeholders: MKCOL""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcol("/user1/") + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/user1/": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "MKCOL": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == "": + found = found | 256 + else: + found = found | 256 | 32 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + + def test_hook_placeholders_PROPPATCH(self, caplog) -> None: + """Run hook and check placeholders: PROPPATCH""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcalendar("/calendar.ics/") + proppatch = get_file_content("proppatch_set_calendar_color.xml") + _, responses = self.proppatch("/calendar.ics/", proppatch) + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/calendar.ics/": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "PROPPATCH": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == "": + found = found | 256 + else: + found = found | 256 | 32 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + + def test_hook_placeholders_MOVE(self, caplog) -> None: + """Run hook and check placeholders: MOVE""" + self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) + found = 0 + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1.ics") + path1 = "/calendar.ics/event1.ics" + path2 = "/calendar.ics/event2.ics" + self.put(path1, event) + self.request("MOVE", path1, check=201, + HTTP_DESTINATION="http://127.0.0.1/"+path2) + for line in caplog.messages: + if line.find("\"hook-json ") != -1: + found = 1 + s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + d = json.loads(s) + if d["user"] == "Anonymous": + found = found | 2 + if d["cwd"]: + found = found | 4 + if d["path"]: + found = found | 8 + if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": + found = found | 16 + if d["request"]: + found = found | 64 + if d["request"] == "MOVE": + found = found | 128 + if d["to_path"]: + found = found | 32 + if d["to_path"] == d["cwd"] + "/collection-root/calendar.ics/event2.ics": + found = found | 256 + if (found != 511): + raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) + else: + logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + class TestMultiFileSystemNoLock(BaseTest): """Tests for multifilesystem_nolock.""" From 258d4cc639ab6b7f0b4445b8845019609889b678 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:16:39 +0200 Subject: [PATCH 7/9] extend changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50ad2d3..7eafbee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ * Fix: add support for query without comp-type * Fix: expanded event with dates are missing VALUE=DATE * Add: [hook] dryrun: option to disable real hook action for testing, add tests for email+rabbitmq +* Fix: storage hook path now added to DELETE, MKCOL, MKCALENDAR, MOVE, and PROPPATCH +* Add: storage hook placeholder now supports "request" and "to_path" (MOVE only) ## 3.5.4 * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute) From 91d4dc3ef1360b3e4394e6784d33f5aecf7ad485 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:42:06 +0200 Subject: [PATCH 8/9] make mypy happy --- radicale/tests/test_storage.py | 36 ++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index d6ecefbb..9b077180 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -200,7 +200,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 @@ -237,7 +241,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 @@ -270,7 +278,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 @@ -303,7 +315,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 @@ -338,7 +354,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 @@ -377,7 +397,11 @@ class TestMultiFileSystem(BaseTest): for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 - s = re.search('.*\"hook-json ({.*})".*', line).group(1).replace("'", "\"") + r = re.search('.*\"hook-json ({.*})".*', line) + if r: + s = r.group(1).replace("'", "\"") + else: + break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 From 7bedca72e237f2c4103594266be08dc3d4bdfa19 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 12 Aug 2025 16:48:04 +0200 Subject: [PATCH 9/9] catch OS without flock --- radicale/tests/test_storage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 9b077180..70248504 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -74,8 +74,7 @@ class TestMultiFileSystem(BaseTest): self.propfind("/") self.propfind("/created_by_hook/", check=404) - @pytest.mark.skipif(not shutil.which("flock"), - reason="flock command not found") + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_storage_locked(self) -> None: """Verify that the storage is locked when the hook runs.""" self.configure({"storage": {"hook": ( @@ -189,6 +188,7 @@ class TestMultiFileSystem(BaseTest): assert answer is not None assert "\r\nUID:%s\r\n" % uid in answer + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_PUT(self, caplog) -> None: """Run hook and check placeholders: PUT""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) @@ -229,6 +229,7 @@ class TestMultiFileSystem(BaseTest): else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_DELETE(self, caplog) -> None: """Run hook and check placeholders: DELETE""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) @@ -270,6 +271,7 @@ class TestMultiFileSystem(BaseTest): else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_MKCALENDAR(self, caplog) -> None: """Run hook and check placeholders: MKCALENDAR""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) @@ -307,6 +309,7 @@ class TestMultiFileSystem(BaseTest): else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_MKCOL(self, caplog) -> None: """Run hook and check placeholders: MKCOL""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) @@ -344,6 +347,7 @@ class TestMultiFileSystem(BaseTest): else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_PROPPATCH(self, caplog) -> None: """Run hook and check placeholders: PROPPATCH""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) @@ -383,6 +387,7 @@ class TestMultiFileSystem(BaseTest): else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) + @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_placeholders_MOVE(self, caplog) -> None: """Run hook and check placeholders: MOVE""" self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}})