- 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
170 lines
4.7 KiB
Python
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"}
|