* feat(trash): a freedesktop trash backend that renames without replacing or refuses (#480) * feat(images): remove a file's records and hide it with a removal mark (#480) Deletes every record of one file under every recorded spelling (image links, tags, auto-tag marks, pending review items, category, collection memberships, favorite) in one transaction. A Needs review item that is kept keeps its annotation rows (ON DELETE RESTRICT). The image_removals table hides an exact file (locator, size, mtime_ns) until it changes; linking skips a marked file, and an explicit tag, category, favorite or collection write shows it again. * feat(images): review and apply Delete images as a reviewed plan job (#480) POST /prompt_manager/images/delete/preview reviews both modes; the maintenance apply takes {token, mode}. Library mode removes the records and writes the removal mark; trash mode moves the file first and removes its records only after a successful move. The workspace listing, the output scan, the duplicate find and Tag output images skip marked files. * feat(images): Delete… in the Images selection bar, inspector and viewer (#480) The concept's Remove links… place, D159: one review per selection that asks each time (nothing preselected) whether to remove the files from the library only or also move them to the trash. The browser test server uses a fake trash. * test(images): portable delete tests, the native no-trash proof, D159 in TESTING.md (#480) * perf(images): cache the removal marks so a listing page reads no extra table (#480) The marks reload only when this process writes or clears one, or a restore replaces the database. The route snapshot gains the preview route. * test(images): the full inspector specs expect D159's Delete… in the panel and the viewer (#480) * fix(images): the delete review starts nothing without a chosen mode, and dims its primary until then (#480) An aria-disabled primary ignores every activation, a chosen one starts once, and the apply helper refuses a missing mode. Tests cover a forced click, a double click, Enter and Space on the primary, Enter in the dialog and a submit event, and the server refusing an apply without an exact mode.
53 lines
1.5 KiB
Python
53 lines
1.5 KiB
Python
"""A fake system trash for tests (#480): renames into a folder outside every root.
|
|
|
|
``install(folder)`` replaces ``utils.trash.BACKEND`` and returns the fake;
|
|
``uninstall`` puts the real one back. ``refuse[path] = code`` refuses one
|
|
file with that code. The fake never copies or unlinks; it records each call.
|
|
The disposable browser server installs it when ``PM_TEST_FAKE_TRASH=1``, so
|
|
no test touches a real trash.
|
|
"""
|
|
|
|
import os
|
|
|
|
from utils import trash
|
|
|
|
|
|
class FakeTrash:
|
|
def __init__(self, folder):
|
|
self.folder = str(folder)
|
|
os.makedirs(self.folder, exist_ok=True)
|
|
self.refuse = {}
|
|
self.moved = [] # (source, destination)
|
|
self.calls = []
|
|
|
|
def available(self, path, roots):
|
|
self.calls.append(("available", str(path)))
|
|
return self.refuse.get(str(path))
|
|
|
|
def move(self, path, roots):
|
|
self.calls.append(("move", str(path)))
|
|
code = self.refuse.get(str(path))
|
|
if code:
|
|
raise trash.TrashRefused(code)
|
|
destination = os.path.join(
|
|
self.folder, f"{len(self.moved)}-{os.path.basename(path)}"
|
|
)
|
|
if os.path.lexists(destination):
|
|
raise trash.TrashRefused(trash.FAILED)
|
|
os.rename(path, destination)
|
|
self.moved.append((str(path), destination))
|
|
return "fake trash"
|
|
|
|
|
|
_ORIGINAL = trash.BACKEND
|
|
|
|
|
|
def install(folder):
|
|
fake = FakeTrash(folder)
|
|
trash.BACKEND = fake
|
|
return fake
|
|
|
|
|
|
def uninstall():
|
|
trash.BACKEND = _ORIGINAL
|