From a6d7fcf1b9061c7421a05671a0a21c9b4163040d Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 14 Apr 2026 20:43:30 +0200 Subject: [PATCH] storage/detect+test file systems with collision: fix to catch HFS+ (MacOS) --- radicale/pathutils.py | 38 ++++++++++---------- radicale/storage/multifilesystem/__init__.py | 9 +++-- radicale/tests/test_storage.py | 18 +++++----- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index c5756992..36a66461 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -406,19 +406,16 @@ def path_supports_symlink(path): return result -def path_is_collision_free(path): - """Check whether path supports case colliding-free entries.""" +def path_is_collision_free_case_sensitive(path): + # Test: case sensitive if not os.path.isdir(path): raise ValueError("%r is not a path" % (path)) - - result = True - - # Test 1: case sensitive base_dir = tempfile.mkdtemp(dir=path) test_dir = "TESTDIR" test_dir_uc = os.path.join(base_dir, test_dir.upper()) test_dir_lc = os.path.join(base_dir, test_dir.lower()) os.mkdir(test_dir_uc) + result = True try: os.mkdir(test_dir_lc) except FileExistsError: @@ -426,20 +423,23 @@ def path_is_collision_free(path): else: # cleanup os.rmdir(test_dir_lc) - finally: - # cleanup - os.rmdir(test_dir_uc) - if not result: - # early exit - os.rmdir(base_dir) - logger.trace("path_is_collision_free: path=%r result=%s", path, result) - return result + # cleanup + os.rmdir(test_dir_uc) + os.rmdir(base_dir) + logger.debug("path_is_collision_free (case-sensitive): path=%r result=%s", path, result) + return result - # Test 2: short filename + +def path_is_collision_free_no_short_filename(path): + """Check whether path supports short-filename collision-free entries.""" + if not os.path.isdir(path): + raise ValueError("%r is not a path" % (path)) + base_dir = tempfile.mkdtemp(dir=path) test_dir = "TESTDIRLONG" test_dir_long = os.path.join(base_dir, test_dir) test_dir_short = os.path.join(base_dir, test_dir[:6] + "~1") os.mkdir(test_dir_long) + result = True try: os.mkdir(test_dir_short) except FileExistsError: @@ -447,10 +447,8 @@ def path_is_collision_free(path): else: # cleanup os.rmdir(test_dir_short) - finally: - # cleanup - os.rmdir(test_dir_long) - # final exit + # cleanup + os.rmdir(test_dir_long) os.rmdir(base_dir) - logger.trace("path_is_collision_free: path=%r result=%s", path, result) + logger.debug("path_is_collision_free (no short-filename): path=%r result=%s", path, result) return result diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 10944913..7e6ead8e 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -173,8 +173,13 @@ class Storage( self._makedirs_synced(self._get_collection_root_folder()) logger.info("Storage location subfolder permissions: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder())) logger.info("Storage location subfolder softlink support: %s", pathutils.path_supports_symlink(self._get_collection_root_folder())) - self._filesystem_root_folder_is_collision_free = pathutils.path_is_collision_free(self._get_collection_root_folder()) - logger.info("Storage location subfolder is collision free: %s", self._filesystem_root_folder_is_collision_free) + filesystem_root_folder_is_collision_free_case_sensitive = pathutils.path_is_collision_free_case_sensitive(self._get_collection_root_folder()) + filesystem_root_folder_is_collision_free_no_short_filename = pathutils.path_is_collision_free_no_short_filename(self._get_collection_root_folder()) + self._filesystem_root_folder_is_collision_free = filesystem_root_folder_is_collision_free_case_sensitive and filesystem_root_folder_is_collision_free_no_short_filename + logger.info("Storage location subfolder is collision free: %s (case-sensitive=%s no-short-filename=%s)", + 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 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) diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 7dccb941..3509829b 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -222,7 +222,7 @@ class TestMultiFileSystem(BaseTest): """Test for colliding files on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) - fs_colliding_free = pathutils.path_is_collision_free(tempfile.mkdtemp()) + fs_collision_free = pathutils.path_is_collision_free_case_sensitive(tempfile.mkdtemp()) file_item = "EvEnT1.iCs" path_coll = "/calendar.ics/" self.mkcalendar(path_coll) @@ -232,11 +232,11 @@ class TestMultiFileSystem(BaseTest): path_lc = os.path.join(path_coll, file_item.lower()) self.put(path_item, event) self.put(path_uc, event, check=409) - if not fs_colliding_free: + if not fs_collision_free: logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 self.put(path_lc, event, check=409) - if not fs_colliding_free: + if not fs_collision_free: logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 2 @@ -244,16 +244,16 @@ class TestMultiFileSystem(BaseTest): """Test for colliding dirs on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) - fs_colliding_free = pathutils.path_is_collision_free(tempfile.mkdtemp()) + fs_collision_free = pathutils.path_is_collision_free_case_sensitive(tempfile.mkdtemp()) path_coll = "/CaLeNdAr.ics/" self.mkcalendar(path_coll) - if fs_colliding_free: + if fs_collision_free: self.mkcalendar(path_coll.lower(), check=201) else: self.mkcalendar(path_coll.lower(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 - if fs_colliding_free: + if fs_collision_free: self.mkcalendar(path_coll.upper(), check=201) else: self.mkcalendar(path_coll.upper(), check=409) @@ -264,17 +264,17 @@ class TestMultiFileSystem(BaseTest): """Test for colliding dirs (shortname) on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) - fs_colliding_free = pathutils.path_is_collision_free(tempfile.mkdtemp()) + fs_collision_free = pathutils.path_is_collision_free_no_short_filename(tempfile.mkdtemp()) path_coll = "/calendarlongname.ics/" path_coll_short = "/calend~1.ics/" self.mkcalendar(path_coll) - if fs_colliding_free: + if fs_collision_free: self.mkcalendar(path_coll_short.lower(), check=201) else: self.mkcalendar(path_coll_short.lower(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 - if fs_colliding_free: + if fs_collision_free: self.mkcalendar(path_coll_short.upper(), check=201) else: self.mkcalendar(path_coll_short.upper(), check=409)