fix(images): percent-encode original-image URLs ('#' in paths 404'd)
An image whose on-disk path contains '#' (post folders like 'BLUE#59') served its hash-named thumbnail fine but 404'd the original: the unencoded '#' in image_url was parsed by the browser as a URL fragment, so '#59/01_timelapse.jpg' never reached the /images route. Add a shared image_url(path) helper that percent-encodes the path (safe='/') and route the 3 raw builders (gallery detail + 2 in series) through it. Not a cleanup-tool deletion — the file is on disk; only the URL was wrong. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ translates that to HTTP 400.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from sqlalchemy import Select, and_, distinct, exists, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -120,6 +121,21 @@ def thumbnail_url(thumbnail_path: str | None, sha256_hex: str, mime: str) -> str
|
||||
return f"/images/thumbs/{bucket}/{sha256_hex}{ext}"
|
||||
|
||||
|
||||
def image_url(path: str) -> str:
|
||||
"""Return the URL to fetch the full-size original from /images.
|
||||
|
||||
The on-disk `path` mirrors the source tree (artist/post folders), so it can
|
||||
contain characters that are special in a URL — most importantly '#' (post
|
||||
titles like 'BLUE#59'), but also spaces, '?', '%'. The serve_image route
|
||||
URL-decodes its <path:subpath> fine, but only if the browser sends the whole
|
||||
path; an unencoded '#' is parsed as a fragment, so '#59/01_timelapse.jpg'
|
||||
never reaches the server and the original 404s while the (hash-named)
|
||||
thumbnail still loads. Percent-encode the path, keeping '/' as the segment
|
||||
separator. Operator-flagged 2026-06-12."""
|
||||
rel = path.split("/images/", 1)[-1]
|
||||
return f"/images/{quote(rel, safe='/')}"
|
||||
|
||||
|
||||
def _require_single_filter(tag_ids, post_id, artist_id) -> None:
|
||||
"""post_id is the post-detail view — it can't combine with the
|
||||
composable filters. tag_ids + artist_id (+ media_type) compose freely
|
||||
@@ -589,7 +605,7 @@ class GalleryService:
|
||||
"created_at": record.created_at.isoformat(),
|
||||
"posted_at": posted_at.isoformat() if posted_at else None,
|
||||
"thumbnail_url": thumbnail_url(record.thumbnail_path, record.sha256, record.mime),
|
||||
"image_url": f"/images/{record.path.split('/images/', 1)[-1]}",
|
||||
"image_url": image_url(record.path),
|
||||
"artist": (
|
||||
{"id": artist.id, "name": artist.name, "slug": artist.slug}
|
||||
if artist is not None else None
|
||||
|
||||
Reference in New Issue
Block a user