from __future__ import annotations import uuid from pathlib import Path from fastapi import ( APIRouter, Depends, File as FastAPIFile, Form, HTTPException, Query, Response, UploadFile, status, ) from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from cpv3.infrastructure.auth import get_current_user from cpv3.infrastructure.deps import get_storage from cpv3.infrastructure.storage.base import StorageService from cpv3.infrastructure.storage.utils import get_user_folder from cpv3.infrastructure.settings import get_settings from cpv3.db.session import get_db from cpv3.modules.files.schemas import ( FileCreate, FileInfoResponse, FileRead, FileUpdate, ) from cpv3.modules.files.service import FileService from cpv3.modules.users.models import User router = APIRouter(prefix="/api/files", tags=["Files"]) MAX_MB_SIZE = 1024 @router.post( "/upload/", response_model=FileInfoResponse, status_code=status.HTTP_201_CREATED ) async def upload_file( file: UploadFile = FastAPIFile(...), folder: str = Form(default=""), project_id: uuid.UUID | None = Form(default=None), current_user: User = Depends(get_current_user), storage: StorageService = Depends(get_storage), db: AsyncSession = Depends(get_db), ) -> FileInfoResponse: # Validate max file size (matches old behavior). file.file.seek(0, 2) size_bytes = file.file.tell() file.file.seek(0) max_size = MAX_MB_SIZE * 1024 * 1024 if size_bytes > max_size: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Размер файла превышает допустимый лимит в {MAX_MB_SIZE} МБ.", ) user_folder = get_user_folder(current_user) resolved_folder = f"{user_folder}/{folder}" if folder else f"{user_folder}/user_upload" inferred_project_id = project_id if inferred_project_id is None and folder.startswith("projects/"): project_token = folder.removeprefix("projects/").split("/", 1)[0] try: inferred_project_id = uuid.UUID(project_token) except ValueError: inferred_project_id = None key = await storage.upload_fileobj( fileobj=file.file, file_name=file.filename or "upload.bin", folder=resolved_folder, gen_name=True, content_type=file.content_type, ) service = FileService(db) file_entry = await service.create_file( requester=current_user, data=FileCreate( project_id=inferred_project_id, original_filename=file.filename or "upload.bin", path=key, storage_backend=get_settings().storage_backend.upper(), mime_type=file.content_type or "application/octet-stream", size_bytes=size_bytes, file_format=Path(file.filename or "upload.bin").suffix.lstrip(".") or None, is_uploaded=True, ), ) info = await storage.get_file_info(key) return FileInfoResponse( file_id=file_entry.id, file_path=info.file_path, file_url=info.file_url, file_size=info.file_size, filename=file.filename, ) @router.get("/get_file/", response_model=FileInfoResponse) async def get_file_info( file_path: str = Query(...), current_user: User = Depends(get_current_user), storage: StorageService = Depends(get_storage), db: AsyncSession = Depends(get_db), ) -> FileInfoResponse: if not current_user.is_staff: user_prefix = f"{get_user_folder(current_user)}/" if not file_path.startswith(user_prefix): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён") if not await storage.exists(file_path): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") service = FileService(db) file = await service.get_file_by_path(file_path) if file is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") info = await storage.get_file_info(file_path) return FileInfoResponse( file_id=file.id, file_path=info.file_path, file_url=info.file_url, file_size=info.file_size, filename=info.filename, ) @router.get("/local/{file_path:path}") async def get_local_file( file_path: str, current_user: User = Depends(get_current_user), ) -> FileResponse: _ = current_user settings = get_settings() full_path = (settings.local_storage_dir / file_path).resolve() if not full_path.exists(): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") return FileResponse(full_path) @router.get("/files/", response_model=list[FileRead]) async def list_file_entries( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> list[FileRead]: service = FileService(db) files = await service.list_files(requester=current_user) return [FileRead.model_validate(f) for f in files] @router.post("/files/", response_model=FileRead, status_code=status.HTTP_201_CREATED) async def create_file_entry( body: FileCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> FileRead: service = FileService(db) file = await service.create_file(requester=current_user, data=body) return FileRead.model_validate(file) @router.get("/files/{file_id}/", response_model=FileRead) async def retrieve_file_entry( file_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> FileRead: service = FileService(db) file = await service.get_file(file_id) if file is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") if not current_user.is_staff and file.owner_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён") return FileRead.model_validate(file) @router.get("/files/{file_id}/resolve/", response_model=FileInfoResponse) async def resolve_file_entry( file_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), storage: StorageService = Depends(get_storage), ) -> FileInfoResponse: service = FileService(db) file = await service.get_file(file_id) if file is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") if not current_user.is_staff and file.owner_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён") if not await storage.exists(file.path): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") info = await storage.get_file_info(file.path) return FileInfoResponse( file_id=file.id, file_path=file.path, file_url=info.file_url, file_size=info.file_size, filename=file.original_filename or info.filename, ) @router.patch("/files/{file_id}/", response_model=FileRead) async def patch_file_entry( file_id: uuid.UUID, body: FileUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> FileRead: service = FileService(db) file = await service.get_file(file_id) if file is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") if not current_user.is_staff and file.owner_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён") file = await service.update_file(file, body) return FileRead.model_validate(file) @router.delete("/files/{file_id}/", status_code=status.HTTP_204_NO_CONTENT) async def delete_file_entry( file_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> Response: service = FileService(db) file = await service.get_file(file_id) if file is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено") if not current_user.is_staff and file.owner_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён") await service.delete_file(file) return Response(status_code=status.HTTP_204_NO_CONTENT)