Spaces:
Runtime error
Runtime error
File size: 4,475 Bytes
00a4363 34b0226 00a4363 34b0226 00a4363 34b0226 00a4363 bdd1640 00a4363 34b0226 00a4363 0649f1b 00a4363 34b0226 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | """Browser uploads go straight to R2 with presigned PUTs; reads redirect to presigned GETs."""
import mimetypes
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import FileResponse, RedirectResponse
from pydantic import BaseModel, Field
from sqlalchemy import select
from app.auth import Ctx, get_ctx
from app.config import settings
from app.models import Upload
from app.services.storage import LocalStorage, keys, safe_filename, storage, verify_local_sig, workspace_owns_key
router = APIRouter(prefix="/api/storage", tags=["storage"])
ALLOWED_TYPES = {
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/svg+xml",
"audio/mpeg",
"audio/wav",
"audio/webm",
"audio/mp4",
"audio/ogg",
"video/mp4",
"application/pdf",
"text/markdown",
"text/plain",
"text/html",
"text/vtt",
}
class UploadStartIn(BaseModel):
filename: str = Field(max_length=300)
content_type: str
size_bytes: int = Field(gt=0)
@router.post("/uploads")
def start_upload(body: UploadStartIn, ctx: Ctx = Depends(get_ctx)):
if body.content_type not in ALLOWED_TYPES:
raise HTTPException(415, "File type not supported")
if body.size_bytes > settings.max_upload_bytes:
raise HTTPException(413, "File too large")
upload_id = uuid.uuid4()
key = keys.upload(ctx.workspace.id, upload_id, body.filename)
upload = Upload(
id=upload_id,
workspace_id=ctx.workspace.id,
user_id=ctx.user.id,
key=key,
filename=body.filename,
content_type=body.content_type,
)
ctx.db.add(upload)
ctx.db.commit()
return {
"upload_id": str(upload_id),
"key": key,
"upload_url": storage.presign_put(key, body.content_type, ttl=900),
"headers": {"Content-Type": body.content_type},
}
@router.post("/uploads/{upload_id}/complete")
def complete_upload(upload_id: uuid.UUID, ctx: Ctx = Depends(get_ctx)):
upload = ctx.db.scalar(
select(Upload).where(Upload.id == upload_id, Upload.workspace_id == ctx.workspace.id)
)
if not upload:
raise HTTPException(404, "Upload not found")
head = storage.head(upload.key)
if not head:
raise HTTPException(409, "Object not found in storage yet")
size = int(head.get("ContentLength") or 0)
if size > settings.max_upload_bytes:
storage.delete(upload.key)
raise HTTPException(413, "File too large")
upload.size_bytes = size
upload.status = "complete"
ctx.db.commit()
return {"upload_id": str(upload.id), "key": upload.key, "size_bytes": size}
@router.get("/objects/{key:path}")
def read_object(key: str, download: bool = False, ctx: Ctx = Depends(get_ctx)):
if not workspace_owns_key(ctx.workspace.id, key):
raise HTTPException(404, "Not found")
name = key.rsplit("/", 1)[-1] if download else None
return RedirectResponse(storage.presign_get(key, download_name=name), status_code=302)
# Local disk backend: the "presigned" URLs LocalStorage hands out point here.
local_router = APIRouter(prefix="/api/storage/local", tags=["storage"], include_in_schema=False)
def _local_path(method: str, key: str, exp: int, sig: str):
if not isinstance(storage, LocalStorage) or not verify_local_sig(method, key, exp, sig):
raise HTTPException(403, "Invalid or expired link")
try:
return storage.path(key)
except ValueError as exc:
raise HTTPException(404, "Not found") from exc
@local_router.get("/{key:path}")
def local_get(key: str, exp: int, sig: str, download: str | None = None):
path = _local_path("GET", key, exp, sig)
if not path.is_file():
raise HTTPException(404, "Not found")
media_type = mimetypes.guess_type(key)[0] or "application/octet-stream"
# FileResponse answers Range requests, so audio and video can seek.
return FileResponse(path, media_type=media_type, filename=safe_filename(download) if download else None,
content_disposition_type="attachment" if download else "inline")
@local_router.put("/{key:path}")
async def local_put(key: str, exp: int, sig: str, request: Request):
_local_path("PUT", key, exp, sig)
body = await request.body()
if len(body) > settings.max_upload_bytes:
raise HTTPException(413, "File too large")
storage.put_bytes(key, body)
return {"ok": True, "size_bytes": len(body)}
|