fix(trash): allow the mount's own trash when the output folder is a mount point (#543)
An output root that is exactly a mount root may hold that mount's .Trash-$uid and .Trash/$uid, with the spec's checks intact. Any other trash inside a root stays refused, and so does a root inside the trash. Media routes no longer serve files in trash folders, and a review with nothing to move shows a neutral disabled primary.
This commit is contained in:
+45
-4
@@ -66,7 +66,11 @@ try:
|
||||
from ...database.models import normalize_image_path
|
||||
from ...database.run_attribution import recorded_output_lookup
|
||||
from ...utils.card_seed import card_seed, header_params, parsed_positive
|
||||
from ...utils.media_folders import in_skipped_folder, is_skipped_folder
|
||||
from ...utils.media_folders import (
|
||||
in_skipped_folder,
|
||||
in_trash_folder,
|
||||
is_skipped_folder,
|
||||
)
|
||||
from ...utils.parallel import map_parallel
|
||||
from ...utils.json_strict import dumps_strict
|
||||
from ...utils.inspection_response import (
|
||||
@@ -92,7 +96,11 @@ except ImportError:
|
||||
from database.models import normalize_image_path
|
||||
from database.run_attribution import recorded_output_lookup
|
||||
from utils.card_seed import card_seed, header_params, parsed_positive
|
||||
from utils.media_folders import in_skipped_folder, is_skipped_folder
|
||||
from utils.media_folders import (
|
||||
in_skipped_folder,
|
||||
in_trash_folder,
|
||||
is_skipped_folder,
|
||||
)
|
||||
from utils.parallel import map_parallel
|
||||
from utils.json_strict import dumps_strict
|
||||
from utils.inspection_response import (
|
||||
@@ -291,6 +299,21 @@ def _media_access_error(path, allowed_dirs):
|
||||
return None
|
||||
|
||||
|
||||
def _record_in_trash_folder(path, roots) -> bool:
|
||||
"""Whether a recorded *path* lies in a trash folder below every root holding it.
|
||||
|
||||
A source named like a trash folder is still a source, so a path it holds
|
||||
without a trash folder below it stays servable (#543).
|
||||
"""
|
||||
real = os.path.realpath(path)
|
||||
below = []
|
||||
for root in roots:
|
||||
real_root = os.path.realpath(root)
|
||||
if real.startswith(os.path.join(real_root, "")):
|
||||
below.append(os.path.relpath(real, real_root))
|
||||
return bool(below) and all(in_trash_folder(relative) for relative in below)
|
||||
|
||||
|
||||
# Media types the gallery scans and thumbnails (a superset is never served).
|
||||
GALLERY_IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif"})
|
||||
MEDIA_EXTENSIONS = GALLERY_IMAGE_EXTENSIONS | VIDEO_EXTENSIONS
|
||||
@@ -2640,7 +2663,10 @@ class ImageRoutesMixin:
|
||||
if denied is not None:
|
||||
return denied
|
||||
|
||||
if not image_path.is_file():
|
||||
if (
|
||||
_record_in_trash_folder(image_path, allowed_dirs)
|
||||
or not image_path.is_file()
|
||||
):
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Image file not found"}, status=404
|
||||
)
|
||||
@@ -2761,6 +2787,13 @@ class ImageRoutesMixin:
|
||||
if _has_traversal(filepath):
|
||||
return _forbidden()
|
||||
suffix = Path(filepath).suffix.lower()
|
||||
if in_trash_folder(filepath):
|
||||
# Thrown away, not an output (#543): as if it were not there.
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Image file not found"},
|
||||
status=404,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
if suffix not in SERVABLE_EXTENSIONS and not (
|
||||
# A JPEG XL preview is served from thumbnails/ only (#317).
|
||||
suffix in thumbnail_settings.THUMBNAIL_EXTENSIONS
|
||||
@@ -3593,7 +3626,9 @@ class ImageRoutesMixin:
|
||||
if denied is not None:
|
||||
return denied
|
||||
|
||||
if not os.path.isfile(image_path):
|
||||
if _record_in_trash_folder(image_path, allowed_dirs) or not os.path.isfile(
|
||||
image_path
|
||||
):
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Image file not found"}, status=404
|
||||
)
|
||||
@@ -3673,6 +3708,8 @@ class ImageRoutesMixin:
|
||||
@staticmethod
|
||||
def _reveal_path(relative: str, root: Path) -> Path | None:
|
||||
"""The real path of a regular media file under *root*, else None."""
|
||||
if in_trash_folder(relative):
|
||||
return None
|
||||
resolved = ImageRoutesMixin._resolve_client_media_path(relative, [root])
|
||||
if resolved is None or _media_access_error(resolved, [root]) is not None:
|
||||
return None
|
||||
@@ -3907,6 +3944,10 @@ class ImageRoutesMixin:
|
||||
if _is_thumbnail_cache_path(relative):
|
||||
# A preview is a placeholder: it carries no metadata (#317).
|
||||
return _forbidden("Previews are not inspected; inspect the original")
|
||||
if in_trash_folder(relative):
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Image file not found"}, status=404
|
||||
)
|
||||
path = Path(roots[index]) / relative
|
||||
denied = _media_access_error(path, [roots[index]])
|
||||
if denied is None:
|
||||
|
||||
+3
-1
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .images import _canonical, _media_access_error
|
||||
from .images import _canonical, _media_access_error, _record_in_trash_folder
|
||||
from .pairs import pair_response
|
||||
|
||||
try:
|
||||
@@ -268,6 +268,8 @@ class LibraryRoutesMixin:
|
||||
denied = _media_access_error(path, roots)
|
||||
if denied is not None or not path.is_file():
|
||||
continue
|
||||
if _record_in_trash_folder(path, roots):
|
||||
continue
|
||||
for index, root in enumerate(roots):
|
||||
canonical_root = _canonical(root)
|
||||
if path.is_relative_to(canonical_root):
|
||||
|
||||
Reference in New Issue
Block a user