from pathlib import Path from typing import List from fastapi import APIRouter, File, HTTPException, UploadFile, status router = APIRouter(prefix="/settings", tags=["settings"]) TEMPLATES_DIR = Path("data/templates") ALLOWED_EXCEL = {".xlsx", ".xltx"} ALLOWED_WORD = {".docx", ".dotx"} ALLOWED_EXTENSIONS = ALLOWED_EXCEL | ALLOWED_WORD # Allowed MIME types when client sends Content-Type (validate if present) ALLOWED_MIME_TYPES = frozenset({ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx "application/vnd.openxmlformats-officedocument.spreadsheetml.template", # .xltx "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx "application/vnd.openxmlformats-officedocument.wordprocessingml.template", # .dotx }) def _ensure_templates_dir() -> Path: TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) return TEMPLATES_DIR @router.get("/templates", response_model=List[dict]) async def list_templates(): """List uploaded template files (name, type, size, mtime).""" _ensure_templates_dir() out: List[dict] = [] for f in sorted(TEMPLATES_DIR.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True): if not f.is_file(): continue suf = f.suffix.lower() if suf not in ALLOWED_EXTENSIONS: continue st = f.stat() out.append({ "name": f.name, "type": "excel" if suf in ALLOWED_EXCEL else "word", "size": st.st_size, "uploaded_at": st.st_mtime, }) return out @router.post("/templates/upload", status_code=status.HTTP_201_CREATED) async def upload_template(file: UploadFile = File(...)): """Upload a .xlsx, .xltx, .docx or .dotx template to data/templates/.""" suf = Path(file.filename or "").suffix.lower() if suf not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only .xlsx, .xltx, .docx and .dotx files are allowed.", ) content_type = (file.content_type or "").strip().split(";")[0].strip().lower() if content_type and content_type not in ALLOWED_MIME_TYPES: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid content type. Allowed: .xlsx, .xltx, .docx, .dotx Office formats.", ) dir_path = _ensure_templates_dir() dest = dir_path / (file.filename or "template" + suf) content = await file.read() dest.write_bytes(content) return {"name": dest.name, "path": str(dest)} def get_latest_excel_template() -> Path | None: """Return path to the most recently modified .xlsx or .xltx in data/templates, or None.""" if not TEMPLATES_DIR.exists(): return None excel_files = [ f for f in TEMPLATES_DIR.iterdir() if f.is_file() and f.suffix.lower() in ALLOWED_EXCEL ] if not excel_files: return None return max(excel_files, key=lambda p: p.stat().st_mtime) def get_quote_template_path(template_filename: str | None) -> Path: """Resolve quote template path: optional filename in data/templates or latest excel template or default.""" if template_filename: candidate = TEMPLATES_DIR / template_filename if candidate.is_file() and candidate.suffix.lower() in ALLOWED_EXCEL: return candidate latest = get_latest_excel_template() if latest: return latest default = Path("templates/quote_template.xlsx") return default