feat(storage): image gallery with thumbnails + lightbox
Add a List/Gallery toggle to the bucket browser that auto-switches to a thumbnail grid when a listing is mostly images, and a keyboard-navigable lightbox (prev/next/esc) for full-size previews. Download endpoint serves inline with the correct image/* content-type (ECS stores octet-stream) so previews render instead of forcing a download.
This commit is contained in:
+19
-3
@@ -218,24 +218,40 @@ async def list_objects(
|
||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
||||
|
||||
|
||||
_MEDIA_BY_EXT = {
|
||||
"jpeg": "image/jpeg", "jpg": "image/jpeg", "png": "image/png", "gif": "image/gif",
|
||||
"webp": "image/webp", "bmp": "image/bmp", "svg": "image/svg+xml",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/buckets/{bucket}/download")
|
||||
async def download_object(bucket: str, key: str = Query(...)):
|
||||
async def download_object(bucket: str, key: str = Query(...), inline: bool = Query(False)):
|
||||
try:
|
||||
_track("download")
|
||||
s3 = _client()
|
||||
obj = s3.get_object(Bucket=bucket, Key=key)
|
||||
body = obj["Body"]
|
||||
filename = key.split("/")[-1] or "download"
|
||||
media = obj.get("ContentType") or "application/octet-stream"
|
||||
ext_media = _MEDIA_BY_EXT.get(_ext(key))
|
||||
stored = obj.get("ContentType")
|
||||
# ECS often stores everything as octet-stream — trust the extension for
|
||||
# known image types so previews render inline instead of downloading.
|
||||
media = ext_media or stored or "application/octet-stream"
|
||||
if ext_media and (not stored or stored == "application/octet-stream"):
|
||||
media = ext_media
|
||||
|
||||
def stream():
|
||||
while chunk := body.read(1024 * 256):
|
||||
yield chunk
|
||||
|
||||
disp = "inline" if inline else "attachment"
|
||||
return StreamingResponse(
|
||||
stream(),
|
||||
media_type=media,
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
headers={
|
||||
"Content-Disposition": f'{disp}; filename="{filename}"',
|
||||
"Cache-Control": "private, max-age=300",
|
||||
},
|
||||
)
|
||||
except ClientError as exc:
|
||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=404)
|
||||
|
||||
Reference in New Issue
Block a user