from __future__ import annotations import json import os from datetime import datetime from pathlib import Path from typing import Any import uuid def _now_iso() -> str: return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") def save_upload(project_dir: Path, file_storage) -> dict[str, Any]: """Save uploaded file under project media directory and generate thumbnail. Returns metadata dict. """ uploads_dir = project_dir / "media" / "images" date_folder = datetime.utcnow().strftime("%Y-%m") target_dir = uploads_dir / date_folder target_dir.mkdir(parents=True, exist_ok=True) original_name = file_storage.filename or "upload" ext = Path(original_name).suffix.lower() or ".bin" safe_name = f"{uuid.uuid4().hex}{ext}" target_path = target_dir / safe_name # save original file_storage.save(str(target_path)) # try to generate thumbnail for common image types thumb_name = None try: from PIL import Image if ext in [".jpg", ".jpeg", ".png", ".webp", ".gif"]: im = Image.open(str(target_path)) im.thumbnail((400, 400)) thumb_name = f"{uuid.uuid4().hex}_thumb{ext}" thumb_path = target_dir / thumb_name im.save(str(thumb_path)) except Exception: thumb_name = None meta = { "filename": safe_name, "original_name": original_name, "url": f"/sites/{{slug}}/media/images/{date_folder}/{safe_name}", "thumb": (f"/sites/{{slug}}/media/images/{date_folder}/{thumb_name}" if thumb_name else None), "size": target_path.stat().st_size, "uploaded_at": _now_iso(), } # update uploads index uploads_index = uploads_dir / "uploads.json" data = {} if uploads_index.exists(): try: data = json.loads(uploads_index.read_text(encoding="utf-8")) except Exception: data = {} data.setdefault(date_folder, []).append({ "filename": safe_name, "original_name": original_name, "size": meta["size"], "uploaded_at": meta["uploaded_at"], }) uploads_index.parent.mkdir(parents=True, exist_ok=True) uploads_index.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") return meta def list_media(project_dir: Path) -> list[dict[str, Any]]: uploads_dir = project_dir / "media" / "images" uploads_index = uploads_dir / "uploads.json" if not uploads_index.exists(): return [] try: data = json.loads(uploads_index.read_text(encoding="utf-8")) except Exception: return [] items = [] for date_folder, files in data.items(): for f in files: items.append({ "date": date_folder, **f, }) return items def delete_media(project_dir: Path, rel_path: str) -> bool: target = (project_dir / rel_path).resolve() if not str(target).startswith(str(project_dir.resolve())): return False if not target.exists(): return False try: target.unlink() except Exception: return False return True