""" =============================================================================== FILE: modules/web_api/routers/files.py ROLE: Раздача сформированных отчетов и выгрузок с сохранением оригинальных имен через изолированные UUID-директории инструментов. =============================================================================== """ import os import time import shutil import urllib.parse from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse router = APIRouter(prefix="/api/v1/files", tags=["Files"]) BASE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) WEB_OUTPUT_DIR = os.path.join(BASE_ROOT, "output", "web") os.makedirs(WEB_OUTPUT_DIR, exist_ok=True) SESSION_TTL_HOURS = 24 # Срок жизни временных сессионных выгрузок def purge_old_tool_sessions(tool_dir_path: str): """Удаляет временные UUID-папки старше SESSION_TTL_HOURS внутри инструмента.""" if not os.path.exists(tool_dir_path): return now = time.time() cutoff = now - (SESSION_TTL_HOURS * 3600) try: for entry in os.listdir(tool_dir_path): subpath = os.path.join(tool_dir_path, entry) if os.path.isdir(subpath): if os.path.getmtime(subpath) < cutoff: shutil.rmtree(subpath, ignore_errors=True) except Exception: pass @router.get("/download/{tool_name}/{session_uuid}/{filename}") async def download_file(tool_name: str, session_uuid: str, filename: str): """ Безопасная отдача файла с каноническим именем из изолированной директории. """ safe_tool = os.path.basename(tool_name) safe_uuid = os.path.basename(session_uuid) safe_filename = os.path.basename(filename) file_path = os.path.join(WEB_OUTPUT_DIR, safe_tool, safe_uuid, safe_filename) if not os.path.exists(file_path) or not os.path.isfile(file_path): raise HTTPException(status_code=404, detail="Файл не найден или срок его действия истек") # Определение MIME-типа media_type = "application/octet-stream" if safe_filename.endswith(".md") or safe_filename.endswith(".txt"): media_type = "text/markdown; charset=utf-8" elif safe_filename.endswith(".xlsx"): media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" elif safe_filename.endswith(".pdf"): media_type = "application/pdf" # Корректная кодировка для кириллических имен файлов encoded_filename = urllib.parse.quote(safe_filename) return FileResponse( path=file_path, media_type=media_type, headers={ "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } )