Exports: use Flask send_file with ASCII-safe filenames to avoid Latin-1 header errors; add slugified download_name
This commit is contained in:
25
app.py
25
app.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user