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 asyncio
import pymongo import pymongo
import aiohttp
from pymongo.collation import Collation from pymongo.collation import Collation
from fastapi import Depends, HTTPException, Response from fastapi import Depends, HTTPException, Response
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@ -407,6 +408,34 @@ class CollectionOps:
return PublicCollOut.from_dict(result) 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( async def list_collections(
self, self,
org: Organization, org: Organization,
@ -852,6 +881,7 @@ class CollectionOps:
file_prep.upload_name, file_prep.upload_name,
stream_iter(), stream_iter(),
MIN_UPLOAD_PART_SIZE, MIN_UPLOAD_PART_SIZE,
mime=file_prep.mime,
): ):
print("Collection thumbnail stream upload failed", flush=True) print("Collection thumbnail stream upload failed", flush=True)
raise HTTPException(status_code=400, detail="upload_failed") raise HTTPException(status_code=400, detail="upload_failed")
@ -1175,6 +1205,24 @@ def init_collections_api(
return await colls.download_collection(coll.id, org) 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( @app.post(
"/orgs/{oid}/collections/{coll_id}/home-url", "/orgs/{oid}/collections/{coll_id}/home-url",
tags=["collections"], tags=["collections"],

View File

@ -382,6 +382,7 @@ class StorageOps:
filename: str, filename: str,
file_: AsyncIterator, file_: AsyncIterator,
min_size: int, min_size: int,
mime: Optional[str] = None,
) -> bool: ) -> bool:
"""do upload to specified key using multipart chunking""" """do upload to specified key using multipart chunking"""
s3storage = self.get_org_primary_storage(org) s3storage = self.get_org_primary_storage(org)
@ -405,7 +406,10 @@ class StorageOps:
key += filename key += filename
mup_resp = await client.create_multipart_upload( 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"] upload_id = mup_resp["UploadId"]