package handler import ( "database/sql" "html/template" "log" "net/http" "strings" "time" ) var downloadTmpl *template.Template // isPreviewable returns true if the content type can be viewed inline in a browser. func isPreviewable(contentType string) bool { if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") || strings.HasPrefix(contentType, "text/") || contentType == "application/pdf" { return true } return false } // Download shows a file info page with view/download buttons. func (h *Handler) Download(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { http.NotFound(w, r) return } rec, err := h.store.Get(id) if err == sql.ErrNoRows { http.NotFound(w, r) return } if err != nil { log.Printf("db get error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } // Check expiry if rec.ExpiresAt != nil && rec.ExpiresAt.Before(time.Now().UTC()) { http.Error(w, "this file has expired", http.StatusGone) return } // Check password — accept individual file cookie OR batch cookie if rec.PasswordHash != nil { fileAuth, fileErr := r.Cookie("auth_" + id) batchAuthed := rec.BatchID != nil && checkBatchAuth(r, *rec.BatchID) if !batchAuthed && (fileErr != nil || fileAuth.Value != "granted") { http.Redirect(w, r, "/f/"+id+"/auth", http.StatusSeeOther) return } } // Determine preview type and generate presigned URL for inline rendering thumbType := "" // "image", "video", "audio", "pdf", or "" previewURL := "" if strings.HasPrefix(rec.ContentType, "image/") { thumbType = "image" } else if strings.HasPrefix(rec.ContentType, "video/") { thumbType = "video" } else if strings.HasPrefix(rec.ContentType, "audio/") { thumbType = "audio" } else if rec.ContentType == "application/pdf" { thumbType = "pdf" } if thumbType != "" { if url, err := h.r2.PresignGet(r.Context(), rec.R2Key, rec.Filename, true); err == nil { previewURL = url } } data := map[string]any{ "ID": rec.ID, "Filename": rec.Filename, "Size": rec.SizeBytes, "ContentType": rec.ContentType, "UploadedAt": rec.UploadedAt.Format("Jan 2, 2006 at 3:04 PM"), "Downloads": rec.DownloadCount, "Previewable": isPreviewable(rec.ContentType), "ViewURL": "/f/" + id + "/view", "DownloadURL": "/f/" + id + "/dl", "ThumbType": thumbType, "PreviewURL": previewURL, } if rec.ExpiresAt != nil { data["ExpiresAt"] = rec.ExpiresAt.Format("Jan 2, 2006 at 3:04 PM") } w.Header().Set("Content-Type", "text/html; charset=utf-8") downloadTmpl.Execute(w, data) } // DirectDownload generates a presigned URL and redirects (attachment disposition). func (h *Handler) DirectDownload(w http.ResponseWriter, r *http.Request) { h.serveFile(w, r, false) } // ViewFile generates a presigned URL and redirects (inline disposition). func (h *Handler) ViewFile(w http.ResponseWriter, r *http.Request) { h.serveFile(w, r, true) } func (h *Handler) serveFile(w http.ResponseWriter, r *http.Request, inline bool) { id := r.PathValue("id") if id == "" { http.NotFound(w, r) return } rec, err := h.store.Get(id) if err == sql.ErrNoRows { http.NotFound(w, r) return } if err != nil { log.Printf("db get error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } if rec.ExpiresAt != nil && rec.ExpiresAt.Before(time.Now().UTC()) { http.Error(w, "this file has expired", http.StatusGone) return } if rec.PasswordHash != nil { fileAuth, fileErr := r.Cookie("auth_" + id) batchAuthed := rec.BatchID != nil && checkBatchAuth(r, *rec.BatchID) if !batchAuthed && (fileErr != nil || fileAuth.Value != "granted") { http.Redirect(w, r, "/f/"+id+"/auth", http.StatusSeeOther) return } } url, err := h.r2.PresignGet(r.Context(), rec.R2Key, rec.Filename, inline) if err != nil { log.Printf("presign error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } h.store.IncrementDownloads(id) http.Redirect(w, r, url, http.StatusFound) }