# #
###############################################################################
+import asyncio
import fastapi
import pydantic
from uuid import UUID
from . import apiv1
+from . import app
from . import backend
from ..packages import Package, File
tags=["Packages"],
)
-async def get_package_by_uuid(uuid: UUID) -> Package:
+async def get_package_by_uuid(uuid: UUID) -> Package | None:
"""
Fetches a package by its UUID
"""
# Return the entire filelist
return [file async for file in await package.get_files()]
+
+@app.get("/packages/{uuid:uuid}/download/{path:path}", include_in_schema=False)
+async def download_file(
+ path: str,
+ package: Package = fastapi.Depends(get_package_by_uuid),
+) -> fastapi.responses.StreamingResponse:
+ # Fail if the package could not be found
+ if not package:
+ raise fastapi.HTTPException(404, "Could not find package")
+
+ # Put the leading slash back into the path
+ path = "/%s" % path
+
+ # Fetch the file
+ file = await package.get_file(path)
+ if not file:
+ raise fastapi.HTTPException(404, "Could not find file %s in %s" % (path, package))
+
+ # XXX Check if this is actually downloadable
+
+ headers = {
+ "Content-Disposition" : "attachment; filename=%s" % file.basename,
+
+ # XXX StreamingResponse does not allow us to set a Content-Length header
+ # because starlette is getting very confused about how much data is to be
+ # sent or has been sent already.
+ #"Content-Length" : "%s" % file.size,
+
+ # This content should not be indexed
+ "X-Robots-Tag" : "noindex",
+ }
+
+ # Create a helper function that will stream the file chunk by chunk
+ async def stream():
+ f = await file.open()
+
+ while True:
+ chunk = await asyncio.to_thread(f.read, 128 * 1024)
+ if not chunk:
+ break
+
+ yield chunk
+
+ return fastapi.responses.StreamingResponse(
+ stream(), media_type=file.mimetype, headers=headers,
+ )
+
+
# Add everything to the APIv1
apiv1.include_router(router)