diff --git a/src/openflexure_microscope_server/utilities.py b/src/openflexure_microscope_server/utilities.py index 2f6c90a4..a13cf19d 100644 --- a/src/openflexure_microscope_server/utilities.py +++ b/src/openflexure_microscope_server/utilities.py @@ -164,7 +164,9 @@ def make_path_safe(unsafe_path_string: str) -> str: else: sanitised = unsafe_character_pattern.sub("_", sanitised) - sanitised_components.append(_sanitise_reserved(sanitised)) + sanitised_components.append( + _sanitise_reserved(sanitised, is_filename=False) + ) else: sanitised_components.append(component) @@ -194,20 +196,26 @@ def make_name_safe(unsafe_name_string: str) -> str: return _sanitise_reserved(name) -def _sanitise_reserved(name: str) -> str: +def _sanitise_reserved(name: str, is_filename: bool = True) -> str: """Check a name against Windows reserved names. :param name: The component to check. - :returns: The sanitised component. + :param is_filename: Whether the component is a filename. + If True, names will be forced into lowercase. + :returns: The sanitised component, in lower case. """ # Check for Windows reserved names. # These are reserved even with an extension (e.g. NUL.txt). # We check the part before the first dot. base_name = name.split(".", maxsplit=1)[0].upper() if base_name in _WINDOWS_RESERVED_NAMES: - return f"{name}_" + return f"{name.lower()}_" if is_filename else f"{name}_" - return name + # We force names to be lowercase to avoid issues on + # Windows where files with the same name + # but different case can cause problems when checking if + # a file already exists. + return name.lower() if is_filename else name class VersionData(BaseModel): diff --git a/tests/unit_tests/test_scan_directories.py b/tests/unit_tests/test_scan_directories.py index 603587a3..80121a32 100644 --- a/tests/unit_tests/test_scan_directories.py +++ b/tests/unit_tests/test_scan_directories.py @@ -247,6 +247,36 @@ def test_scan_sequence_and_listing(caplog): assert len(caplog.records) == 0 +def test_scan_sequence_case_sensitive(): + """Check created scans are added in order and listed correctly, even when cases are different.""" + _clear_scan_dir() + scan_dir_manager = ScanDirectoryManager(BASE_SCAN_DIR) + + # Create some scan data and mark it as successful to get an end date. + scan_data = fake_active_scan_data() + scan_data.set_final_data(result="Success") + # Make 4 scans + scan_dir = scan_dir_manager.new_scan_dir("fake_scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("FAKE_SCAN") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("Fake_Scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("FAKE_scan") + scan_dir.save_scan_data(scan_data) + scan_dir = scan_dir_manager.new_scan_dir("fAkE_sCaN") + scan_dir.save_scan_data(scan_data) + + # Check they exist and are numbered sequentially + all_scans = scan_dir_manager.all_scans + assert_unique_of_length(all_scans, 5) + assert "fake_scan_0001" in all_scans + assert "fake_scan_0002" in all_scans + assert "fake_scan_0003" in all_scans + assert "fake_scan_0004" in all_scans + assert "fake_scan_0005" in all_scans + + def test_scan_name_non_sequential(): """Check new scan has the correct name if the directories are not sequential.""" _clear_scan_dir() diff --git a/tests/unit_tests/test_utilities.py b/tests/unit_tests/test_utilities.py index 52b94fad..dbff78e4 100644 --- a/tests/unit_tests/test_utilities.py +++ b/tests/unit_tests/test_utilities.py @@ -17,6 +17,7 @@ def test_make_name_safe_basic(): assert make_name_safe("name with spaces") == "name_with_spaces" assert make_name_safe("name/with/slashes") == "name_with_slashes" assert make_name_safe("name\\with\\backslashes") == "name_with_backslashes" + assert make_name_safe("sPoNgEmoCk") == "spongemock" def test_make_name_safe_trailing_chars(): @@ -34,21 +35,25 @@ def test_make_name_safe_trailing_chars(): def test_make_name_safe_reserved_names(name): """Test Windows reserved names.""" # Base name should be sanitized - assert make_name_safe(name) == f"{name}_" + assert make_name_safe(name) == f"{name.lower()}_" # Case-insensitive assert make_name_safe(name.lower()) == f"{name.lower()}_" # With extension - assert make_name_safe(f"{name}.txt") == f"{name}.txt_" + assert make_name_safe(f"{name}.txt") == f"{name.lower()}.txt_" # Multiple extensions - assert make_name_safe(f"{name}.tar.gz") == f"{name}.tar.gz_" + assert make_name_safe(f"{name}.tar.gz") == f"{name.lower()}.tar.gz_" def test_make_name_safe_reserved_names_false_positives(): """Test that names containing but not equal to reserved names are safe.""" - assert make_name_safe("CONSTANT") == "CONSTANT" - assert make_name_safe("CON2") == "CON2" - assert make_name_safe("ICON") == "ICON" - assert make_name_safe("CHILLI CON CARNE") == "CHILLI_CON_CARNE" + assert make_name_safe("CONSTANT") == "constant" + assert make_name_safe("CON2") == "con2" + assert make_name_safe("ICON") == "icon" + assert make_name_safe("icon") == "icon" + assert make_name_safe("iCon") == "icon" + assert make_name_safe("iCOn") == "icon" + assert make_name_safe("icOn") == "icon" + assert make_name_safe("CHILLI CON CARNE") == "chilli_con_carne" def test_make_path_safe_basic():