From: Michael Tremer Date: Mon, 7 Jul 2025 17:44:05 +0000 (+0000) Subject: api: Implement downloading files from packages X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0afa05aafd113f082be2838f61e998061f6cc333;p=pbs.git api: Implement downloading files from packages Signed-off-by: Michael Tremer --- diff --git a/src/api/packages.py b/src/api/packages.py index b5af7974..8e75f1dd 100644 --- a/src/api/packages.py +++ b/src/api/packages.py @@ -18,6 +18,7 @@ # # ############################################################################### +import asyncio import fastapi import pydantic @@ -25,6 +26,7 @@ import pydantic from uuid import UUID from . import apiv1 +from . import app from . import backend from ..packages import Package, File @@ -35,7 +37,7 @@ router = fastapi.APIRouter( 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 """ @@ -88,5 +90,53 @@ async def get_filelist_by_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) diff --git a/src/buildservice/packages.py b/src/buildservice/packages.py index 7a0b6ebd..4568b424 100644 --- a/src/buildservice/packages.py +++ b/src/buildservice/packages.py @@ -656,6 +656,12 @@ class File(sqlmodel.SQLModel, table=True): path: str = sqlmodel.Field(primary_key=True) + # Basename + + @property + def basename(self): + return os.path.basename(self.path) + # Size size: int