rswag-online/backend/app/api/upload.py

146 lines
3.7 KiB
Python

"""Design upload API — users upload their own artwork."""
import io
import re
import uuid
from datetime import date
from pathlib import Path
from fastapi import APIRouter, Form, HTTPException, UploadFile
from PIL import Image
from pydantic import BaseModel
from app.config import get_settings
from app.api.designs import design_service
router = APIRouter()
settings = get_settings()
ALLOWED_TYPES = {"image/png", "image/jpeg", "image/webp"}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
MIN_DIMENSION = 500
class UploadResponse(BaseModel):
slug: str
name: str
image_url: str
status: str
products: list[dict]
def slugify(text: str) -> str:
text = text.lower().strip()
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[\s_-]+', '-', text)
text = re.sub(r'^-+|-+$', '', text)
return text
@router.post("/upload", response_model=UploadResponse)
async def upload_design(
file: UploadFile,
name: str = Form(...),
space: str = Form("default"),
tags: str = Form(""),
):
"""Upload a custom design image."""
# Validate content type
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(400, "Only PNG, JPEG, and WebP files are accepted")
# Read file and check size
contents = await file.read()
if len(contents) > MAX_FILE_SIZE:
raise HTTPException(400, "File size must be under 10 MB")
# Open with Pillow and validate dimensions
try:
img = Image.open(io.BytesIO(contents))
except Exception:
raise HTTPException(400, "Could not read image file")
if img.width < MIN_DIMENSION or img.height < MIN_DIMENSION:
raise HTTPException(400, f"Image must be at least {MIN_DIMENSION}x{MIN_DIMENSION} pixels")
# Create slug
slug = slugify(name)
if not slug:
slug = f"upload-{uuid.uuid4().hex[:8]}"
# Check for existing design
design_dir = settings.designs_dir / "uploads" / slug
if design_dir.exists():
slug = f"{slug}-{uuid.uuid4().hex[:6]}"
design_dir = settings.designs_dir / "uploads" / slug
# Save image as PNG
design_dir.mkdir(parents=True, exist_ok=True)
img = img.convert("RGBA")
image_path = design_dir / f"{slug}.png"
img.save(str(image_path), "PNG")
# Build metadata
safe_name = name.replace('"', '\\"')
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else ["custom", "upload"]
tags_str = ", ".join(tag_list)
space_field = space if space != "default" else "all"
metadata_content = f"""name: "{safe_name}"
slug: {slug}
description: "Custom uploaded design"
tags: [{tags_str}]
space: {space_field}
category: uploads
created: "{date.today().isoformat()}"
author: user-upload
source:
file: {slug}.png
format: png
dimensions:
width: {img.width}
height: {img.height}
dpi: 300
color_profile: sRGB
products:
- type: sticker
provider: prodigi
sku: GLOBAL-STI-KIS-3X3
variants: [matte, gloss]
retail_price: 3.50
- type: shirt
provider: printful
sku: "71"
variants: [S, M, L, XL, 2XL]
retail_price: 29.99
- type: print
provider: prodigi
sku: GLOBAL-FAP-A4
variants: [matte, lustre]
retail_price: 12.99
status: draft
"""
metadata_path = design_dir / "metadata.yaml"
metadata_path.write_text(metadata_content)
# Clear design cache so the new upload is discoverable
design_service.clear_cache()
products = [
{"type": "sticker", "price": 3.50},
{"type": "shirt", "price": 29.99},
{"type": "print", "price": 12.99},
]
return UploadResponse(
slug=slug,
name=name,
image_url=f"/api/designs/{slug}/image",
status="draft",
products=products,
)