feat: file upload, allowlist, and download/preview

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
肉末
2026-07-14 12:47:58 +08:00
parent 0e59936e34
commit eda7010cae
4 changed files with 219 additions and 7 deletions
+52 -2
View File
@@ -1,12 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.db import get_db
from app.schemas import ItemCreate, ItemOut, WallResponse
from app.schemas import ItemCreate, ItemOut, TTLOption, WallResponse
from app.services import items as items_service
from app.services import storage as storage_service
router = APIRouter(prefix="/api/items", tags=["items"])
_INLINE_IMAGE_PREFIXES = ("image/",)
def _client_ip(request: Request) -> str:
forwarded = request.headers.get("x-forwarded-for")
@@ -27,6 +31,32 @@ def create_item(
return items_service.item_to_dict(item)
@router.post("/upload", response_model=ItemOut)
async def upload_item(
request: Request,
db: Session = Depends(get_db),
file: UploadFile = File(...),
is_public: bool = Form(True),
burn_after_read: bool = Form(False),
ttl: TTLOption = Form("24h"),
title: str | None = Form(None),
):
rel_path, size, mime, safe_name = await storage_service.save_upload(file)
item = items_service.create_file_item(
db,
file_name=safe_name,
file_path=rel_path,
mime=mime,
size_bytes=size,
is_public=is_public,
burn_after_read=burn_after_read,
ttl=ttl,
created_ip=_client_ip(request),
title=title,
)
return items_service.item_to_dict(item)
@router.get("/wall", response_model=WallResponse)
def wall(
page: int = Query(1, ge=1),
@@ -42,6 +72,26 @@ def wall(
)
@router.get("/{slug}/file")
def download_file(slug: str, db: Session = Depends(get_db)):
item = items_service.get_file_item(db, slug)
if item is None:
raise HTTPException(status_code=404, detail="Not found")
path = items_service.resolve_file_path(item)
if path is None or not path.is_file():
raise HTTPException(status_code=404, detail="Not found")
media_type = item.mime or "application/octet-stream"
filename = item.file_name or path.name
inline = media_type.startswith(_INLINE_IMAGE_PREFIXES)
return FileResponse(
path,
media_type=media_type,
filename=filename,
content_disposition_type="inline" if inline else "attachment",
)
@router.get("/{slug}", response_model=ItemOut)
def get_item(slug: str, db: Session = Depends(get_db)):
item = items_service.get_item(db, slug)