Merge remote-tracking branch 'origin/main' into feat/499-macos-trash

# Conflicts:
#	TESTING.md
#	py/api/image_delete.py
#	utils/trash.py
This commit is contained in:
Vito Sansevero
2026-10-29 16:42:25 -07:00
54 changed files with 3592 additions and 193 deletions
+6 -4
View File
@@ -45,6 +45,7 @@ from .foreign_runs import (
store_foreign,
)
from .hashing import generate_prompt_hash
from .media_folders import in_skipped_folder
from .metadata_extractor import ComfyUIMetadataExtractor
from .parallel import note_refusal, refused_at_shutdown
from .logging_config import get_logger
@@ -428,11 +429,12 @@ class ImageGenerationHandler(FileSystemEventHandler):
Returns:
True if the file has a supported image extension and is not
inside a thumbnails directory, False otherwise
inside a thumbnails or trash folder, False otherwise
"""
# Skip files in thumbnails directory - those are derivatives, not
# generated images
if "/thumbnails/" in filepath or "\\thumbnails\\" in filepath:
# Previews are derivatives, and a trash folder inside an output
# folder holds thrown-away files (#501): neither is a new output.
folders = filepath.replace("\\", "/").split("/")[:-1]
if in_skipped_folder(folders):
return False
return filepath.lower().endswith(self.supported_extensions)
+31
View File
@@ -0,0 +1,31 @@
"""The folders no media walker lists below an output root (#501).
``thumbnails`` holds PromptManager's own previews. A trash folder that sits
inside an output root (``D:\\$RECYCLE.BIN`` when ``D:\\`` is a root,
``.Trash-1000`` at a mount point, ``.Trashes`` on a macOS volume) holds files
the user threw away: they are not outputs, so they are never listed, scanned,
linked, tagged or previewed. Every walker asks these predicates instead of
testing names itself; only folders *below* a root are tested.
"""
TRASH_FOLDER_NAMES = frozenset({".trash", ".trashes", "$recycle.bin"})
PREVIEW_FOLDER = "thumbnails"
def is_trash_folder(name: str) -> bool:
"""A trash folder name, in any case: ``.Trash``, ``.Trash-<uid>``,
``.Trashes`` or ``$RECYCLE.BIN``."""
folded = name.casefold()
if folded in TRASH_FOLDER_NAMES:
return True
return folded.startswith(".trash-") and folded[7:].isdigit()
def is_skipped_folder(name: str) -> bool:
"""A folder no walker descends: ``thumbnails`` (exact) or a trash folder."""
return name == PREVIEW_FOLDER or is_trash_folder(name)
def in_skipped_folder(parts) -> bool:
"""Whether any of the folder names *parts* is skipped."""
return any(is_skipped_folder(part) for part in parts)
+19 -7
View File
@@ -16,7 +16,9 @@ same mount, else the file's mount's ``.Trash/$uid`` (only under a real, sticky
``renameat2(RENAME_NOREPLACE)``; without that call the move is refused.
macOS uses :class:`utils.trash_macos.MacTrashBackend` (#499): NSFileManager's
``trashItemAtURL`` through ctypes, after refusing what it cannot show to be
recoverable. Windows has no backend yet (:class:`UnavailableBackend`, #500).
recoverable. Windows never uses the Recycle Bin (#500: files over its capacity
are deleted permanently, undetectably), so it answers ``windows-recycle``
through :class:`UnavailableBackend`.
``BACKEND`` is the one replaceable seam; tests install a fake.
"""
@@ -39,6 +41,8 @@ LOCKED = "locked"
FAILED = "failed"
NO_PERMISSION = "no-permission"
CLOUD_SYNC = "cloud-sync"
# Windows never uses the Recycle Bin (#500): it can delete permanently.
WINDOWS_RECYCLE = "windows-recycle"
CODES = frozenset(
{
NO_TRASH,
@@ -49,6 +53,7 @@ CODES = frozenset(
FAILED,
NO_PERMISSION,
CLOUD_SYNC,
WINDOWS_RECYCLE,
}
)
@@ -204,13 +209,16 @@ def _owned_folder(path: str, uid: int, *, create: bool) -> bool:
class UnavailableBackend:
"""No trash on this system: every move is refused, nothing is touched."""
"""No trash on this system: every move is refused with *code*, untouched."""
def __init__(self, code: str = NO_TRASH):
self.code = code
def available(self, path: str, roots) -> str | None:
return NO_TRASH
return self.code
def move(self, path: str, roots) -> str:
raise TrashRefused(NO_TRASH)
raise TrashRefused(self.code)
class FreedesktopBackend:
@@ -372,17 +380,21 @@ class FreedesktopBackend:
def default_backend(platform: str = sys.platform):
"""The backend for *platform*; Windows has none yet (#500).
"""The backend for *platform* (#480, #499, #500).
macOS gets :class:`utils.trash_macos.MacTrashBackend`, which loads its
Foundation bridge only on first use (#499).
Foundation bridge only on first use (#499). Windows never uses the Recycle
Bin (#500).
"""
if platform == "darwin":
from . import trash_macos
return trash_macos.MacTrashBackend()
if platform == "win32" or platform.startswith("cygwin"):
return UnavailableBackend()
# #500, ruling (a): Windows deletes files larger than the Recycle Bin's
# capacity permanently, and IFileOperation's PreDeleteItem guard cannot
# tell (native probe runs 40951037575, 40953347904).
return UnavailableBackend(WINDOWS_RECYCLE)
return FreedesktopBackend()