feat: Use X-Accel-Redirect to improve file download speed

Instead of making FastAPI handle file download, which has serious
performance issues on big files [1], this change uses nginx's `X-Accel`
feature to delegate single-file downloads to nginx.

Partial fix for #1079, as it solves the CPU usage issue for single-file
downloads.

[1] https://github.com/fastapi/fastapi/discussions/6050
This commit is contained in:
Michael Manganiello
2024-08-12 20:01:00 -03:00
parent dff554b53b
commit d20f4ad935
3 changed files with 25 additions and 6 deletions

View File

@@ -13,7 +13,8 @@ GUNICORN_WORKERS: Final = int(os.environ.get("GUNICORN_WORKERS", 2))
# PATHS
ROMM_BASE_PATH: Final = os.environ.get("ROMM_BASE_PATH", "/romm")
LIBRARY_BASE_PATH: Final = f"{ROMM_BASE_PATH}/library"
LIBRARY_PATH_SUFFIX: Final = "/library"
LIBRARY_BASE_PATH: Final = f"{ROMM_BASE_PATH}{LIBRARY_PATH_SUFFIX}"
RESOURCES_BASE_PATH: Final = f"{ROMM_BASE_PATH}/resources"
ASSETS_BASE_PATH: Final = f"{ROMM_BASE_PATH}/assets"
FRONTEND_RESOURCES_PATH: Final = "/assets/romm/resources"

View File

@@ -9,6 +9,7 @@ from anyio import Path, open_file
from config import (
DISABLE_DOWNLOAD_ENDPOINT_AUTH,
LIBRARY_BASE_PATH,
LIBRARY_PATH_SUFFIX,
RESOURCES_BASE_PATH,
)
from decorators.auth import protected_route
@@ -22,7 +23,7 @@ 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 import File, HTTPException, Query, Request, Response, UploadFile, status
from fastapi.responses import FileResponse
from handler.database import db_platform_handler, db_rom_handler
from handler.filesystem import fs_resource_handler, fs_rom_handler
@@ -210,17 +211,28 @@ async def get_rom_content(
if not rom:
raise RomNotFoundInDatabaseException(id)
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
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(
headers={
"Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"',
"Content-Type": "application/octet-stream",
"X-Accel-Redirect": f"{LIBRARY_PATH_SUFFIX}/{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(
headers={
"Content-Disposition": f'attachment; filename="{quote(files_to_download[0])}"',
"Content-Type": "application/octet-stream",
"X-Accel-Redirect": f"{LIBRARY_PATH_SUFFIX}/{rom.full_path}/{files_to_download[0]}",
},
)
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
# Builds a generator of tuples for each member file
async def local_files() -> AsyncIterator[AsyncMemberFile]:
async def contents(filename: str) -> AsyncIterator[bytes]:

View File

@@ -87,5 +87,11 @@ http {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Library files
location /library {
internal;
alias /romm/library;
}
}
}