Closes #543
This commit is contained in:
@@ -37,6 +37,7 @@ from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from .logging_config import get_logger
|
||||
from .media_folders import in_skipped_folder
|
||||
|
||||
try:
|
||||
from ..database.workflow_documents import DocumentReader, resolve_image_row
|
||||
@@ -298,7 +299,13 @@ class GalleryDiagnostics:
|
||||
try:
|
||||
image_files = []
|
||||
for ext in [".png", ".jpg", ".jpeg", ".webp"]:
|
||||
image_files.extend(Path(abs_path).rglob(f"*{ext}"))
|
||||
image_files.extend(
|
||||
found
|
||||
for found in Path(abs_path).rglob(f"*{ext}")
|
||||
if not in_skipped_folder(
|
||||
found.relative_to(abs_path).parts[:-1]
|
||||
)
|
||||
)
|
||||
self.logger.info(f" [IMG] Images found: {len(image_files)}")
|
||||
except Exception as e:
|
||||
self.logger.error(f" [FAIL] Error scanning: {e}")
|
||||
|
||||
+36
-2
@@ -6,16 +6,22 @@ inside an output root (``D:\\$RECYCLE.BIN`` when ``D:\\`` is a root,
|
||||
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.
|
||||
|
||||
``in_trash_below_roots`` is the check for a path someone names (#543): it
|
||||
resolves the path first, so a link or another spelling cannot get past it.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
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()
|
||||
``.Trashes`` or ``$RECYCLE.BIN``. Windows ignores a name's trailing dots
|
||||
and spaces, so ``$RECYCLE.BIN.`` is that folder; they are ignored here too."""
|
||||
folded = name.rstrip(" .").casefold()
|
||||
if folded in TRASH_FOLDER_NAMES:
|
||||
return True
|
||||
return folded.startswith(".trash-") and folded[7:].isdigit()
|
||||
@@ -26,6 +32,34 @@ def is_skipped_folder(name: str) -> bool:
|
||||
return name == PREVIEW_FOLDER or is_trash_folder(name)
|
||||
|
||||
|
||||
def in_trash_folder(relative: str) -> bool:
|
||||
"""Whether a root-relative file path lies in a trash folder (#543).
|
||||
|
||||
Takes either separator. Previews of it (``thumbnails/.Trash-1000/...``) count.
|
||||
"""
|
||||
folders = relative.replace("\\", "/").split("/")[:-1]
|
||||
return any(is_trash_folder(part) for part in folders)
|
||||
|
||||
|
||||
def in_trash_below_roots(path, roots) -> bool:
|
||||
"""Whether *path*, resolved, is in a trash folder below every root holding it.
|
||||
|
||||
A source named like a trash folder is still a source, so a file it holds
|
||||
with no trash folder below it is not in one. Paths compare resolved and
|
||||
normcased. A path no root visibly holds tests its whole path: a spelling
|
||||
difference must not fail open.
|
||||
"""
|
||||
real = os.path.normcase(os.path.realpath(path))
|
||||
below = []
|
||||
for root in roots:
|
||||
real_root = os.path.normcase(os.path.realpath(root))
|
||||
if real.startswith(os.path.join(real_root, "")):
|
||||
below.append(os.path.relpath(real, real_root))
|
||||
if not below:
|
||||
return in_trash_folder(real)
|
||||
return all(in_trash_folder(relative) for relative in below)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
+18
-4
@@ -6,7 +6,10 @@ Two rules hold for every backend:
|
||||
filesystem. Nothing is ever copied, unlinked or overwritten: a file that
|
||||
cannot be moved recoverably stays where it is, and ``TrashRefused`` says why.
|
||||
- **Never into a root.** A trash folder inside an output root would be listed
|
||||
as images, so it is refused (``trash-in-root``).
|
||||
as images, so it is refused (``trash-in-root``). The one exception (#543): for
|
||||
an output root that is exactly a mount root, that mount's own ``.Trash-$uid``
|
||||
and ``.Trash/$uid``, which every walker and media route ignores
|
||||
(``utils.media_folders``).
|
||||
|
||||
Linux and BSD use :class:`FreedesktopBackend`, the freedesktop.org Trash
|
||||
specification 1.0: the home trash (``$XDG_DATA_HOME/Trash``) for a file on the
|
||||
@@ -184,10 +187,21 @@ def _canonical(path: str) -> str:
|
||||
return os.path.normcase(os.path.realpath(path))
|
||||
|
||||
|
||||
def _check_roots(trash_dir: str, roots) -> None:
|
||||
def _check_roots(trash_dir: str, roots, top: str | None = None) -> None:
|
||||
"""Refuse a trash that a root would list, or that holds a root.
|
||||
|
||||
*top* is the mount a per-drive *trash_dir* was built from. A root that is
|
||||
exactly that mount may hold it, unless a link makes the trash really live
|
||||
somewhere else (#543). Any other root holding it refuses, also beside one.
|
||||
"""
|
||||
trash_key = _canonical(trash_dir)
|
||||
top_key = None if top is None else _canonical(top)
|
||||
unlinked = trash_key == os.path.normcase(trash_dir)
|
||||
for root in roots:
|
||||
if _inside(trash_key, _canonical(str(root))):
|
||||
root_key = _canonical(str(root))
|
||||
if _inside(root_key, trash_key):
|
||||
raise TrashRefused(TRASH_IN_ROOT)
|
||||
if _inside(trash_key, root_key) and not (unlinked and root_key == top_key):
|
||||
raise TrashRefused(TRASH_IN_ROOT)
|
||||
|
||||
|
||||
@@ -267,7 +281,7 @@ class FreedesktopBackend:
|
||||
candidates.append(os.path.join(shared, str(self._uid)))
|
||||
candidates.append(os.path.join(top, f".Trash-{self._uid}"))
|
||||
for trash_dir in candidates:
|
||||
_check_roots(trash_dir, roots)
|
||||
_check_roots(trash_dir, roots, top)
|
||||
if self._usable(trash_dir, create=create):
|
||||
label = f"{os.path.relpath(trash_dir, top)} on its drive"
|
||||
return trash_dir, top, label
|
||||
|
||||
Reference in New Issue
Block a user