package handler import ( "archive/zip" "fmt" "html/template" "io" "log" "net/http" "strings" "time" ) var batchTmpl *template.Template func (h *Handler) Batch(w http.ResponseWriter, r *http.Request) { batchID := r.PathValue("id") if batchID == "" { http.NotFound(w, r) return } files, err := h.store.GetBatch(batchID) if err != nil { log.Printf("db get batch error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } if len(files) == 0 { http.NotFound(w, r) return } type fileData struct { ID string Filename string Size int64 ContentType string UploadedAt string ThumbType string PreviewURL string ViewURL string DownloadURL string } var items []fileData var totalSize int64 for _, f := range files { // Skip expired files if f.ExpiresAt != nil && f.ExpiresAt.Before(time.Now().UTC()) { continue } thumbType := "" previewURL := "" if strings.HasPrefix(f.ContentType, "image/") { thumbType = "image" } else if strings.HasPrefix(f.ContentType, "video/") { thumbType = "video" } else if strings.HasPrefix(f.ContentType, "audio/") { thumbType = "audio" } else if f.ContentType == "application/pdf" { thumbType = "pdf" } if thumbType != "" { if url, err := h.r2.PresignGet(r.Context(), f.R2Key, f.Filename, true); err == nil { previewURL = url } } totalSize += f.SizeBytes items = append(items, fileData{ ID: f.ID, Filename: f.Filename, Size: f.SizeBytes, ContentType: f.ContentType, UploadedAt: f.UploadedAt.Format("Jan 2, 2006 at 3:04 PM"), ThumbType: thumbType, PreviewURL: previewURL, ViewURL: "/f/" + f.ID + "/view", DownloadURL: "/f/" + f.ID + "/dl", }) } if len(items) == 0 { http.NotFound(w, r) return } data := map[string]any{ "BatchID": batchID, "Files": items, "Count": len(items), "TotalSize": totalSize, } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache, must-revalidate") batchTmpl.Execute(w, data) } // BatchDownload streams all batch files as a zip archive. func (h *Handler) BatchDownload(w http.ResponseWriter, r *http.Request) { batchID := r.PathValue("id") if batchID == "" { http.NotFound(w, r) return } files, err := h.store.GetBatch(batchID) if err != nil { log.Printf("db get batch error: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } if len(files) == 0 { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.zip"`, batchID)) zw := zip.NewWriter(w) defer zw.Close() for _, f := range files { if f.ExpiresAt != nil && f.ExpiresAt.Before(time.Now().UTC()) { continue } if f.PasswordHash != nil { continue // skip password-protected files } // Get presigned URL for this file dlURL, err := h.r2.PresignGet(r.Context(), f.R2Key, f.Filename, false) if err != nil { log.Printf("batch zip presign error for %s: %v", f.ID, err) continue } // Fetch the file from R2 resp, err := http.Get(dlURL) if err != nil { log.Printf("batch zip fetch error for %s: %v", f.ID, err) continue } fw, err := zw.Create(f.Filename) if err != nil { resp.Body.Close() log.Printf("batch zip create entry error: %v", err) continue } io.Copy(fw, resp.Body) resp.Body.Close() } }