Files
main_backend/cpv3/modules/files/router.py
T
Daniil 259d3da89f rev 4
2026-04-07 13:42:45 +03:00

251 lines
8.5 KiB
Python

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)