mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-12 12:32:11 -03:00
fix(pending-delete): drop the staging container with its last batch (#1145)
Deleting a folder that holds models answered "A deletion is still pending inside this folder. Wait for the undo window to expire." even though nothing was staged and no window was running. `.lm-pending-delete` is only a container: the purge, the undo and the staging rollback all removed the batch dir inside it and left the container behind, and the folder manifest matched the directory *name* alone. Any folder where a model had ever been deleted with the undo window was therefore undeletable for good, and the same name match refused folder renames. * `staging_dir_has_content()` is the shared answer to "is there still a batch here?": an empty container is a leftover, a quarantined batch (`*.orphaned`) is preserved data and still counts, and an unreadable directory counts too — never delete over something that could not be listed. * The container is removed with its last batch (purge, undo, staging rollback, merged-loser cleanup). `rmdir` only ever succeeds on an empty directory, and the path is checked to really be a `.lm-pending-delete`, so a model folder is never at risk. * A manifest-less batch dir with nothing in it is a crash leftover between `makedirs` and the manifest write: it is dropped instead of quarantined, which would have left an empty container that blocks deletion just the same. Verified: purge, undo and the empty-manifest-less sweep all leave no container behind, the container survives while another batch is staged, and an empty one no longer blocks folder delete or rename while a real (or quarantined) batch still does. 3818 backend tests pass.
This commit is contained in:
@@ -2298,3 +2298,89 @@ async def test_symlink5_cross_volume_merge_then_one_undo_restores_everything(
|
||||
assert not (root_b / PENDING_DELETE_DIR_NAME / bid_b).exists()
|
||||
staging = root_a / PENDING_DELETE_DIR_NAME
|
||||
assert not staging.exists() or not any(staging.iterdir())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (n) the staging CONTAINER is dropped with its last batch (issue #1145)
|
||||
# ---------------------------------------------------------------------------
|
||||
async def test_n_purge_removes_the_empty_staging_container(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""`.lm-pending-delete` is a container, not a record of a pending delete.
|
||||
|
||||
Leaving it behind made every later folder delete (and rename) answer "A
|
||||
deletion is still pending inside this folder" forever, because the folder
|
||||
manifest only matched the directory name.
|
||||
"""
|
||||
root = tmp_path / "loras"
|
||||
root.mkdir()
|
||||
await _register_model_root(monkeypatch, lora_roots=[root])
|
||||
|
||||
service = await PendingDeleteService.get_instance()
|
||||
batch_id = await _stage_simple(service, root, "expired")
|
||||
|
||||
manifest_path = root / PENDING_DELETE_DIR_NAME / batch_id / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["expires_at"] = int(time.time()) - 10
|
||||
manifest_path.write_text(json.dumps(manifest))
|
||||
|
||||
await service.purge_expired()
|
||||
|
||||
assert not (root / PENDING_DELETE_DIR_NAME).exists()
|
||||
|
||||
|
||||
async def test_n_undo_removes_the_empty_staging_container(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
root = tmp_path / "loras"
|
||||
root.mkdir()
|
||||
await _register_model_root(monkeypatch, lora_roots=[root])
|
||||
|
||||
service = await PendingDeleteService.get_instance()
|
||||
batch_id = await _stage_simple(service, root, "restored")
|
||||
|
||||
await service.undo(batch_id)
|
||||
|
||||
assert (root / "restored.safetensors").exists()
|
||||
assert not (root / PENDING_DELETE_DIR_NAME).exists()
|
||||
|
||||
|
||||
async def test_n_container_survives_while_another_batch_is_staged(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""Only the last batch out drops the container."""
|
||||
root = tmp_path / "loras"
|
||||
root.mkdir()
|
||||
await _register_model_root(monkeypatch, lora_roots=[root])
|
||||
|
||||
service = await PendingDeleteService.get_instance()
|
||||
expired_id = await _stage_simple(service, root, "expired")
|
||||
fresh_id = await _stage_simple(service, root, "fresh")
|
||||
|
||||
manifest_path = root / PENDING_DELETE_DIR_NAME / expired_id / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["expires_at"] = int(time.time()) - 10
|
||||
manifest_path.write_text(json.dumps(manifest))
|
||||
|
||||
await service.purge_expired()
|
||||
|
||||
staging = root / PENDING_DELETE_DIR_NAME
|
||||
assert staging.is_dir()
|
||||
assert not (staging / expired_id).exists()
|
||||
assert (staging / fresh_id).exists()
|
||||
|
||||
|
||||
async def test_n_empty_manifestless_batch_is_dropped_not_quarantined(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""A crash between makedirs and the manifest leaves nothing to preserve."""
|
||||
root = tmp_path / "loras"
|
||||
root.mkdir()
|
||||
staging = root / PENDING_DELETE_DIR_NAME
|
||||
(staging / "batch1").mkdir(parents=True)
|
||||
await _register_model_root(monkeypatch, lora_roots=[root])
|
||||
|
||||
service = await PendingDeleteService.get_instance()
|
||||
await service.purge_expired(scan_roots=True)
|
||||
|
||||
assert not staging.exists()
|
||||
|
||||
Reference in New Issue
Block a user