From: Michael Tremer Date: Wed, 2 Jul 2025 18:32:19 +0000 (+0000) Subject: api: Move over the downloads endpoints X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9fe82db2190511f3af1b4f67d5add747a99008c8;p=pbs.git api: Move over the downloads endpoints Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index 05c3a965..5177de46 100644 --- a/Makefile.am +++ b/Makefile.am @@ -125,6 +125,7 @@ api_PYTHON = \ src/api/builders.py \ src/api/builds.py \ src/api/distros.py \ + src/api/downloads.py \ src/api/events.py \ src/api/mirrors.py \ src/api/uploads.py \ diff --git a/src/api/__init__.py b/src/api/__init__.py index a4fffd38..8d6e4019 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -55,6 +55,7 @@ from . import auth from . import builders from . import builds from . import distros +from . import downloads from . import events from . import mirrors from . import uploads diff --git a/src/api/downloads.py b/src/api/downloads.py new file mode 100644 index 00000000..11104425 --- /dev/null +++ b/src/api/downloads.py @@ -0,0 +1,122 @@ +############################################################################### +# # +# Pakfire - The IPFire package management system # +# Copyright (C) 2025 Pakfire development team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +import datetime +import fastapi +import ipaddress +import stat + +from . import app +from . import backend + +# Create a new router for all endpoints +router = fastapi.APIRouter( + prefix="/downloads", +) + +# XXX These endpoints need some ratelimiting applied + +@router.head("/{path:path}") +async def head(path: str) -> fastapi.Response: + """ + Handle any HEAD requests + """ + # Stat the file + s = await backend.stat(path, stat.S_IFREG) + + # Send 404 if the file does not exist + if not s: + raise fastapi.HTTPException(404) + + # Fetch the MIME type + mimetype = await backend.mimetype(path) + + # Send a couple of headers + return fastapi.Response( + headers = { + "Content-Type" : mimetype or "application/octet-stream", + "Content-Length" : "%s" % s.st_size, + "Last-Modified" : datetime.datetime.fromtimestamp(s.st_mtime).isoformat(), + "Etag" : "%x-%x" % (int(s.st_mtime), s.st_size), + }, + ) + +async def get_current_address( + request: fastapi.Request +) -> ipaddress.IPv6Address | ipaddress.IPv4Address: + return ipaddress.ip_address(request.client.host) + + +@router.get("/{path:path}") +async def get( + path: str, current_address: ipaddress.IPv6Address | ipaddress.IPv4Address = \ + fastapi.Depends(get_current_address), +) -> fastapi.responses.RedirectResponse: + """ + Handle any downloads + """ + # Check if the file exists + if not await backend.stat(path, stat.S_IFREG): + raise fastapi.HTTPException(404) + + print(path) + + # Tell the clients to never cache the redirect + headers = { + "Cache-Control" : "no-store", + } + + # Fetch all mirrors for this client + mirrors = await backend.mirrors.get_mirrors_for_address(current_address) + + # Walk through all mirrors + for mirror in mirrors: + # Don't send clients to a mirror they don't support + if isinstance(current_address, ipaddress.IPv6Address): + if not mirror.supports_ipv6(): + continue + elif isinstance(current_address, ipaddress.IPv4Address): + if not mirror.supports_ipv4(): + continue + + # Skip the mirror if it does not serve the file we are looking for + if not await mirror.serves_file(path): + continue + + # Log action + #log.info("Sending %s to download %s from %s", self.current_address, path, mirror) + + # Make the URL + url = mirror.make_url(path) + + # Redirect the user + return fastapi.responses.RedirectResponse(url, headers=headers) + + # If we go here, we did not find any working mirror. + # We will send the user to our local file store and hopefully the right + # file will be there. If not, the client will receive 404 from there. + url = backend.path_to_url(path, mirrored=False) + + print("URL", url) + + return fastapi.responses.RedirectResponse(url, headers=headers) + +# Add everything to the app +app.include_router(router)