73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""
|
|
===============================================================================
|
|
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}"
|
|
}
|
|
) |