diff --git a/backend/btrixcloud/colls.py b/backend/btrixcloud/colls.py index e74e5b6a..a475328b 100644 --- a/backend/btrixcloud/colls.py +++ b/backend/btrixcloud/colls.py @@ -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"], diff --git a/backend/btrixcloud/storages.py b/backend/btrixcloud/storages.py index 012aeddd..1e585217 100644 --- a/backend/btrixcloud/storages.py +++ b/backend/btrixcloud/storages.py @@ -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"]