Exports: use Flask send_file with ASCII-safe filenames to avoid Latin-1 header errors; add slugified download_name

This commit is contained in:
2025-09-04 09:07:16 +03:00
parent 361f9d0bbe
commit cc767dcf46

25
app.py
View File

@@ -14,9 +14,12 @@ from flask import (
abort, abort,
flash, flash,
Response, Response,
send_file,
) )
from io import BytesIO from io import BytesIO
import re import re
import unicodedata
from urllib.parse import quote as urlquote
from xhtml2pdf import pisa # type: ignore from xhtml2pdf import pisa # type: ignore
from docx import Document # type: ignore from docx import Document # type: ignore
from htmldocx import HtmlToDocx # type: ignore from htmldocx import HtmlToDocx # type: ignore
@@ -284,17 +287,24 @@ def create_app():
html + "</body></html>" html + "</body></html>"
) )
def _safe_download_name(title: str, uid: str, ext: str) -> str:
base = (title or f"page-{uid[:8]}").strip()
ascii_base = unicodedata.normalize("NFKD", base).encode("ascii", "ignore").decode("ascii")
ascii_base = re.sub(r"[^A-Za-z0-9_.-]+", "_", ascii_base).strip("_") or f"page-{uid[:8]}"
return f"{ascii_base}.{ext}"
@app.route("/p/<string:uid>.pdf") @app.route("/p/<string:uid>.pdf")
def export_pdf(uid: str): def export_pdf(uid: str):
row = _fetch_page(uid) row = _fetch_page(uid)
title = row["title"] or f"page-{uid[:8]}" title = row["title"] or f"page-{uid[:8]}"
cleaned = _sanitize_html_for_pdf(row["html"]) cleaned = _sanitize_html_for_pdf(row["html"])
html_doc = _wrap_html_for_export(title, cleaned) html_doc = _wrap_html_for_export(title, cleaned)
out = BytesIO() out = BytesIO()
pisa.CreatePDF(src=html_doc, dest=out) pisa.CreatePDF(src=html_doc, dest=out)
out.seek(0) out.seek(0)
headers = {"Content-Disposition": f"attachment; filename={title}.pdf"} filename = _safe_download_name(title, uid, "pdf")
return Response(out.read(), mimetype="application/pdf", headers=headers) # send_file properly sets Content-Disposition with download_name
return send_file(out, mimetype="application/pdf", as_attachment=True, download_name=filename)
@app.route("/p/<string:uid>.docx") @app.route("/p/<string:uid>.docx")
def export_docx(uid: str): def export_docx(uid: str):
@@ -306,8 +316,13 @@ def create_app():
out = BytesIO() out = BytesIO()
doc.save(out) doc.save(out)
out.seek(0) out.seek(0)
headers = {"Content-Disposition": f"attachment; filename={title}.docx"} filename = _safe_download_name(title, uid, "docx")
return Response(out.read(), mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers=headers) return send_file(
out,
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
as_attachment=True,
download_name=filename,
)
# Optional: simple 404 page to keep things minimal # Optional: simple 404 page to keep things minimal
@app.errorhandler(404) @app.errorhandler(404)