From 7c4005cbf472299df877b66f7236cf2c6ca511d9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:16:17 +0200 Subject: [PATCH 01/18] tox: add 3 optional test cases to test using Linux OS: vfat, hfsplus, ntfs (requires sudo and filesystem tools) --- pyproject.toml | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c8276115..e220f9c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,87 @@ deps = [ ] commands = [["pytest", "-r", "s", "--log-level", "5", "."]] +[tool.tox.env.py_filesystem_vfat] +allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ] +extras = ["test"] +deps = [ + "pytest" +] +commands_pre = [ + # create 64 MByte disk image + ["dd", "if=/dev/zero", "of=/tmp/vfat.img", "bs=1M", "count=64"], + # create file system + ["mkfs.vfat", "/tmp/vfat.img", "-n", "VFAT"], + # unconditionally create mount point + ["mkdir", "-p", "/tmp/vfat"], + # mount image + ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/vfat.img", "/tmp/vfat"], +] +setenv = { TEMP = "/tmp/vfat" } +commands = [["pytest", "-r", "s", "."]] +commands_post = [ + # umount image + ["sudo", "/usr/bin/umount", "-d", "/tmp/vfat"], + # remove mount point + ["rmdir", "/tmp/vfat"], + # remove image + ["rm", "/tmp/vfat.img"] +] + +[tool.tox.env.py_filesystem_hfsp] +allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ] +extras = ["test"] +deps = [ + "pytest" +] +commands_pre = [ + # create 64 MByte disk image + ["dd", "if=/dev/zero", "of=/tmp/hfsp.img", "bs=1M", "count=64"], + # create file system + ["mkfs.hfsplus", "/tmp/hfsp.img", "-v", "HFSP"], + # unconditionally create mount point + ["mkdir", "-p", "/tmp/hfsp"], + # mount image + ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp"], +] +setenv = { TEMP = "/tmp/hfsp" } +commands = [["pytest", "-r", "s", "."]] +commands_post = [ + # umount image + ["sudo", "/usr/bin/umount", "-d", "/tmp/hfsp"], + # remove mount point + ["rmdir", "/tmp/hfsp"], + # remove image + ["rm", "/tmp/hfsp.img"] +] + +[tool.tox.env.py_filesystem_ntfs] +allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ] +extras = ["test"] +deps = [ + "pytest" +] +commands_pre = [ + # create 64 MByte disk image + ["dd", "if=/dev/zero", "of=/tmp/ntfs.img", "bs=1M", "count=64"], + # create file system + ["mkfs.ntfs", "/tmp/ntfs.img", "-L", "NTFS", "-F"], + # unconditionally create mount point + ["mkdir", "-p", "/tmp/ntfs"], + # mount image + ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/ntfs.img", "/tmp/ntfs"], +] +setenv = { TEMP = "/tmp/ntfs" } +commands = [["pytest", "-r", "s", "."]] +commands_post = [ + # umount image + ["sudo", "/usr/bin/umount", "-d", "/tmp/ntfs"], + # remove mount point + ["rmdir", "/tmp/ntfs"], + # remove image + ["rm", "/tmp/ntfs.img"] +] + [tool.tox.env.flake8] deps = ["flake8==7.1.0"] commands = [["flake8", "."]] From e998943a7353d28e53fc4cdc5ff1a88266b8d92a Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:17:40 +0200 Subject: [PATCH 02/18] application startup: stop if TEMP is set but not writable or even not existing --- radicale/app/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 0a93dc2c..e8594fe9 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -100,6 +100,11 @@ class Application(ApplicationPartDelete, ApplicationPartHead, """ super().__init__(configuration) + if 'TEMP' in os.environ: + if not os.path.isdir(os.environ['TEMP']): + raise RuntimeError("TEMP found in environment, but directory is not existing: %r" % os.environ['TEMP']) + if not os.access(os.environ['TEMP'], os.W_OK): + raise RuntimeError("TEMP found in environment, but not writable: %r" % os.environ['TEMP']) self._mask_passwords = configuration.get("logging", "mask_passwords") self._delay_on_error = configuration.get("server", "delay_on_error") logger.info("delay_on_error set to: %.3f seconds", self._delay_on_error) From cd4d83200320dec0c37194b21c74902250f906fa Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:20:55 +0200 Subject: [PATCH 03/18] Adjust: respond with 500 in case principal collection cannot be created (e.g. filesystem issues) --- radicale/app/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index e8594fe9..9956e023 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -540,9 +540,13 @@ class Application(ApplicationPartDelete, ApplicationPartHead, except ValueError as e: logger.warning("Failed to create predefined collection %r: %s", name_coll, e) except ValueError as e: - logger.warning("Failed to create principal " - "collection %r: %s", user, e) - user = "" + logger.error("Failed to create principal " + "collection for user %r: ValueError %s", user, e) + return response(*httputils.INTERNAL_SERVER_ERROR) + except OSError as e: + logger.error("Failed to create principal " + "collection for user %r: OSerror %s", user, e) + return response(*httputils.INTERNAL_SERVER_ERROR) else: logger.warning("Access to principal path %r denied by " "rights backend", principal_path) From 1a34c7826193281b74151b6f2024498bbab8d421 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:21:41 +0200 Subject: [PATCH 04/18] test and display features of used collection base directory --- radicale/pathutils.py | 75 +++++++++++++++++++- radicale/storage/multifilesystem/__init__.py | 3 + 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index 36a66461..71ba4438 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -407,7 +407,7 @@ def path_supports_symlink(path): def path_is_collision_free_case_sensitive(path): - # Test: case sensitive + """Check whether path supports case sensitive entries.""" if not os.path.isdir(path): raise ValueError("%r is not a path" % (path)) base_dir = tempfile.mkdtemp(dir=path) @@ -452,3 +452,76 @@ def path_is_collision_free_no_short_filename(path): os.rmdir(base_dir) logger.debug("path_is_collision_free (no short-filename): path=%r result=%s", path, result) return result + + +def path_supports_unicode(path): + """Check whether path supports unicode.""" + if not os.path.isdir(path): + raise ValueError("%r is not a path" % (path)) + base_dir = tempfile.mkdtemp(dir=path) + part = "TESTπŸ˜€" + test_dir = os.path.join(base_dir, part) + result = True + try: + os.mkdir(test_dir) + except OSError: + result = False + else: + with os.scandir(base_dir) as entries: + if part not in (e.name for e in entries): + result = False + # cleanup + os.rmdir(test_dir) + # cleanup + os.rmdir(base_dir) + logger.debug("path_supports_unicode: path=%r result=%s", path, result) + return result + + +def path_supports_trailing_whitespace(path): + """Check whether path supports trailing whitespace.""" + if not os.path.isdir(path): + raise ValueError("%r is not a path" % (path)) + base_dir = tempfile.mkdtemp(dir=path) + part = "TEST " + test_dir = os.path.join(base_dir, part) + result = True + try: + os.mkdir(test_dir) + except OSError: + result = False + else: + with os.scandir(base_dir) as entries: + if part not in (e.name for e in entries): + result = False + # cleanup + os.rmdir(test_dir) + # cleanup + os.rmdir(base_dir) + logger.debug("path_supports_trailing_whitespace: path=%r result=%s", path, result) + return result + + +def path_supports_problematic_chars(path): + """Check whether path supports problematic chars.""" + if not os.path.isdir(path): + raise ValueError("%r is not a path" % (path)) + base_dir = tempfile.mkdtemp(dir=path) + result = True + for char in ['*', '?']: + part = "TES" + char + "T" + test_dir = os.path.join(base_dir, part) + try: + os.mkdir(test_dir) + except OSError: + result = False + else: + with os.scandir(base_dir) as entries: + if part not in (e.name for e in entries): + result = False + # cleanup + os.rmdir(test_dir) + # cleanup + os.rmdir(base_dir) + logger.debug("path_supports_problematic chars: path=%r result=%s", path, result) + return result diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 7e6ead8e..7deeaac7 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -180,6 +180,9 @@ class Storage( self._filesystem_root_folder_is_collision_free, filesystem_root_folder_is_collision_free_case_sensitive, filesystem_root_folder_is_collision_free_no_short_filename) + logger.info("Storage location subfolder suppports unicode: %s", pathutils.path_supports_unicode(self._get_collection_root_folder())) + logger.info("Storage location subfolder suppports trailing whitespace: %s", pathutils.path_supports_trailing_whitespace(self._get_collection_root_folder())) + logger.info("Storage location subfolder suppports problematic chars: %s", pathutils.path_supports_problematic_chars(self._get_collection_root_folder())) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken) From 93a4d9390c080e96ee15191e75e396dad15de78f Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:22:46 +0200 Subject: [PATCH 05/18] add various test cases for filesystems --- radicale/tests/test_auth.py | 41 ++++++++++++++++++++++++++-------- radicale/tests/test_storage.py | 20 +++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index 8051e0f5..69110e73 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -31,7 +31,7 @@ from typing import Iterable, Tuple, Union import pytest -from radicale import xmlutils +from radicale import pathutils, xmlutils from radicale.auth import htpasswd from radicale.tests import BaseTest @@ -64,7 +64,7 @@ class TestBaseAuthRequests(BaseTest): def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str, test_matrix: Union[str, Iterable[Tuple[str, str, bool]]] - = "ascii", delay: float = 0) -> None: + = "ascii", delay: float = 0, check: int = 207) -> None: """Test htpasswd authentication with user "tmp" and password "bepo" for ``test_matrix`` "ascii" or user "πŸ˜€" and password "πŸ”‘" for ``test_matrix`` "unicode".""" @@ -88,7 +88,7 @@ class TestBaseAuthRequests(BaseTest): elif isinstance(test_matrix, str): raise ValueError("Unknown test matrix %r" % test_matrix) for user, password, valid in test_matrix: - self.propfind("/", check=207 if valid else 401, + self.propfind("/", check=check if valid else 401, login="%s:%s" % (user, password)) def test_htpasswd_plain(self) -> None: @@ -102,7 +102,11 @@ class TestBaseAuthRequests(BaseTest): ("tmp", "be:po", True), ("tmp", "bepo", False))) def test_htpasswd_plain_unicode(self) -> None: - self._test_htpasswd("plain", "πŸ˜€:πŸ”‘", "unicode") + if not pathutils.path_supports_unicode(self.colpath): + check = 500 + else: + check = 207 + self._test_htpasswd("plain", "πŸ˜€:πŸ”‘", "unicode", check=check) def test_htpasswd_md5(self) -> None: self._test_htpasswd("md5", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") @@ -111,8 +115,12 @@ class TestBaseAuthRequests(BaseTest): self._test_htpasswd("autodetect", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") def test_htpasswd_md5_unicode(self): + if not pathutils.path_supports_unicode(self.colpath): + check = 500 + else: + check = 207 self._test_htpasswd( - "md5", "πŸ˜€:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode") + "md5", "πŸ˜€:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode", check=check) def test_htpasswd_sha256(self) -> None: self._test_htpasswd("sha256", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/") @@ -166,7 +174,11 @@ class TestBaseAuthRequests(BaseTest): @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") def test_htpasswd_bcrypt_unicode(self) -> None: - self._test_htpasswd("bcrypt", "πŸ˜€:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode") + if not pathutils.path_supports_unicode(self.colpath): + check = 500 + else: + check = 207 + self._test_htpasswd("bcrypt", "πŸ˜€:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode", check=check) @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") def test_htpasswd_bcrypt_long(self) -> None: @@ -280,12 +292,23 @@ class TestBaseAuthRequests(BaseTest): else: raise - @pytest.mark.skipif(sys.platform == "win32", reason="leading and trailing " - "whitespaces not allowed in file names") def test_htpasswd_whitespace_user(self) -> None: for user in (" tmp", "tmp ", " tmp "): + if not pathutils.path_supports_trailing_whitespace(self.colpath) and user.endswith(' '): + check = 500 + else: + check = 207 self._test_htpasswd("plain", "%s:bepo" % user, ( - (user, "bepo", True), ("tmp", "bepo", False))) + (user, "bepo", True), ("tmp", "bepo", False)), check=check) + + def test_htpasswd_problem_user(self) -> None: + for user in ("tm*p", "tm?p"): + if not pathutils.path_supports_problematic_chars(self.colpath): + check = 500 + else: + check = 207 + self._test_htpasswd("plain", "%s:bepo" % user, ( + (user, "bepo", True), ("tmp", "bepo", False)), check=check) def test_htpasswd_whitespace_password(self) -> None: for password in (" bepo", "bepo ", " bepo "): diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 73176132..1ae28047 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -190,6 +190,26 @@ class TestMultiFileSystem(BaseTest): assert answer is not None assert "\r\nUID:%s\r\n" % uid in answer + @pytest.mark.skipif(not pathutils.path_is_collision_free_case_sensitive(tempfile.mkdtemp()), reason="TEMP is not case sensitive") + def test_collection_storage_dummy_case_sensitivity(self) -> None: + """Test collection storage case sensitivity.""" + + @pytest.mark.skipif(not pathutils.path_is_collision_free_no_short_filename(tempfile.mkdtemp()), reason="TEMP has short filename") + def test_collection_storage_dummy_no_short_filename(self) -> None: + """Test collection storage no short filename.""" + + @pytest.mark.skipif(not pathutils.path_supports_unicode(tempfile.mkdtemp()), reason="TEMP is not supporting unicode") + def test_collection_storage_dummy_no_support_of_unicode(self) -> None: + """Test collection storage no support of unicode.""" + + @pytest.mark.skipif(not pathutils.path_supports_trailing_whitespace(tempfile.mkdtemp()), reason="TEMP is not supporting trailing whitespace") + def test_collection_storage_dummy_no_support_of_trailing_whitespace(self) -> None: + """Test collection storage no support of trailing space.""" + + @pytest.mark.skipif(not pathutils.path_supports_problematic_chars(tempfile.mkdtemp()), reason="TEMP is not supporting problematic chars") + def test_collection_storage_dummy_no_support_of_problematic_chars(self) -> None: + """Test collection storage no support of problematic chars.""" + @pytest.mark.skipif(not pathutils.path_supports_symlink(tempfile.mkdtemp()), reason="TEMP is not supporting symlink") def test_collection_sharing_by_softlink(self) -> None: """Test collection sharing by softlink.""" From f2c75d974b22420474bc95a94f73e0eb93772085 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 18 Apr 2026 16:23:12 +0200 Subject: [PATCH 06/18] changelog: extension --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1af270..edd4c501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * Improve: `path_to_filesystem()` by pre-detection of collision-free file system * Adjustment: MKCOL/MKCALENDAR return now CONFLICT instead of BADREQUEST of file name collision * Improve: [auth] catch bcrypt>=5.0.0 enforced max password length early and support legacy "passlib" as well as "libpass" (rework 3.6.0, "packaging" not needed anymore) +* Improve: application will stop on startup if TEMP is provided but not existing or not writable +* Extension: tox with new optional test cases to test with LinuxOS vfat, hfsplus, ntfs filesystems +* Adjust: respond with 500 in case principal collection cannot be created (e.g. filesystem issues) ## 3.7.1 * Fix: share address book collection as birthday calendar not working on non-DEBUG level From fc579d54f6ee3c256c43cb58d9abce777fdc6a25 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 07:41:57 +0200 Subject: [PATCH 07/18] test: add filesystem vfat check (1st try) --- .github/workflows/test.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 273fd590..a68632dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,6 +67,24 @@ jobs: - name: Test with newest Python on latest Ubuntu run: tox -c pyproject.toml -e py + test-ubuntu-python-newest-with-vfat: + name: Test Ubuntu:latest Python:newest vfat + needs: [lint, test-ubuntu-python-newest] + strategy: + matrix: + os: [ubuntu-latest] + python-version: ['3.14'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install Test dependencies + run: pip install tox + - name: Test with newest Python on latest Ubuntu + run: tox -c pyproject.toml -e py_filesystem_vfat + test-python-32bit: name: Test Ubuntu:latest Python:32-bit needs: [lint, test-ubuntu-python-newest, integ-test] @@ -229,7 +247,7 @@ jobs: js-test: name: JS Type Check runs-on: ubuntu-latest - needs: test-ubuntu-python-newest + needs: [test-ubuntu-python-newest, test-ubuntu-python-newest-with-vfat] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 From 704287f4e960d9bd884fafa550210f31c3ff0bc9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 08:13:34 +0200 Subject: [PATCH 08/18] test: add ntfs/hfs+ tests, rearange dependencies and names --- .github/workflows/test.yml | 56 +++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a68632dd..8d7adbd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ on: jobs: test-ubuntu-python-newest: - name: Test Ubuntu:latest Python:newest + name: Test Python:newest Ubuntu:latest needs: lint strategy: matrix: @@ -48,7 +48,7 @@ jobs: run: tox -c pyproject.toml -e py test-ubuntu-python-newest-with-passlib: - name: Test Ubuntu:latest Python:newest passlib + name: Test passlib Python:newest Ubuntu:latest needs: [lint, test-ubuntu-python-newest] strategy: matrix: @@ -64,11 +64,11 @@ jobs: run: pip install tox - name: Switch back to passlib run: sed -i 's|libpass[^"]*|passlib|' pyproject.toml - - name: Test with newest Python on latest Ubuntu + - name: Test with newest Python on latest Ubuntu using passlib run: tox -c pyproject.toml -e py test-ubuntu-python-newest-with-vfat: - name: Test Ubuntu:latest Python:newest vfat + name: Test VFAT Python:newest Ubuntu:latest needs: [lint, test-ubuntu-python-newest] strategy: matrix: @@ -82,11 +82,47 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Test dependencies run: pip install tox - - name: Test with newest Python on latest Ubuntu + - name: Test with newest Python on latest Ubuntu using VFAT run: tox -c pyproject.toml -e py_filesystem_vfat + test-ubuntu-python-newest-with-ntfs: + name: Test NTFS Python:newest Ubuntu:latest + needs: [lint, test-ubuntu-python-newest] + strategy: + matrix: + os: [ubuntu-latest] + python-version: ['3.14'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install Test dependencies + run: pip install tox + - name: Test with newest Python on latest Ubuntu using NTFS + run: tox -c pyproject.toml -e py_filesystem_ntfs + + test-ubuntu-python-newest-with-hfsp: + name: Test HFS+ Python:newest Ubuntu:latest + needs: [lint, test-ubuntu-python-newest] + strategy: + matrix: + os: [ubuntu-latest] + python-version: ['3.14'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install Test dependencies + run: pip install tox + - name: Test with newest Python on latest Ubuntu using HFS+ + run: tox -c pyproject.toml -e py_filesystem_hfsp + test-python-32bit: - name: Test Ubuntu:latest Python:32-bit + name: Test 32-bit Python:3.11 Ubuntu:latest needs: [lint, test-ubuntu-python-newest, integ-test] strategy: matrix: @@ -120,13 +156,13 @@ jobs: python3 -m venv venv . venv/bin/activate pip install --upgrade pip - - name: Test 32-bit + - name: Test with 32-bit Python on latest Ubuntu run: | . venv/bin/activate tox -c /__w/Radicale/Radicale/pyproject.toml -e py test-ubuntu-python-oldest: - name: Test Ubuntu:latest Python:oldest + name: Test Python:oldest Ubuntu:latest needs: [lint, test-ubuntu-python-newest, test-python-32bit] strategy: matrix: @@ -145,7 +181,7 @@ jobs: test-otheros-python-newest: name: Test MacOS/Windows:latest Python:newest - needs: [lint, test-ubuntu-python-newest, test-python-32bit] + needs: [lint, test-ubuntu-python-newest, test-ubuntu-python-newest-with-ntfs, test-ubuntu-python-newest-with-hfsp, test-ubuntu-python-newest-with-vfat] strategy: matrix: os: [macos-latest, windows-latest] @@ -247,7 +283,7 @@ jobs: js-test: name: JS Type Check runs-on: ubuntu-latest - needs: [test-ubuntu-python-newest, test-ubuntu-python-newest-with-vfat] + needs: [test-ubuntu-python-newest] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 From bfb2b60c07b381014e3dd484075addc3f6d02a2b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 08:27:30 +0200 Subject: [PATCH 09/18] test: add new for hfs (supporting Ubuntu), fix umask for ntfs --- pyproject.toml | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e220f9c3..002387f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,8 @@ commands_post = [ ["rm", "/tmp/vfat.img"] ] -[tool.tox.env.py_filesystem_hfsp] +[tool.tox.env.py_filesystem_hfsplus] +# Fedora allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ] extras = ["test"] deps = [ @@ -141,6 +142,34 @@ commands_post = [ ["rm", "/tmp/hfsp.img"] ] +[tool.tox.env.py_filesystem_hfs] +# Ubuntu +allowlist_externals = [ "sudo", "dd", "mkfs.hfs", "mkdir", "rm", "rmdir", "chmod" ] +extras = ["test"] +deps = [ + "pytest" +] +commands_pre = [ + # create 64 MByte disk image + ["dd", "if=/dev/zero", "of=/tmp/hfsp.img", "bs=1M", "count=64"], + # create file system + ["mkfs.hfs", "/tmp/hfsp.img", "-v", "HFSP"], + # unconditionally create mount point + ["mkdir", "-p", "/tmp/hfsp"], + # mount image + ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp"], +] +setenv = { TEMP = "/tmp/hfsp" } +commands = [["pytest", "-r", "s", "."]] +commands_post = [ + # umount image + ["sudo", "/usr/bin/umount", "-d", "/tmp/hfsp"], + # remove mount point + ["rmdir", "/tmp/hfsp"], + # remove image + ["rm", "/tmp/hfsp.img"] +] + [tool.tox.env.py_filesystem_ntfs] allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ] extras = ["test"] @@ -155,7 +184,7 @@ commands_pre = [ # unconditionally create mount point ["mkdir", "-p", "/tmp/ntfs"], # mount image - ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/ntfs.img", "/tmp/ntfs"], + ["sudo", "/usr/bin/mount", "-o", "loop,umask=0000", "/tmp/ntfs.img", "/tmp/ntfs"], ] setenv = { TEMP = "/tmp/ntfs" } commands = [["pytest", "-r", "s", "."]] From 8dc035555d0858c0bd6ab3cb4aae704eff0f6688 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 08:28:14 +0200 Subject: [PATCH 10/18] test: align for HFS on Ubuntu --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d7adbd7..d02b6cae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -119,11 +119,11 @@ jobs: - name: Install Test dependencies run: pip install tox - name: Test with newest Python on latest Ubuntu using HFS+ - run: tox -c pyproject.toml -e py_filesystem_hfsp + run: tox -c pyproject.toml -e py_filesystem_hfs test-python-32bit: name: Test 32-bit Python:3.11 Ubuntu:latest - needs: [lint, test-ubuntu-python-newest, integ-test] + needs: [lint, test-ubuntu-python-newest, integ-test, test-ubuntu-python-newest-with-passlib] strategy: matrix: os: [ubuntu-latest] From 3d861a144938812b089ea864dd2493304224c6d8 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 08:46:47 +0200 Subject: [PATCH 11/18] test/hfs: install dependencies --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d02b6cae..2e3ea484 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,6 +116,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Update system + run: apt-get update + - name: Install hfsprogs + run: apt-get install -y hfsprogs - name: Install Test dependencies run: pip install tox - name: Test with newest Python on latest Ubuntu using HFS+ From 6638e15fd082731684becb304bc227213eb3910b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 08:54:57 +0200 Subject: [PATCH 12/18] storage/mtime resultion test: do not stop hard in case mtime cannot be set --- radicale/storage/multifilesystem/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 7deeaac7..65ea9b81 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -110,7 +110,7 @@ class Storage( except Exception as e: logger.warning("Storage item mtime resolution test not possible, cannot set utime on file: %r (%s)", path, e) os.remove(path) - raise + raise ValueError # do not raise a hard PermissionError logger.debug("Storage item mtime resoultion test set: %d ns" % MTIME_NS_TEST) mtime_ns = os.stat(path).st_mtime_ns - mtime_ns logger.debug("Storage item mtime resoultion test get: %d ns" % mtime_ns) From e457bc938bbf7b4b95debae337797a196ddd9c7e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 09:02:52 +0200 Subject: [PATCH 13/18] test/bugfix --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e3ea484..92f10830 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,9 +117,11 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Update system - run: apt-get update + run: | + apt-get update - name: Install hfsprogs - run: apt-get install -y hfsprogs + run: | + apt-get install -y hfsprogs - name: Install Test dependencies run: pip install tox - name: Test with newest Python on latest Ubuntu using HFS+ From 9fe8e3db57aeb3af902f5eab70dc162cb43e116e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 09:08:27 +0200 Subject: [PATCH 14/18] test/hfs: fix --- .github/workflows/test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92f10830..c422a499 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,12 +116,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Update system - run: | - apt-get update - name: Install hfsprogs run: | - apt-get install -y hfsprogs + sudo apt-get update + sudo apt-get install -y hfsprogs - name: Install Test dependencies run: pip install tox - name: Test with newest Python on latest Ubuntu using HFS+ From 6468c0031cdcb7113e20ec0a12a38ad4f0525154 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 09:24:16 +0200 Subject: [PATCH 15/18] test/hfs: remove dedicated ubuntu case --- pyproject.toml | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 002387f2..fddb1cb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,35 +129,7 @@ commands_pre = [ # unconditionally create mount point ["mkdir", "-p", "/tmp/hfsp"], # mount image - ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp"], -] -setenv = { TEMP = "/tmp/hfsp" } -commands = [["pytest", "-r", "s", "."]] -commands_post = [ - # umount image - ["sudo", "/usr/bin/umount", "-d", "/tmp/hfsp"], - # remove mount point - ["rmdir", "/tmp/hfsp"], - # remove image - ["rm", "/tmp/hfsp.img"] -] - -[tool.tox.env.py_filesystem_hfs] -# Ubuntu -allowlist_externals = [ "sudo", "dd", "mkfs.hfs", "mkdir", "rm", "rmdir", "chmod" ] -extras = ["test"] -deps = [ - "pytest" -] -commands_pre = [ - # create 64 MByte disk image - ["dd", "if=/dev/zero", "of=/tmp/hfsp.img", "bs=1M", "count=64"], - # create file system - ["mkfs.hfs", "/tmp/hfsp.img", "-v", "HFSP"], - # unconditionally create mount point - ["mkdir", "-p", "/tmp/hfsp"], - # mount image - ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp"], + ["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp", "-t", "hfsplus"], ] setenv = { TEMP = "/tmp/hfsp" } commands = [["pytest", "-r", "s", "."]] From c197a58841f92f4eb9b56a550210c2e9c9796aab Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 09:24:45 +0200 Subject: [PATCH 16/18] test/hfs: fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c422a499..0c7eef1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,7 +123,7 @@ jobs: - name: Install Test dependencies run: pip install tox - name: Test with newest Python on latest Ubuntu using HFS+ - run: tox -c pyproject.toml -e py_filesystem_hfs + run: tox -c pyproject.toml -e py_filesystem_hfsplus test-python-32bit: name: Test 32-bit Python:3.11 Ubuntu:latest From 3462695abe23321c26ec6696a814487d9e9a0657 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 09:40:45 +0200 Subject: [PATCH 17/18] test/hfs: remove, not supported on Ubuntu image --- .github/workflows/test.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c7eef1f..337bb135 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,28 +103,6 @@ jobs: - name: Test with newest Python on latest Ubuntu using NTFS run: tox -c pyproject.toml -e py_filesystem_ntfs - test-ubuntu-python-newest-with-hfsp: - name: Test HFS+ Python:newest Ubuntu:latest - needs: [lint, test-ubuntu-python-newest] - strategy: - matrix: - os: [ubuntu-latest] - python-version: ['3.14'] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - name: Install hfsprogs - run: | - sudo apt-get update - sudo apt-get install -y hfsprogs - - name: Install Test dependencies - run: pip install tox - - name: Test with newest Python on latest Ubuntu using HFS+ - run: tox -c pyproject.toml -e py_filesystem_hfsplus - test-python-32bit: name: Test 32-bit Python:3.11 Ubuntu:latest needs: [lint, test-ubuntu-python-newest, integ-test, test-ubuntu-python-newest-with-passlib] @@ -185,7 +163,7 @@ jobs: test-otheros-python-newest: name: Test MacOS/Windows:latest Python:newest - needs: [lint, test-ubuntu-python-newest, test-ubuntu-python-newest-with-ntfs, test-ubuntu-python-newest-with-hfsp, test-ubuntu-python-newest-with-vfat] + needs: [lint, test-ubuntu-python-newest, test-ubuntu-python-newest-with-ntfs, test-ubuntu-python-newest-with-vfat] strategy: matrix: os: [macos-latest, windows-latest] From a60ba7ae7f524c902b2f0a49490a913c33d1c727 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 19 Apr 2026 11:59:42 +0200 Subject: [PATCH 18/18] pathutils: change loglevel for detection --- radicale/pathutils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index 71ba4438..31d6d875 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -426,7 +426,7 @@ def path_is_collision_free_case_sensitive(path): # cleanup os.rmdir(test_dir_uc) os.rmdir(base_dir) - logger.debug("path_is_collision_free (case-sensitive): path=%r result=%s", path, result) + logger.trace("path_is_collision_free (case-sensitive): path=%r result=%s", path, result) return result @@ -450,7 +450,7 @@ def path_is_collision_free_no_short_filename(path): # cleanup os.rmdir(test_dir_long) os.rmdir(base_dir) - logger.debug("path_is_collision_free (no short-filename): path=%r result=%s", path, result) + logger.trace("path_is_collision_free (no short-filename): path=%r result=%s", path, result) return result @@ -474,7 +474,7 @@ def path_supports_unicode(path): os.rmdir(test_dir) # cleanup os.rmdir(base_dir) - logger.debug("path_supports_unicode: path=%r result=%s", path, result) + logger.trace("path_supports_unicode: path=%r result=%s", path, result) return result @@ -498,7 +498,7 @@ def path_supports_trailing_whitespace(path): os.rmdir(test_dir) # cleanup os.rmdir(base_dir) - logger.debug("path_supports_trailing_whitespace: path=%r result=%s", path, result) + logger.trace("path_supports_trailing_whitespace: path=%r result=%s", path, result) return result @@ -523,5 +523,5 @@ def path_supports_problematic_chars(path): os.rmdir(test_dir) # cleanup os.rmdir(base_dir) - logger.debug("path_supports_problematic chars: path=%r result=%s", path, result) + logger.trace("path_supports_problematic chars: path=%r result=%s", path, result) return result