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
This commit is contained in:
James 2026-02-12 17:27:12 -05:00
parent 1b49dac87f
commit f59c12e25c
2 changed files with 22 additions and 1 deletions

21
main.go
View File

@ -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) {

View File

@ -39,7 +39,7 @@
Share
</button>
{{if .Document.PDFPath}}
<a href="/pdf/{{.Document.ID}}" target="_blank" class="inline-flex items-center px-3 py-2 bg-brand-600 hover:bg-brand-700 text-white text-sm font-medium rounded-lg transition-all">
<a href="/pdf/{{.Document.ID}}?download=1" class="inline-flex items-center px-3 py-2 bg-brand-600 hover:bg-brand-700 text-white text-sm font-medium rounded-lg transition-all">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>