diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index e69deab9e..afe8b1c2f 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -23,13 +23,13 @@ from endpoints.responses.rom import ( from exceptions.endpoint_exceptions import RomNotFoundInDatabaseException from exceptions.fs_exceptions import RomAlreadyExistsException from fastapi import File, HTTPException, Query, Request, UploadFile, status -from fastapi.responses import FileResponse +from fastapi.responses import Response from handler.database import db_platform_handler, db_rom_handler from handler.filesystem import fs_resource_handler, fs_rom_handler from handler.filesystem.base_handler import CoverSize from handler.metadata import meta_igdb_handler, meta_moby_handler from logger.logger import log -from stream_zip import NO_COMPRESSION_32, ZIP_AUTO, AsyncMemberFile, async_stream_zip +from stream_zip import NO_COMPRESSION_64, ZIP_AUTO, AsyncMemberFile, async_stream_zip from utils.router import APIRouter router = APIRouter() @@ -154,7 +154,12 @@ def get_rom(request: Request, id: int) -> DetailedRomSchema: "/roms/{id}/content/{file_name}", [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else ["roms.read"], ) -def head_rom_content(request: Request, id: int, file_name: str): +async def head_rom_content( + request: Request, + id: int, + file_name: str, + files: Annotated[list[str] | None, Query()] = None, +): """Head rom content endpoint Args: @@ -171,15 +176,30 @@ def head_rom_content(request: Request, id: int, file_name: str): if not rom: raise RomNotFoundInDatabaseException(id) - rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}" + files_to_check = files or [r["filename"] for r in rom.files] - return FileResponse( - path=rom_path if not rom.multi else f'{rom_path}/{rom.files[0]["filename"]}', - filename=file_name, + if not rom.multi: + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"', + "X-Accel-Redirect": f"/library/{rom.full_path}", + }, + ) + + if len(files_to_check) == 1: + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(files_to_check[0])}"', + "X-Accel-Redirect": f"/library/{rom.full_path}/{files_to_check[0]}", + }, + ) + + return Response( + media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{quote(rom.name)}.zip"', - "Content-Type": "application/zip", - "Content-Length": str(rom.file_size_bytes), + "Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"', }, ) @@ -214,11 +234,21 @@ async def get_rom_content( files_to_download = files or [r["filename"] for r in rom.files] if not rom.multi: - return FileResponse(path=rom_path, filename=rom.file_name) + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"', + "X-Accel-Redirect": f"/library/{rom.full_path}", + }, + ) if len(files_to_download) == 1: - return FileResponse( - path=f"{rom_path}/{files_to_download[0]}", filename=files_to_download[0] + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(files_to_download[0])}"', + "X-Accel-Redirect": f"/library/{rom.full_path}/{files_to_download[0]}", + }, ) # Builds a generator of tuples for each member file @@ -252,7 +282,7 @@ async def get_rom_content( f"{file_name}.m3u", now, S_IFREG | 0o600, - NO_COMPRESSION_32, + NO_COMPRESSION_64, m3u_file(), ) @@ -263,7 +293,7 @@ async def get_rom_content( zipped_chunks, media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"' + "Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"', }, emit_body={"id": rom.id}, ) diff --git a/backend/main.py b/backend/main.py index 8f3eb38b1..c810f870d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -32,7 +32,6 @@ from endpoints import ( ) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.gzip import GZipMiddleware from handler.auth.base_handler import ALGORITHM from handler.auth.hybrid_auth import HybridAuthBackend from handler.auth.middleware import CustomCSRFMiddleware, SessionMiddleware @@ -68,9 +67,6 @@ if not IS_PYTEST_RUN and not DISABLE_CSRF_PROTECTION: exempt_urls=[re.compile(r"^/token.*"), re.compile(r"^/ws")], ) -# Enable GZip compression for responses -app.add_middleware(GZipMiddleware, minimum_size=1024) - # Handles both basic and oauth authentication app.add_middleware( AuthenticationMiddleware, diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 5565cad7b..87896c31f 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -14,9 +14,14 @@ http { scgi_temp_path /tmp/scgi; sendfile on; + client_body_buffer_size 128k; client_max_body_size 0; + client_header_buffer_size 1k; + large_client_header_buffers 4 16k; + send_timeout 60s; + keepalive_timeout 65s; tcp_nopush on; - # types_hash_max_size 2048; + tcp_nodelay on; include /etc/nginx/mime.types; default_type application/octet-stream; @@ -41,6 +46,13 @@ http { error_log /dev/stderr; gzip on; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_min_length 1024; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # include /etc/nginx/conf.d/*.conf; # include /etc/nginx/sites-enabled/*; @@ -87,5 +99,11 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } + + # Internally redirect download requests + location /library { + internal; + alias /romm/library; + } } }