128 lines
5.0 KiB
Python
128 lines
5.0 KiB
Python
# upload.py — FastAPI route for upload + quota check + voice conversion
|
|
|
|
from fastapi import APIRouter, UploadFile, Form, HTTPException, Request, Depends
|
|
from slowapi import Limiter
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
from pathlib import Path
|
|
from convert_to_opus import convert_to_opus
|
|
from database import get_db
|
|
from models import UploadLog, UserQuota, User
|
|
from sqlalchemy import select
|
|
|
|
limiter = Limiter(key_func=get_remote_address)
|
|
router = APIRouter()
|
|
# # Not needed for SlowAPI ≥0.1.5
|
|
DATA_ROOT = Path("./data")
|
|
|
|
@limiter.limit("5/minute")
|
|
@router.post("/upload")
|
|
async def upload(request: Request, db = Depends(get_db), uid: str = Form(...), file: UploadFile = Form(...)):
|
|
from log import log_violation
|
|
try:
|
|
user_dir = DATA_ROOT / uid
|
|
user_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
raw_path = user_dir / ("raw." + file.filename.split(".")[-1])
|
|
import uuid
|
|
|
|
unique_name = str(uuid.uuid4()) + ".opus"
|
|
|
|
# Save temp upload FIRST
|
|
with open(raw_path, "wb") as f:
|
|
f.write(await file.read())
|
|
|
|
# Block music/singing via Ollama prompt
|
|
import requests
|
|
try:
|
|
with open(raw_path, "rb") as f:
|
|
audio = f.read()
|
|
res = requests.post("http://localhost:11434/api/generate", json={
|
|
"model": "whisper",
|
|
"prompt": "Does this audio contain music or singing? Answer yes or no only.",
|
|
"audio": audio
|
|
}, timeout=10)
|
|
resp = res.json().get("response", "").lower()
|
|
if "yes" in resp:
|
|
raw_path.unlink(missing_ok=True)
|
|
raise HTTPException(status_code=403, detail="Upload rejected: music or singing detected")
|
|
except Exception as ollama_err:
|
|
# fallback: allow, log if needed
|
|
pass
|
|
processed_path = user_dir / unique_name
|
|
|
|
# Block unconfirmed users (use ORM)
|
|
user = db.exec(select(User).where((User.username == uid) | (User.email == uid))).first()
|
|
# If result is a Row or tuple, extract the User object
|
|
if user is not None and not isinstance(user, User) and hasattr(user, "__getitem__"):
|
|
user = user[0]
|
|
from log import log_violation
|
|
log_violation("UPLOAD", request.client.host, uid, f"DEBUG: Incoming uid={uid}, user found={user}, confirmed={getattr(user, 'confirmed', None)}")
|
|
log_violation("UPLOAD", request.client.host, uid, f"DEBUG: After unpack, user={user}, type={type(user)}, confirmed={getattr(user, 'confirmed', None)}")
|
|
if not user or not hasattr(user, "confirmed") or not user.confirmed:
|
|
raw_path.unlink(missing_ok=True)
|
|
raise HTTPException(status_code=403, detail="Account not confirmed")
|
|
|
|
# DB-based quota check
|
|
quota = db.get(UserQuota, uid)
|
|
if quota and quota.storage_bytes >= 100 * 1024 * 1024:
|
|
raw_path.unlink(missing_ok=True)
|
|
raise HTTPException(status_code=400, detail="Quota exceeded")
|
|
|
|
try:
|
|
convert_to_opus(str(raw_path), str(processed_path))
|
|
except Exception as e:
|
|
raw_path.unlink(missing_ok=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
original_size = raw_path.stat().st_size
|
|
raw_path.unlink(missing_ok=True) # cleanup
|
|
|
|
# Always copy latest upload as stream.opus for redirect compatibility
|
|
import shutil
|
|
stream_path = user_dir / "stream.opus"
|
|
shutil.copy2(processed_path, stream_path)
|
|
|
|
# Also update ./data/{uid}/stream.opus for public stream listing
|
|
streams_dir = Path("data") / uid
|
|
streams_dir.mkdir(parents=True, exist_ok=True)
|
|
streams_stream_path = streams_dir / "stream.opus"
|
|
shutil.copy2(processed_path, streams_stream_path)
|
|
|
|
db.add(UploadLog(
|
|
uid=uid,
|
|
ip=request.client.host,
|
|
filename=file.filename,
|
|
size_bytes=original_size
|
|
))
|
|
|
|
# Store updated quota
|
|
size = processed_path.stat().st_size
|
|
quota = db.get(UserQuota, uid)
|
|
if not quota:
|
|
quota = UserQuota(uid=uid)
|
|
db.add(quota)
|
|
quota.storage_bytes += size
|
|
db.commit()
|
|
|
|
return {
|
|
"stream_url": f"http://localhost:8000/streams/{uid}/stream.opus",
|
|
"filename": file.filename,
|
|
"original_size": round(original_size / 1024, 1),
|
|
"quota": {
|
|
"used_mb": round(quota.storage_bytes / (1024 * 1024), 2)
|
|
}
|
|
}
|
|
except HTTPException as e:
|
|
# Already a JSON response, just re-raise
|
|
raise e
|
|
except Exception as e:
|
|
import traceback
|
|
tb = traceback.format_exc()
|
|
# Log and return a JSON error
|
|
try:
|
|
log_violation("UPLOAD", request.client.host, uid, f"Unexpected error: {type(e).__name__}: {str(e)}\n{tb}")
|
|
except Exception:
|
|
pass
|
|
return {"detail": f"Server error: {type(e).__name__}: {str(e)}"}
|