"""File storage endpoints.""" import json from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Query from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app.dependencies import get_db, get_current_user, get_current_superuser from app.models.user import User from app import crud from app.schemas.file import ( StoredFile as StoredFileSchema, FileCreate, FileUpdate, FileUploadResponse, FileListResponse, ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE, ) router = APIRouter() def file_to_schema(db_file) -> dict: """Convert a StoredFile model to schema dict.""" return { "id": db_file.id, "original_filename": db_file.original_filename, "content_type": db_file.content_type, "size_bytes": db_file.size_bytes, "storage_type": db_file.storage_type, "description": db_file.description, "tags": json.loads(db_file.tags) if db_file.tags else None, "is_public": db_file.is_public, "uploaded_by": db_file.uploaded_by, "file_hash": db_file.file_hash, "created_at": db_file.created_at, "updated_at": db_file.updated_at, } @router.post("/upload", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED) async def upload_file( file: UploadFile = File(...), description: Optional[str] = None, tags: Optional[str] = None, # Comma-separated tags is_public: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Upload a file. Returns file metadata with download URL. """ # Read file content content = await file.read() size = len(content) # Validate upload is_valid, error = crud.file_storage.validate_upload( content_type=file.content_type, size_bytes=size ) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error ) # Parse tags tag_list = None if tags: tag_list = [t.strip() for t in tags.split(",") if t.strip()] # Create file metadata metadata = FileCreate( description=description, tags=tag_list, is_public=is_public ) # Reset file position for reading await file.seek(0) # Save file import io stored_file = crud.file_storage.create( db, file=io.BytesIO(content), filename=file.filename, content_type=file.content_type, size_bytes=size, uploaded_by=current_user.id, metadata=metadata ) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="upload", resource_type="file", resource_id=stored_file.id, details={"filename": file.filename, "size": size}, status="success" ) return { "id": stored_file.id, "original_filename": stored_file.original_filename, "content_type": stored_file.content_type, "size_bytes": stored_file.size_bytes, "download_url": f"/api/v1/files/{stored_file.id}/download" } @router.get("/", response_model=FileListResponse) def list_files( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), mine_only: bool = False, is_public: Optional[bool] = None, content_type: Optional[str] = None, ): """ List files with pagination and filtering. Regular users can only see their own files and public files. Superusers can see all files. """ skip = (page - 1) * page_size # Filter by ownership for non-superusers if not current_user.is_superuser: if mine_only: uploaded_by = current_user.id is_public = None else: # Show user's files and public files own_files = crud.file_storage.get_multi( db, skip=0, limit=1000, # Get all for filtering uploaded_by=current_user.id ) public_files = crud.file_storage.get_multi( db, skip=0, limit=1000, is_public=True ) # Combine and deduplicate all_files = {f.id: f for f in own_files} all_files.update({f.id: f for f in public_files}) files_list = list(all_files.values()) # Sort by created_at desc files_list.sort(key=lambda x: x.created_at, reverse=True) # Paginate total = len(files_list) files = files_list[skip:skip + page_size] return { "files": [file_to_schema(f) for f in files], "total": total, "page": page, "page_size": page_size } uploaded_by = current_user.id if mine_only else None else: uploaded_by = current_user.id if mine_only else None files = crud.file_storage.get_multi( db, skip=skip, limit=page_size, uploaded_by=uploaded_by, is_public=is_public, content_type=content_type ) total = crud.file_storage.count( db, uploaded_by=uploaded_by, is_public=is_public ) return { "files": [file_to_schema(f) for f in files], "total": total, "page": page, "page_size": page_size } @router.get("/{file_id}", response_model=StoredFileSchema) def get_file( file_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Get file metadata. Users can only access their own files or public files. """ stored_file = crud.file_storage.get(db, id=file_id) if not stored_file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Check access if not stored_file.is_public and stored_file.uploaded_by != current_user.id and not current_user.is_superuser: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) return file_to_schema(stored_file) @router.get("/{file_id}/download") def download_file( file_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Download a file. Users can only download their own files or public files. """ stored_file = crud.file_storage.get(db, id=file_id) if not stored_file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Check access if not stored_file.is_public and stored_file.uploaded_by != current_user.id and not current_user.is_superuser: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Get file path file_path = crud.file_storage.get_file_content(stored_file) if not file_path: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File content not found" ) return FileResponse( path=file_path, filename=stored_file.original_filename, media_type=stored_file.content_type ) @router.put("/{file_id}", response_model=StoredFileSchema) def update_file( file_id: str, file_in: FileUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Update file metadata. Users can only update their own files. """ stored_file = crud.file_storage.get(db, id=file_id) if not stored_file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Check ownership if stored_file.uploaded_by != current_user.id and not current_user.is_superuser: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) updated = crud.file_storage.update(db, db_obj=stored_file, obj_in=file_in) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="update", resource_type="file", resource_id=file_id, details={"filename": stored_file.original_filename}, status="success" ) return file_to_schema(updated) @router.delete("/{file_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_file( file_id: str, permanent: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Delete a file. Users can only delete their own files. Superusers can permanently delete files. """ stored_file = crud.file_storage.get(db, id=file_id) if not stored_file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="File not found" ) # Check ownership if stored_file.uploaded_by != current_user.id and not current_user.is_superuser: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="delete", resource_type="file", resource_id=file_id, details={ "filename": stored_file.original_filename, "permanent": permanent }, status="success" ) if permanent and current_user.is_superuser: crud.file_storage.hard_delete(db, id=file_id) else: crud.file_storage.soft_delete(db, id=file_id) return None @router.get("/allowed-types/", response_model=List[str]) def get_allowed_types( current_user: User = Depends(get_current_user), ): """ Get list of allowed file types for upload. """ return ALLOWED_CONTENT_TYPES @router.get("/max-size/", response_model=dict) def get_max_size( current_user: User = Depends(get_current_user), ): """ Get maximum allowed file size. """ return { "max_size_bytes": MAX_FILE_SIZE, "max_size_mb": MAX_FILE_SIZE / (1024 * 1024) }