Merge pull request #686 from willmiao/fix/model-extension-delete-rename

fix(model): preserve original extension on rename
This commit is contained in:
pixelpaws
2025-11-19 11:43:01 +08:00
committed by GitHub
2 changed files with 110 additions and 6 deletions

View File

@@ -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",
]
@@ -336,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:]

View File

@@ -184,6 +184,107 @@ 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_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"