Files
MincedPad/backend/app/api/public.py
T
2026-07-14 12:48:42 +08:00

118 lines
3.6 KiB
Python

from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from starlette.background import BackgroundTask
from app import db as db_module
from app.db import get_db
from app.models import Item
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")
if forwarded:
return forwarded.split(",")[0].strip()
if request.client and request.client.host:
return request.client.host
return ""
@router.post("", response_model=ItemOut)
def create_item(
payload: ItemCreate,
request: Request,
db: Session = Depends(get_db),
):
item = items_service.create_text_item(db, payload, _client_ip(request))
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),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
):
rows, total = items_service.list_wall(db, page=page, page_size=page_size)
return WallResponse(
items=[ItemOut.model_validate(items_service.item_to_dict(i)) for i in rows],
page=page,
page_size=page_size,
total=total,
)
def _burn_file_after_response(item_id: int) -> None:
db = db_module.SessionLocal()
try:
item = db.get(Item, item_id)
if item is not None and item.burn_after_read and not item.burned:
items_service.burn_file_item(db, item)
finally:
db.close()
@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)
background = None
if item.burn_after_read:
background = BackgroundTask(_burn_file_after_response, item.id)
return FileResponse(
path,
media_type=media_type,
filename=filename,
content_disposition_type="inline" if inline else "attachment",
background=background,
)
@router.get("/{slug}", response_model=ItemOut)
def get_item(slug: str, db: Session = Depends(get_db)):
item = items_service.get_item(db, slug)
if item is None:
raise HTTPException(status_code=404, detail="Not found")
return items_service.item_to_dict(item)