fix(thumbnails): gate the preview fallback past the cap, and History outputs (#543)
The gate's OVER branch looked its source up by name and served it without the trash check. A History output recorded inside a trash folder now answers missing.
This commit is contained in:
+2
-1
@@ -2494,7 +2494,8 @@ files to the system trash. **No code path unlinks or overwrites a media file.**
|
||||
`_media_access_error` is the one gate (by-id, inspect, reveal, link, rescope as
|
||||
`file-missing`, a prompt's media listing); `/images/serve` and the autotag target ask
|
||||
`in_trash_below_roots` too, `media_target` refuses by name (annotations, collections,
|
||||
whose capture store denies links and aliases), and previews are not made. The delete
|
||||
whose capture store denies links and aliases), and previews are neither made nor served, the fallback past the preview gate's cap
|
||||
included (`_quick_source`). A History output in one answers `missing`. The delete
|
||||
and Return flows never use this gate. Windows spellings (trailing dots and spaces, case,
|
||||
8.3) run for real on the Windows job in `tests/test_trash_gate_routes.py`.
|
||||
- **An output folder that is a mount point** (#543): the mount's own `.Trash-$uid` and
|
||||
|
||||
@@ -17,6 +17,7 @@ from aiohttp import web
|
||||
|
||||
from . import thumbnail_settings
|
||||
from .images import _canonical, _public_path, SERVABLE_EXTENSIONS, VIDEO_EXTENSIONS
|
||||
from .images import in_trash_below_roots
|
||||
from .images import existing_media_thumbnail, media_thumbnail_rel
|
||||
|
||||
try:
|
||||
@@ -248,6 +249,9 @@ def _open_current_target(
|
||||
A *preview* (a file under ``thumbnails/``) may also be JPEG XL (#317)."""
|
||||
if _canonical(target) != target:
|
||||
raise CurrentLocationUnavailable("not_authorized", stale=True)
|
||||
if in_trash_below_roots(target, [namespace]):
|
||||
# An output recorded before #501 skipped trash folders (#543).
|
||||
raise CurrentLocationUnavailable("missing")
|
||||
if not _authorized(target, namespace, scope):
|
||||
raise CurrentLocationUnavailable("not_authorized")
|
||||
suffix = target.suffix.lower()
|
||||
|
||||
@@ -225,6 +225,8 @@ def _quick_source(root, parts):
|
||||
for name in (base + extension, base + extension.upper()):
|
||||
path = folder / name
|
||||
if _regular_file(path) and _is_within(path, root):
|
||||
if in_trash_below_roots(path, [root]):
|
||||
return None # also the fallback past the gate's cap (#543)
|
||||
return path, extension in VIDEO_EXTENSIONS
|
||||
return None
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import os
|
||||
import unittest
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
try:
|
||||
from tests.test_api_admin import _write_png
|
||||
@@ -20,7 +21,8 @@ except ImportError: # discovered with tests/ as the top-level directory
|
||||
from test_image_delete_routes import ImageDeleteTestCase
|
||||
|
||||
from py.api.images import media_thumbnail_rel
|
||||
from py.api.thumbnail_auto import _thumbnail_source
|
||||
from py.api.history_current import CurrentLocationUnavailable, _open_current_target
|
||||
from py.api.thumbnail_auto import OVER, _quick_source, _thumbnail_source
|
||||
from utils.media_folders import in_trash_below_roots, in_trash_folder, is_trash_folder
|
||||
|
||||
ON_CI = bool(os.environ.get("GITHUB_ACTIONS"))
|
||||
@@ -204,6 +206,28 @@ class TestTheGate(_GateCase):
|
||||
kept = Path(media_thumbnail_rel(Path("kept.png"))).parts
|
||||
self.assertIsNotNone(_thumbnail_source(self.root, kept))
|
||||
|
||||
async def test_the_preview_fallback_past_the_cap_is_gated_too(self):
|
||||
# The gate's OVER branch looks the source up by name and serves it.
|
||||
self.links()
|
||||
gate = self.api._preview_gate()
|
||||
with patch.object(gate, "run", AsyncMock(return_value=OVER)):
|
||||
linked = media_thumbnail_rel(Path("linkdir/x.png"))
|
||||
self.assertEqual(await self.serve(linked), 404)
|
||||
kept = media_thumbnail_rel(Path("kept.png"))
|
||||
self.assertEqual(await self.serve(kept), 200)
|
||||
self.assertIsNone(
|
||||
_quick_source(
|
||||
self.root, Path(media_thumbnail_rel(Path("linkdir/x.png"))).parts
|
||||
)
|
||||
)
|
||||
|
||||
async def test_a_history_output_in_a_trash_folder_does_not_open(self):
|
||||
self.links()
|
||||
target = self.root / ".Trash-1000" / "files" / "x.png"
|
||||
with self.assertRaises(CurrentLocationUnavailable) as caught:
|
||||
_open_current_target(Path(os.path.realpath(target)), self.root, None)
|
||||
self.assertEqual(caught.exception.reason, "missing")
|
||||
|
||||
async def test_a_source_named_like_a_trash_folder_still_reads(self):
|
||||
source = self.root / ".Trash-77"
|
||||
source.mkdir()
|
||||
|
||||
Reference in New Issue
Block a user