mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-24 10:03:55 +08:00
* Keep temp-directory assets visible while their files exist Assets written to the temp directory were flagged as missing and dropped from GET /api/assets, even with the file sitting on disk. One list of directories was answering two different questions -- where the scanner looks for new files, and which files ComfyUI considers its own -- and temp belongs only in the second, so every temp reference was disowned by the prune that runs at startup and on POST /api/assets/prune. Ownership now covers temp. Discovery still does not: the temp directory is wiped before the scan runs, and assets written there are already registered with a hash, mime type and dimensions, so walking it would find nothing. Temp references are instead reconciled against the filesystem directly, so a temp file that really is gone is still retired rather than lingering as a broken entry. get_prefixes_for_root becomes get_scan_prefixes_for_root so the two questions are told apart by name rather than by comment. * Cover the unhashed temp asset in the reconciliation tests The existing temp tests all registered hashed assets, so they never exercised the path an unhashed asset takes when its file is gone: the orphaned rows are removed rather than kept as missing, exactly as under any other root.
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.assets.database.models import Asset, AssetReference, Base
|
|
from app.assets.database.queries.asset_reference import (
|
|
mark_references_missing_outside_prefixes,
|
|
)
|
|
from app.assets.scanner import (
|
|
collect_paths_for_roots,
|
|
get_owned_prefixes,
|
|
get_temp_prefixes,
|
|
sync_prefixes_with_filesystem,
|
|
)
|
|
from app.assets.services.file_utils import get_mtime_ns
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def autoclean_unit_test_assets():
|
|
"""Override parent autouse fixture - temp asset tests don't need server cleanup."""
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def session():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
with Session(engine) as sess:
|
|
yield sess
|
|
|
|
|
|
@pytest.fixture
|
|
def comfy_dirs():
|
|
with tempfile.TemporaryDirectory() as base:
|
|
dirs = {
|
|
name: Path(base) / name
|
|
for name in ("models", "input", "output", "temp", "elsewhere")
|
|
}
|
|
for d in dirs.values():
|
|
d.mkdir()
|
|
with (
|
|
patch("folder_paths.get_input_directory", return_value=str(dirs["input"])),
|
|
patch("folder_paths.get_output_directory", return_value=str(dirs["output"])),
|
|
patch("folder_paths.get_temp_directory", return_value=str(dirs["temp"])),
|
|
patch(
|
|
"app.assets.scanner.get_comfy_models_folders",
|
|
return_value=[("checkpoints", [str(dirs["models"])], set())],
|
|
),
|
|
):
|
|
yield dirs
|
|
|
|
|
|
def _write(directory: Path, name: str) -> str:
|
|
p = directory / name
|
|
p.write_bytes(b"\x00" * 100)
|
|
return str(p)
|
|
|
|
|
|
def _register(
|
|
session: Session,
|
|
file_path: str,
|
|
ref_id: str,
|
|
*,
|
|
mtime_ns: int,
|
|
asset_hash: str | None = "",
|
|
) -> None:
|
|
if asset_hash == "":
|
|
asset_hash = f"blake3:{ref_id}"
|
|
session.add(Asset(id=f"asset-{ref_id}", hash=asset_hash, size_bytes=100))
|
|
session.flush()
|
|
session.add(
|
|
AssetReference(
|
|
id=ref_id,
|
|
asset_id=f"asset-{ref_id}",
|
|
name=os.path.basename(file_path),
|
|
owner_id="",
|
|
file_path=file_path,
|
|
mtime_ns=mtime_ns,
|
|
)
|
|
)
|
|
session.flush()
|
|
|
|
|
|
def _mtime(path: str) -> int:
|
|
return get_mtime_ns(os.stat(path, follow_symlinks=True))
|
|
|
|
|
|
def test_owned_prefixes_include_temp(comfy_dirs):
|
|
owned = get_owned_prefixes()
|
|
assert str(comfy_dirs["temp"]) in owned, (
|
|
"temp must be owned, or the prune disowns assets whose files are present"
|
|
)
|
|
for name in ("models", "input", "output"):
|
|
assert str(comfy_dirs[name]) in owned, f"{name} must stay owned"
|
|
|
|
|
|
def test_discovery_does_not_walk_temp(comfy_dirs):
|
|
temp_file = _write(comfy_dirs["temp"], "preview.png")
|
|
output_file = _write(comfy_dirs["output"], "render.png")
|
|
|
|
with patch("app.assets.scanner.collect_models_files", return_value=[]):
|
|
paths = collect_paths_for_roots(("models", "input", "output"))
|
|
|
|
assert output_file in paths, "scan roots must still be walked"
|
|
assert temp_file not in paths, (
|
|
"temp is wiped before every scan, so walking it only ever finds nothing"
|
|
)
|
|
|
|
|
|
def test_prune_keeps_live_temp_reference(session, comfy_dirs):
|
|
temp_file = _write(comfy_dirs["temp"], "preview.png")
|
|
stray_file = _write(comfy_dirs["elsewhere"], "stray.png")
|
|
_register(session, temp_file, "temp-ref", mtime_ns=_mtime(temp_file))
|
|
_register(session, stray_file, "stray-ref", mtime_ns=_mtime(stray_file))
|
|
session.commit()
|
|
|
|
marked = mark_references_missing_outside_prefixes(session, get_owned_prefixes())
|
|
session.commit()
|
|
|
|
session.expire_all()
|
|
assert marked == 1, "only the reference outside every owned directory is disowned"
|
|
assert session.get(AssetReference, "temp-ref").is_missing is False, (
|
|
"a temp file on disk is not missing, however often the prune runs"
|
|
)
|
|
assert session.get(AssetReference, "stray-ref").is_missing is True, (
|
|
"owning temp must not stop the prune disowning files elsewhere"
|
|
)
|
|
|
|
|
|
def test_temp_sync_marks_deleted_file_missing(session, comfy_dirs):
|
|
temp_file = _write(comfy_dirs["temp"], "preview.png")
|
|
_register(session, temp_file, "temp-ref", mtime_ns=_mtime(temp_file))
|
|
session.commit()
|
|
os.remove(temp_file)
|
|
|
|
sync_prefixes_with_filesystem(session, get_temp_prefixes())
|
|
session.commit()
|
|
|
|
session.expire_all()
|
|
assert session.get(AssetReference, "temp-ref").is_missing is True, (
|
|
"nothing else stats temp, so this pass is what retires a wiped file"
|
|
)
|
|
|
|
|
|
def test_temp_sync_drops_unhashed_asset_whose_file_is_gone(session, comfy_dirs):
|
|
temp_file = _write(comfy_dirs["temp"], "preview.png")
|
|
_register(session, temp_file, "temp-ref", mtime_ns=_mtime(temp_file), asset_hash=None)
|
|
session.commit()
|
|
os.remove(temp_file)
|
|
|
|
sync_prefixes_with_filesystem(session, get_temp_prefixes())
|
|
session.commit()
|
|
|
|
session.expire_all()
|
|
assert session.get(AssetReference, "temp-ref") is None, (
|
|
"an unhashed asset with no surviving reference is retired, not kept as missing"
|
|
)
|
|
assert session.get(Asset, "asset-temp-ref") is None, (
|
|
"the orphaned asset row goes with its last reference"
|
|
)
|
|
|
|
|
|
def test_temp_sync_keeps_live_file(session, comfy_dirs):
|
|
temp_file = _write(comfy_dirs["temp"], "preview.png")
|
|
_register(session, temp_file, "temp-ref", mtime_ns=_mtime(temp_file))
|
|
session.commit()
|
|
|
|
sync_prefixes_with_filesystem(session, get_temp_prefixes())
|
|
session.commit()
|
|
|
|
session.expire_all()
|
|
ref = session.get(AssetReference, "temp-ref")
|
|
assert ref.is_missing is False, "the file is still there"
|
|
assert ref.needs_verify is False, "an unchanged file needs no re-verification"
|