"""File upload, download, and listing endpoints.""" import logging import os import shutil from pathlib import Path from typing import List, Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Depends, Query, Request from fastapi.responses import FileResponse from pydantic import BaseModel from ..auth.dependencies import get_current_user, get_current_user_flexible from ..config import MAX_UPLOAD_SIZE, ALLOWED_EXTENSIONS from ..services.db_schema import insert_file_metadata, query_file_history, query_file_stats logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/files", tags=["files"]) # Resolve data directories relative to project root _project_root = Path(__file__).resolve().parent.parent.parent.parent _input_dir = _project_root / "data" / "input" _output_dir = _project_root / "data" / "output" _result_dir = _project_root / "data" / "result" class FileItem(BaseModel): name: str size: int modified: float directory: str class UploadResponse(BaseModel): filename: str size: int path: str def _ensure_dirs(): for d in [_input_dir, _output_dir, _result_dir]: d.mkdir(parents=True, exist_ok=True) def _record_file_action(filename: str, directory: str, size: int, action: str, user: str = None): """Record a file operation to the metadata table. Best-effort, non-blocking.""" try: insert_file_metadata(filename=filename, directory=directory, size=size, action=action, user=user) except Exception: logger.debug("Failed to record file metadata for %s/%s action=%s", directory, filename, action, exc_info=True) @router.post("/upload", response_model=UploadResponse) async def upload_file( file: UploadFile = File(...), current_user: dict = Depends(get_current_user), ): _ensure_dirs() # Validate extension ext = Path(file.filename).suffix.lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException(400, f"不支持的文件类型: {ext}") # Validate size content = await file.read() if len(content) > MAX_UPLOAD_SIZE: raise HTTPException(400, f"文件过大,最大 {MAX_UPLOAD_SIZE // 1024 // 1024}MB") # Save with secure name from werkzeug.utils import secure_filename safe_name = secure_filename(file.filename) or file.filename dest = _input_dir / safe_name # Avoid overwrite: add suffix if exists counter = 0 stem = Path(safe_name).stem suffix = Path(safe_name).suffix while dest.exists(): counter += 1 dest = _input_dir / f"{stem}_{counter}{suffix}" dest.write_bytes(content) _record_file_action(dest.name, "input", len(content), "upload", current_user.get("username")) return UploadResponse( filename=dest.name, size=len(content), path=str(dest.relative_to(_project_root)), ) @router.get("/list") async def list_files( directory: str = "input", current_user: dict = Depends(get_current_user), ) -> List[FileItem]: dir_map = {"input": _input_dir, "output": _output_dir, "result": _result_dir} target_dir = dir_map.get(directory) if not target_dir or not target_dir.is_dir(): return [] files = [] for f in sorted(target_dir.iterdir()): if f.is_file(): stat = f.stat() files.append(FileItem( name=f.name, size=stat.st_size, modified=stat.st_mtime, directory=directory, )) return files @router.get("/download/{directory}/{filename}") async def download_file( directory: str, filename: str, current_user: dict = Depends(get_current_user_flexible), ): dir_map = {"input": _input_dir, "output": _output_dir, "result": _result_dir} target_dir = dir_map.get(directory) if not target_dir: raise HTTPException(404, "目录不存在") file_path = target_dir / filename if not file_path.is_file(): raise HTTPException(404, "文件不存在") return FileResponse( str(file_path), filename=filename, media_type="application/octet-stream", ) @router.delete("/{directory}/{filename}") async def delete_file( directory: str, filename: str, current_user: dict = Depends(get_current_user), ): dir_map = {"input": _input_dir, "output": _output_dir, "result": _result_dir} target_dir = dir_map.get(directory) if not target_dir: raise HTTPException(404, "目录不存在") file_path = target_dir / filename if not file_path.is_file(): raise HTTPException(404, "文件不存在") size = file_path.stat().st_size file_path.unlink() _record_file_action(filename, directory, size, "delete", current_user.get("username")) return {"message": f"已删除 {filename}"} @router.post("/clear/{directory}") async def clear_directory( directory: str, current_user: dict = Depends(get_current_user), ): dir_map = {"input": _input_dir, "output": _output_dir, "result": _result_dir} target_dir = dir_map.get(directory) if not target_dir: raise HTTPException(404, "目录不存在") count = 0 for f in target_dir.iterdir(): if f.is_file(): f.unlink() count += 1 _record_file_action("*", directory, 0, "clear", current_user.get("username")) return {"message": f"已清除 {count} 个文件", "count": count} @router.get("/history") async def get_file_history( page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), directory: Optional[str] = None, action: Optional[str] = None, current_user: dict = Depends(get_current_user), ): """Query file operation history with pagination and optional filters.""" offset = (page - 1) * page_size rows = query_file_history( directory=directory, action=action, limit=page_size, offset=offset, ) return {"page": page, "page_size": page_size, "items": rows} @router.get("/stats") async def get_file_stats( current_user: dict = Depends(get_current_user), ): """Return file storage statistics per directory.""" return {"directories": query_file_stats()}