Files
grimlock/backend/api/files.py
JA 6d6b1d0fbb Session fixes: auth working, frontend files created, running locally
- Fixed circular imports in API files
- Created missing frontend lib files (api.ts, socket.ts, types.ts)
- Fixed register endpoint to return token instead of user
- Updated Anthropic client version
- Backend running locally on port 8000
- Frontend running on port 3000
- Authentication working
- Still need: channel response fix, WebSocket auth fix
2026-02-14 04:45:39 +00:00

170 lines
4.7 KiB
Python

"""
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"}