Add thumbnail endpoint (#2468)

- Add /thumbnail collections endpoint to serve the thumbnail as an image for public
collections.
- Also fix uploading thumbnail images to use correct mime, if available.
This commit is contained in:
Ilya Kreymer 2025-03-07 12:29:36 -08:00 committed by GitHub
parent 13bf818914
commit 6c192df49d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 1 deletions

View File

@ -11,6 +11,7 @@ import os
import asyncio
import pymongo
import aiohttp
from pymongo.collation import Collation
from fastapi import Depends, HTTPException, Response
from fastapi.responses import StreamingResponse
@ -407,6 +408,34 @@ class CollectionOps:
return PublicCollOut.from_dict(result)
async def get_public_thumbnail(
self, slug: str, org: Organization
) -> StreamingResponse:
"""return thumbnail of public collection, if any"""
result = await self.get_collection_raw_by_slug(
slug, public_or_unlisted_only=True
)
thumbnail = result.get("thumbnail")
if not thumbnail:
raise HTTPException(status_code=404, detail="thumbnail_not_found")
image_file = ImageFile(**thumbnail)
image_file_out = await image_file.get_public_image_file_out(
org, self.storage_ops
)
path = self.storage_ops.resolve_internal_access_path(image_file_out.path)
async def reader():
async with aiohttp.ClientSession() as session:
async with session.get(path) as resp:
async for chunk in resp.content.iter_chunked(4096):
yield chunk
headers = {"Cache-Control": "max-age=3600, stale-while-revalidate=86400"}
return StreamingResponse(reader(), media_type=image_file.mime, headers=headers)
async def list_collections(
self,
org: Organization,
@ -852,6 +881,7 @@ class CollectionOps:
file_prep.upload_name,
stream_iter(),
MIN_UPLOAD_PART_SIZE,
mime=file_prep.mime,
):
print("Collection thumbnail stream upload failed", flush=True)
raise HTTPException(status_code=400, detail="upload_failed")
@ -1175,6 +1205,24 @@ def init_collections_api(
return await colls.download_collection(coll.id, org)
@app.get(
"/public/orgs/{org_slug}/collections/{coll_slug}/thumbnail",
tags=["collections", "public"],
response_class=StreamingResponse,
)
async def get_public_thumbnail(
org_slug: str,
coll_slug: str,
):
try:
org = await colls.orgs.get_org_by_slug(org_slug)
# pylint: disable=broad-exception-caught
except Exception:
# pylint: disable=raise-missing-from
raise HTTPException(status_code=404, detail="collection_not_found")
return await colls.get_public_thumbnail(coll_slug, org)
@app.post(
"/orgs/{oid}/collections/{coll_id}/home-url",
tags=["collections"],

View File

@ -382,6 +382,7 @@ class StorageOps:
filename: str,
file_: AsyncIterator,
min_size: int,
mime: Optional[str] = None,
) -> bool:
"""do upload to specified key using multipart chunking"""
s3storage = self.get_org_primary_storage(org)
@ -405,7 +406,10 @@ class StorageOps:
key += filename
mup_resp = await client.create_multipart_upload(
ACL="bucket-owner-full-control", Bucket=bucket, Key=key
ACL="bucket-owner-full-control",
Bucket=bucket,
Key=key,
ContentType=mime or "",
)
upload_id = mup_resp["UploadId"]