Load Media refuses unreadable files in plain words; gallery delete refuses odd names; stale tests updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9uCRvqd1GDx6PZDZLyovu
131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""What ComfyUI has in its temp directory, and what it has saved in its output directory.
|
|
|
|
Previews and other transient outputs land in temp, and they are wiped when the
|
|
server restarts -- so "where did that go?" has an answer for as long as the
|
|
server has been up, and this is where it is. Saved videos and pictures land in
|
|
output and stay, which makes that the gallery: the same listing, and the one
|
|
thing temp does not need, a way to delete.
|
|
|
|
Only media, and only what can be shown: a temp directory also fills with
|
|
whatever any node felt like writing, and a browser listing 400 .pt files is not
|
|
a media bin.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
#: What ComfyUI serves through /view and a browser can draw.
|
|
KINDS = {
|
|
"image": {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"},
|
|
"video": {".mp4", ".webm", ".mov", ".mkv"},
|
|
"audio": {".wav", ".mp3", ".flac", ".ogg"},
|
|
}
|
|
|
|
#: A long session can leave thousands. The window shows the newest; the rest are
|
|
#: on disk and nobody scrolls to them.
|
|
MAX_FILES = 500
|
|
|
|
|
|
def kind_of(name: str) -> str | None:
|
|
ext = os.path.splitext(name)[1].lower()
|
|
for kind, exts in KINDS.items():
|
|
if ext in exts:
|
|
return kind
|
|
return None
|
|
|
|
|
|
def temp_dir() -> str | None:
|
|
"""ComfyUI's temp directory, or None when ComfyUI is not here to ask."""
|
|
try:
|
|
import folder_paths
|
|
return folder_paths.get_temp_directory()
|
|
except Exception: # noqa: BLE001
|
|
return None
|
|
|
|
|
|
def output_dir() -> str | None:
|
|
"""ComfyUI's output directory, or None when ComfyUI is not here to ask."""
|
|
try:
|
|
import folder_paths
|
|
return folder_paths.get_output_directory()
|
|
except Exception: # noqa: BLE001
|
|
return None
|
|
|
|
|
|
def base_of(where: str) -> str | None:
|
|
"""The folder `where` ("temp" or "output") names; anything else names nothing."""
|
|
return {"temp": temp_dir, "output": output_dir}.get(where, lambda: None)()
|
|
|
|
|
|
def listing(limit: int = MAX_FILES, where: str = "temp") -> dict:
|
|
"""`{files, path, detail}`, newest first.
|
|
|
|
`detail` carries the reason there is nothing rather than leaving an empty
|
|
list to be read as "nothing was made" -- which is a different thing from
|
|
"this is not a ComfyUI install" and from "the directory was wiped".
|
|
"""
|
|
limit = max(1, min(int(limit or MAX_FILES), MAX_FILES))
|
|
saved = where == "output"
|
|
base = base_of(where)
|
|
if not base:
|
|
return {"files": [], "path": None,
|
|
"detail": f"ComfyUI is not here to ask where its {'saved' if saved else 'temp'} files go."}
|
|
if not os.path.isdir(base):
|
|
return {"files": [], "path": base,
|
|
"detail": f"The {'output' if saved else 'temp'} directory does not exist yet. It is made when "
|
|
"something is first written to it."}
|
|
|
|
found: list[dict] = []
|
|
for root, _dirs, names in os.walk(base):
|
|
for name in names:
|
|
kind = kind_of(name)
|
|
if not kind:
|
|
continue
|
|
full = os.path.join(root, name)
|
|
try:
|
|
stat = os.stat(full)
|
|
except OSError:
|
|
continue # vanished between walk and stat
|
|
sub = os.path.relpath(root, base)
|
|
found.append({
|
|
"filename": name,
|
|
"subfolder": "" if sub == "." else sub.replace(os.sep, "/"),
|
|
"kind": kind,
|
|
"size": stat.st_size,
|
|
"mtime": stat.st_mtime,
|
|
})
|
|
|
|
found.sort(key=lambda f: f.get("mtime", 0), reverse=True)
|
|
detail = "" if found else ("Nothing saved yet. A saved video or picture lands here." if saved
|
|
else "Nothing here. Temp files are wiped when ComfyUI restarts.")
|
|
return {"files": found[:limit], "path": base, "detail": detail}
|
|
|
|
|
|
def delete(filename, subfolder="", where: str = "output") -> dict:
|
|
"""Remove one media file from `where`. `{deleted: True}`, or `{deleted: False, detail}` saying why not.
|
|
|
|
The name is resolved the way /view resolves it and must stay inside that folder; and only
|
|
what the listing would show can go, so this cannot be turned on a model, a workflow or any
|
|
other file that happens to live next to the pictures.
|
|
"""
|
|
base = base_of(where)
|
|
if not base or not isinstance(filename, str) or not filename or not kind_of(filename):
|
|
return {"deleted": False, "detail": "Name a saved picture, video or sound."}
|
|
base = os.path.realpath(base)
|
|
try:
|
|
full = os.path.realpath(os.path.join(base, subfolder if isinstance(subfolder, str) else "",
|
|
os.path.basename(filename)))
|
|
inside = os.path.commonpath((full, base)) == base and full != base
|
|
except ValueError: # a NUL byte in a name, or another drive
|
|
inside = False
|
|
if not inside:
|
|
return {"deleted": False, "detail": "That file is not in this folder."}
|
|
try:
|
|
os.remove(full)
|
|
except FileNotFoundError:
|
|
return {"deleted": False, "detail": "That file is already gone."}
|
|
except OSError as err:
|
|
return {"deleted": False, "detail": f"Could not delete it: {err.strerror or err}"}
|
|
return {"deleted": True}
|