""" Files API - Upload and download files """ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File as FastAPIFile from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from pydantic import BaseModel from typing import List, Optional import os import uuid from pathlib import Path import aiofiles import mimetypes from core.database import get_db from core.models import File, User from api.auth import get_current_user router = APIRouter() # File storage configuration UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/tmp/uploads") Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) class FileResponse(BaseModel): id: int filename: str original_filename: str file_size: int mime_type: str uploaded_by: int channel_id: Optional[int] created_at: str download_url: str class Config: from_attributes = True @router.post("/upload", response_model=FileResponse) async def upload_file( file: UploadFile = FastAPIFile(...), channel_id: Optional[int] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Upload a file""" # Generate unique filename file_ext = os.path.splitext(file.filename)[1] unique_filename = f"{uuid.uuid4()}{file_ext}" file_path = os.path.join(UPLOAD_DIR, unique_filename) # Save file try: async with aiofiles.open(file_path, 'wb') as f: content = await file.read() await f.write(content) file_size = len(content) mime_type = file.content_type or mimetypes.guess_type(file.filename)[0] or "application/octet-stream" except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}") # Create file record file_record = File( filename=unique_filename, original_filename=file.filename, file_path=file_path, file_size=file_size, mime_type=mime_type, uploaded_by=current_user.id, channel_id=channel_id ) db.add(file_record) db.commit() db.refresh(file_record) return { **file_record.__dict__, "created_at": file_record.created_at.isoformat(), "download_url": f"/api/files/{file_record.id}/download" } @router.get("/{file_id}/download") async def download_file( file_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Download a file""" # Get file record file_record = db.query(File).filter(File.id == file_id).first() if not file_record: raise HTTPException(status_code=404, detail="File not found") # Check file exists if not os.path.exists(file_record.file_path): raise HTTPException(status_code=404, detail="File not found on disk") # Stream file def iterfile(): with open(file_record.file_path, mode="rb") as f: yield from f return StreamingResponse( iterfile(), media_type=file_record.mime_type, headers={ "Content-Disposition": f"attachment; filename={file_record.original_filename}" } ) @router.get("/", response_model=List[FileResponse]) async def list_files( channel_id: Optional[int] = None, limit: int = 50, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """List files (optionally filtered by channel)""" query = db.query(File) if channel_id: query = query.filter(File.channel_id == channel_id) files = query.order_by(File.id.desc()).limit(limit).all() return [ { **f.__dict__, "created_at": f.created_at.isoformat(), "download_url": f"/api/files/{f.id}/download" } for f in files ] @router.delete("/{file_id}") async def delete_file( file_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Delete a file""" file_record = db.query(File).filter(File.id == file_id).first() if not file_record: raise HTTPException(status_code=404, detail="File not found") # Check permissions (owner or admin) if file_record.uploaded_by != current_user.id and current_user.role.value != "admin": raise HTTPException(status_code=403, detail="Not authorized to delete this file") # Delete from disk try: if os.path.exists(file_record.file_path): os.remove(file_record.file_path) except Exception as e: pass # Continue even if disk delete fails # Delete record db.delete(file_record) db.commit() return {"message": "File deleted"}