From 963f6b13839696496429b59c61f2be0fa72ef889 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 19 Nov 2025 11:08:08 +0800 Subject: [PATCH 1/2] fix(model): preserve original extension on rename --- py/services/model_lifecycle_service.py | 13 ++-- .../services/test_model_lifecycle_service.py | 59 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/py/services/model_lifecycle_service.py b/py/services/model_lifecycle_service.py index 88995b5f..594c5f4b 100644 --- a/py/services/model_lifecycle_service.py +++ b/py/services/model_lifecycle_service.py @@ -234,16 +234,19 @@ class ModelLifecycleService: raise ValueError("Invalid characters in file name") target_dir = os.path.dirname(file_path) - old_file_name = os.path.splitext(os.path.basename(file_path))[0] - new_file_path = os.path.join(target_dir, f"{new_file_name}.safetensors").replace( - os.sep, "/" - ) + base_name = os.path.basename(file_path) + old_file_name, old_extension = os.path.splitext(base_name) + if not old_extension: + old_extension = ".safetensors" + new_file_path = os.path.join( + target_dir, f"{new_file_name}{old_extension}" + ).replace(os.sep, "/") if os.path.exists(new_file_path): raise ValueError("A file with this name already exists") patterns = [ - f"{old_file_name}.safetensors", + f"{old_file_name}{old_extension}", f"{old_file_name}.metadata.json", f"{old_file_name}.metadata.json.bak", ] diff --git a/tests/services/test_model_lifecycle_service.py b/tests/services/test_model_lifecycle_service.py index fa677e2b..65c63cf3 100644 --- a/tests/services/test_model_lifecycle_service.py +++ b/tests/services/test_model_lifecycle_service.py @@ -184,6 +184,65 @@ async def test_delete_model_updates_update_service(tmp_path: Path): assert update_service.calls == [("lora", 42, [1002])] +@pytest.mark.asyncio +async def test_rename_model_preserves_extension(tmp_path: Path): + old_name = "model" + old_extension = ".gguf" + new_name = "model-renamed" + + model_path = tmp_path / f"{old_name}{old_extension}" + model_path.write_bytes(b"model") + + preview_path = tmp_path / f"{old_name}.preview.png" + preview_path.write_bytes(b"preview") + + metadata_path = tmp_path / f"{old_name}.metadata.json" + metadata_payload = { + "file_name": old_name, + "file_path": model_path.as_posix(), + "preview_url": preview_path.as_posix(), + } + metadata_path.write_text(json.dumps(metadata_payload)) + + async def metadata_loader(path: str): + with open(path, "r", encoding="utf-8") as handle: + return json.load(handle) + + scanner = DummyScanner() + metadata_manager = PassthroughMetadataManager() + service = ModelLifecycleService( + scanner=scanner, + metadata_manager=metadata_manager, + metadata_loader=metadata_loader, + ) + + result = await service.rename_model( + file_path=model_path.as_posix(), + new_file_name=new_name, + ) + + expected_main = tmp_path / f"{new_name}{old_extension}" + expected_metadata = tmp_path / f"{new_name}.metadata.json" + expected_preview = tmp_path / f"{new_name}.preview.png" + + assert expected_main.exists() + assert not model_path.exists() + assert result["new_file_path"].endswith(f"{new_name}{old_extension}") + assert expected_preview.exists() + assert not preview_path.exists() + + saved_metadata = json.loads(expected_metadata.read_text()) + assert saved_metadata["file_name"] == new_name + assert saved_metadata["file_path"].endswith(f"{new_name}{old_extension}") + assert saved_metadata["preview_url"].endswith(f"{new_name}.preview.png") + + assert scanner.calls + old_call_path, new_call_path, payload = scanner.calls[0] + assert old_call_path.endswith(f"{old_name}{old_extension}") + assert new_call_path.endswith(f"{new_name}{old_extension}") + assert payload["file_name"] == new_name + + @pytest.mark.asyncio async def test_delete_model_removes_gguf_file(tmp_path: Path): model_path = tmp_path / "model.gguf" From 51b5261f4010423335ad2491721fb8fc034602cf Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 19 Nov 2025 11:20:09 +0800 Subject: [PATCH 2/2] fix(model): align rename extension detection --- py/services/model_lifecycle_service.py | 2 +- .../services/test_model_lifecycle_service.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/py/services/model_lifecycle_service.py b/py/services/model_lifecycle_service.py index 594c5f4b..0e0a2def 100644 --- a/py/services/model_lifecycle_service.py +++ b/py/services/model_lifecycle_service.py @@ -339,7 +339,7 @@ class ModelLifecycleService: return suffix basename = os.path.basename(filename) - dot_index = basename.find(".") + dot_index = basename.rfind(".") if dot_index != -1: return basename[dot_index:] diff --git a/tests/services/test_model_lifecycle_service.py b/tests/services/test_model_lifecycle_service.py index 65c63cf3..4817745c 100644 --- a/tests/services/test_model_lifecycle_service.py +++ b/tests/services/test_model_lifecycle_service.py @@ -243,6 +243,48 @@ async def test_rename_model_preserves_extension(tmp_path: Path): assert payload["file_name"] == new_name +@pytest.mark.asyncio +async def test_rename_model_with_dotted_basename(tmp_path: Path): + old_name = "model.v1" + old_extension = ".gguf" + new_name = "renamed-model" + + model_path = tmp_path / f"{old_name}{old_extension}" + model_path.write_bytes(b"content") + + metadata_path = tmp_path / f"{old_name}.metadata.json" + metadata_payload = { + "file_name": old_name, + "file_path": model_path.as_posix(), + } + metadata_path.write_text(json.dumps(metadata_payload)) + + async def metadata_loader(path: str): + with open(path, "r", encoding="utf-8") as handle: + return json.load(handle) + + scanner = DummyScanner() + metadata_manager = PassthroughMetadataManager() + service = ModelLifecycleService( + scanner=scanner, + metadata_manager=metadata_manager, + metadata_loader=metadata_loader, + ) + + result = await service.rename_model( + file_path=model_path.as_posix(), + new_file_name=new_name, + ) + + expected_main = tmp_path / f"{new_name}{old_extension}" + assert expected_main.exists() + assert result["new_file_path"] == expected_main.as_posix() + assert any(p.endswith(f"{new_name}{old_extension}") for p in result["renamed_files"]) + + saved_metadata = json.loads((tmp_path / f"{new_name}.metadata.json").read_text()) + assert saved_metadata["file_name"] == new_name + assert saved_metadata["file_path"].endswith(f"{new_name}{old_extension}") + @pytest.mark.asyncio async def test_delete_model_removes_gguf_file(tmp_path: Path): model_path = tmp_path / "model.gguf"