185 lines
6.3 KiB
Python
185 lines
6.3 KiB
Python
"""Editor draft routes — persisted in-progress gallery-editor sessions.
|
|
|
|
The gallery editor (image canvas) lets users layer edits on top of a
|
|
photo (or a blank canvas). Persisting those layered sessions to the
|
|
server makes them survive cache clears and roams across devices —
|
|
unlike the legacy per-image localStorage drafts.
|
|
|
|
Each draft carries:
|
|
- id — opaque uuid (the client never sees gallery-image ids
|
|
as draft ids, so blank-canvas drafts work too)
|
|
- source_image_id (nullable) — back-pointer for "this draft started as
|
|
an edit of GalleryImage X"
|
|
- payload — full JSON snapshot (layers as base64 PNG dataURLs,
|
|
offsets, opacities, etc.) the editor knows how to
|
|
rehydrate
|
|
- thumbnail — small data URL for the landing-list grid
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
|
|
from core.database import EditorDraft, SessionLocal
|
|
from src.auth_helpers import get_current_user
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DraftCreate(BaseModel):
|
|
name: Optional[str] = None
|
|
source_image_id: Optional[str] = None
|
|
width: Optional[int] = None
|
|
height: Optional[int] = None
|
|
payload: Dict[str, Any]
|
|
thumbnail: Optional[str] = None
|
|
|
|
|
|
class DraftUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
width: Optional[int] = None
|
|
height: Optional[int] = None
|
|
payload: Optional[Dict[str, Any]] = None
|
|
thumbnail: Optional[str] = None
|
|
|
|
|
|
def _owns(d: EditorDraft, user: Optional[str]) -> bool:
|
|
if user is None:
|
|
return True
|
|
return (d.owner or None) == user
|
|
|
|
|
|
def _summary(d: EditorDraft) -> Dict[str, Any]:
|
|
"""List-view representation — omits the bulky payload."""
|
|
return {
|
|
"id": d.id,
|
|
"name": d.name or "Untitled",
|
|
"source_image_id": d.source_image_id,
|
|
"width": d.width,
|
|
"height": d.height,
|
|
"thumbnail": d.thumbnail,
|
|
"created_at": d.created_at.isoformat() if d.created_at else None,
|
|
"updated_at": d.updated_at.isoformat() if d.updated_at else None,
|
|
}
|
|
|
|
|
|
def setup_editor_draft_routes() -> APIRouter:
|
|
router = APIRouter(tags=["editor-drafts"])
|
|
|
|
@router.get("/api/editor-drafts")
|
|
async def list_drafts(request: Request) -> Dict[str, List[Dict[str, Any]]]:
|
|
user = get_current_user(request)
|
|
db = SessionLocal()
|
|
try:
|
|
q = db.query(EditorDraft).filter(EditorDraft.is_active == True)
|
|
if user is not None:
|
|
q = q.filter(EditorDraft.owner == user)
|
|
rows = q.order_by(EditorDraft.updated_at.desc()).limit(200).all()
|
|
return {"drafts": [_summary(d) for d in rows]}
|
|
finally:
|
|
db.close()
|
|
|
|
@router.get("/api/editor-drafts/{draft_id}")
|
|
async def get_draft(request: Request, draft_id: str) -> Dict[str, Any]:
|
|
user = get_current_user(request)
|
|
db = SessionLocal()
|
|
try:
|
|
d = db.query(EditorDraft).filter(
|
|
EditorDraft.id == draft_id, EditorDraft.is_active == True
|
|
).first()
|
|
if not d or not _owns(d, user):
|
|
raise HTTPException(404, "Draft not found")
|
|
try:
|
|
payload = json.loads(d.payload) if d.payload else {}
|
|
except Exception:
|
|
payload = {}
|
|
return {
|
|
**_summary(d),
|
|
"payload": payload,
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
@router.post("/api/editor-drafts")
|
|
async def create_draft(request: Request, body: DraftCreate) -> Dict[str, Any]:
|
|
user = get_current_user(request)
|
|
db = SessionLocal()
|
|
try:
|
|
d = EditorDraft(
|
|
id=str(uuid.uuid4()),
|
|
owner=user,
|
|
name=(body.name or "Untitled")[:200],
|
|
source_image_id=body.source_image_id,
|
|
width=body.width,
|
|
height=body.height,
|
|
payload=json.dumps(body.payload or {}),
|
|
thumbnail=body.thumbnail,
|
|
)
|
|
db.add(d)
|
|
db.commit()
|
|
db.refresh(d)
|
|
return _summary(d)
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.warning(f"editor-draft create failed: {e}")
|
|
raise HTTPException(500, "Could not save draft")
|
|
finally:
|
|
db.close()
|
|
|
|
@router.put("/api/editor-drafts/{draft_id}")
|
|
async def update_draft(request: Request, draft_id: str, body: DraftUpdate) -> Dict[str, Any]:
|
|
user = get_current_user(request)
|
|
db = SessionLocal()
|
|
try:
|
|
d = db.query(EditorDraft).filter(
|
|
EditorDraft.id == draft_id, EditorDraft.is_active == True
|
|
).first()
|
|
if not d or not _owns(d, user):
|
|
raise HTTPException(404, "Draft not found")
|
|
if body.name is not None:
|
|
d.name = body.name[:200]
|
|
if body.width is not None:
|
|
d.width = body.width
|
|
if body.height is not None:
|
|
d.height = body.height
|
|
if body.payload is not None:
|
|
d.payload = json.dumps(body.payload)
|
|
if body.thumbnail is not None:
|
|
d.thumbnail = body.thumbnail
|
|
db.commit()
|
|
db.refresh(d)
|
|
return _summary(d)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.warning(f"editor-draft update failed: {e}")
|
|
raise HTTPException(500, "Could not update draft")
|
|
finally:
|
|
db.close()
|
|
|
|
@router.delete("/api/editor-drafts/{draft_id}")
|
|
async def delete_draft(request: Request, draft_id: str) -> Dict[str, str]:
|
|
user = get_current_user(request)
|
|
db = SessionLocal()
|
|
try:
|
|
d = db.query(EditorDraft).filter(EditorDraft.id == draft_id).first()
|
|
if not d or not _owns(d, user):
|
|
raise HTTPException(404, "Draft not found")
|
|
d.is_active = False
|
|
db.commit()
|
|
return {"status": "deleted", "id": draft_id}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(500, str(e))
|
|
finally:
|
|
db.close()
|
|
|
|
return router
|