From f59c12e25c8fe9de5f3132b4aa432f4b835e0086 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 12 Feb 2026 17:27:12 -0500 Subject: [PATCH] Add download link with pretty filename from document title - servePDF now supports ?download=1 query param - Looks up document title and uses it as the Content-Disposition filename - Download button on document page triggers actual download (not tab open) - Added sanitizeFilename helper for safe Content-Disposition values --- main.go | 21 +++++++++++++++++++++ templates/document.html | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 2a90f5f..762a714 100644 --- a/main.go +++ b/main.go @@ -275,6 +275,7 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { func servePDF(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") + download := r.URL.Query().Get("download") == "1" // Try PDF first, then TXT for _, ext := range []string{".pdf", ".txt"} { @@ -285,6 +286,13 @@ func servePDF(w http.ResponseWriter, r *http.Request) { } else { w.Header().Set("Content-Type", "text/plain") } + if download { + filename := hash + ext + if doc, err := GetDocument(hash); err == nil && doc.Title != "" { + filename = sanitizeFilename(doc.Title) + ext + } + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + } http.ServeFile(w, r, path) return } @@ -293,6 +301,13 @@ func servePDF(w http.ResponseWriter, r *http.Request) { // Try without extension path := filepath.Join(storeDir, hash) if _, err := os.Stat(path); err == nil { + if download { + filename := hash + if doc, err := GetDocument(hash); err == nil && doc.Title != "" { + filename = sanitizeFilename(doc.Title) + } + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + } http.ServeFile(w, r, path) return } @@ -300,6 +315,12 @@ func servePDF(w http.ResponseWriter, r *http.Request) { http.Error(w, "File not found", http.StatusNotFound) } +// sanitizeFilename removes characters unsafe for use in Content-Disposition filenames. +func sanitizeFilename(name string) string { + replacer := strings.NewReplacer(`"`, "'", "/", "-", "\\", "-", "\n", " ", "\r", "") + return replacer.Replace(name) +} + // API handlers func apiSearchHandler(w http.ResponseWriter, r *http.Request) { diff --git a/templates/document.html b/templates/document.html index 718472c..62e1637 100644 --- a/templates/document.html +++ b/templates/document.html @@ -39,7 +39,7 @@ Share {{if .Document.PDFPath}} - +