94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
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
|