]> git.ipfire.org Git - pbs.git/commitdiff
api: Move over the downloads endpoints
authorMichael Tremer <michael.tremer@ipfire.org>
Wed, 2 Jul 2025 18:32:19 +0000 (18:32 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 2 Jul 2025 18:32:19 +0000 (18:32 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/api/__init__.py
src/api/downloads.py [new file with mode: 0644]

index 05c3a9654eb9c4cc2389312eda4a84cc28ecf221..5177de4631cc8055a0a69d7735cd5b89500bf6dd 100644 (file)
@@ -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 \
index a4fffd38ba82836e87125fdfc448fa806e4dd8c1..8d6e4019bb03cecc4b28d957b5207b8421700659 100644 (file)
@@ -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 (file)
index 0000000..1110442
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+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)