--- /dev/null
+###############################################################################
+# #
+# 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)