package handlers import ( "bytes" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "warpbox.dev/backend/libs/helpers" "warpbox.dev/backend/libs/services" "warpbox.dev/backend/libs/web" ) type downloadPageData struct { Box boxView Files []fileView ZipURL string Locked bool Obfuscated bool CanPreview bool DownloadCount int MaxDownloads int ExpiresLabel string } type boxView struct { ID string } type fileView struct { ID string Name string Size string ContentType string PreviewKind string URL string DownloadURL string ThumbnailURL string } type previewPageData struct { Box boxView File fileView Locked bool DownloadURL string } func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("download page missing box", "source", "download", "severity", "warn", "code", 4040, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r)) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("download page unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error()) a.renderPage(w, r, http.StatusForbidden, "download.html", web.PageData{ Title: "Download unavailable", Description: "This Warpbox link is no longer available.", Data: downloadPageData{ Box: boxView{ID: box.ID}, ExpiresLabel: err.Error(), }, }) return } locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) files := make([]fileView, 0, len(box.Files)) if !(locked && box.Obfuscate) { for _, file := range box.Files { files = append(files, a.fileView(box, file)) } } expiresLabel := boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST") title := "Shared files on Warpbox" description := fmt.Sprintf("%d file%s shared via Warpbox ยท expires %s", len(box.Files), plural(len(box.Files)), expiresLabel) if locked && box.Obfuscate { title = "Protected Warpbox link" description = "This shared box is password protected." } a.renderPage(w, r, http.StatusOK, "download.html", web.PageData{ Title: title, Description: description, ImageURL: absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID)), Data: downloadPageData{ Box: boxView{ID: box.ID}, Files: files, ZipURL: fmt.Sprintf("/d/%s/zip", box.ID), Locked: locked, Obfuscated: box.Obfuscate, DownloadCount: box.DownloadCount, MaxDownloads: box.MaxDownloads, ExpiresLabel: expiresLabel, }, }) a.logger.Info("download page viewed", "source", "download", "severity", "user_activity", "code", 2003, "box_id", box.ID, "ip", uploadClientIP(r), "locked", locked) } func plural(n int) string { if n == 1 { return "" } return "s" } func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) { box, file, ok := a.loadFileForRequest(w, r) if !ok { return } locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) view := a.fileView(box, file) title := file.Name description := fmt.Sprintf("%s shared via Warpbox", helpers.FormatBytes(file.Size)) imageURL := absoluteURL(r, view.ThumbnailURL) if locked && box.Obfuscate { title = "Protected Warpbox file" description = "This shared file is password protected." imageURL = absoluteURL(r, "/static/img/file-placeholder.webp") } a.renderPage(w, r, http.StatusOK, "preview.html", web.PageData{ Title: title, Description: description, ImageURL: imageURL, Data: previewPageData{ Box: boxView{ID: box.ID}, File: view, Locked: locked, DownloadURL: view.DownloadURL, }, }) a.logger.Info("file preview page viewed", "source", "download", "severity", "user_activity", "code", 2004, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r)) } func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) { box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { a.logger.Warn("protected file download blocked", "source", "download", "severity", "warn", "code", 4013, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r)) http.Error(w, "password required", http.StatusUnauthorized) return } a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1") a.logger.Info("file content served", "source", "download", "severity", "user_activity", "code", 2005, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r), "attachment", r.URL.Query().Get("inline") != "1") } func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) { box, file, ok := a.loadFileForRequest(w, r) if !ok { return } if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) { a.servePlaceholderThumbnail(w, r) return } object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file) if err != nil { // The thumbnail isn't generated yet (background job pending). Serve the // placeholder but mark it non-cacheable, otherwise the browser would // keep showing the placeholder until a hard refresh once the real // thumbnail lands. The real thumbnail below is content-stable, so it // gets a long immutable cache. a.servePlaceholderThumbnail(w, r) return } defer object.Body.Close() w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body)) } // servePlaceholderThumbnail serves the fallback image with no-store so the // browser re-requests on the next load and picks up the real thumbnail as soon // as it has been generated. func (a *App) servePlaceholderThumbnail(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store, must-revalidate") http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp")) } func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { http.NotFound(w, r) return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) return } if !a.uploadService.VerifyPassword(box, r.FormValue("password")) { a.logger.Warn("box unlock failed", "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID, "ip", uploadClientIP(r)) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) return } http.SetCookie(w, &http.Cookie{ Name: unlockCookieName(box.ID), Value: a.uploadService.UnlockToken(box), Path: "/d/" + box.ID, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: box.ExpiresAt, }) a.logger.Info("box unlocked", "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID, "ip", uploadClientIP(r)) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) } func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (services.Box, services.File, bool) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("file request missing box", "source", "download", "severity", "warn", "code", 4041, "box_id", r.PathValue("boxID"), "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r)) http.NotFound(w, r) return services.Box{}, services.File{}, false } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("file request unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r), "error", err.Error()) http.Error(w, err.Error(), statusForDownloadError(err)) return services.Box{}, services.File{}, false } file, err := a.uploadService.FindFile(box, r.PathValue("fileID")) if err != nil { a.logger.Warn("file request missing file", "source", "download", "severity", "warn", "code", 4042, "box_id", box.ID, "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r)) http.NotFound(w, r) return services.Box{}, services.File{}, false } return box, file, true } func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) { object, err := a.uploadService.OpenFileObject(r.Context(), box, file) if err != nil { a.logger.Warn("file object missing", "source", "download", "severity", "warn", "code", 4043, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r), "error", err.Error()) http.NotFound(w, r) return } defer object.Body.Close() w.Header().Set("Content-Type", file.ContentType) if attachment { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name)) } if seeker, ok := object.Body.(io.ReadSeeker); ok { http.ServeContent(w, r, file.Name, object.ModTime, seeker) } else { if object.Size > 0 { w.Header().Set("Content-Length", fmt.Sprintf("%d", object.Size)) } w.WriteHeader(http.StatusOK) _, _ = io.Copy(w, object.Body) } if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) { a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error()) } } func readSeekCloser(source io.ReadCloser) io.ReadSeeker { data, err := io.ReadAll(source) if err != nil { return bytes.NewReader(nil) } return bytes.NewReader(data) } func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { a.logger.Warn("zip request missing box", "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r)) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { a.logger.Warn("zip request unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error()) http.Error(w, err.Error(), statusForDownloadError(err)) return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { a.logger.Warn("protected zip download blocked", "source", "download", "severity", "warn", "code", 4014, "box_id", box.ID, "ip", uploadClientIP(r)) http.Error(w, "password required", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", "warpbox-"+box.ID+".zip")) w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) if err := a.uploadService.WriteZip(w, box); err != nil { a.logger.Error("zip download failed", "source", "download", "severity", "error", "code", 5002, "box_id", box.ID, "error", err.Error()) return } if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) { a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error()) } a.logger.Info("zip downloaded", "source", "download", "severity", "user_activity", "code", 2006, "box_id", box.ID, "ip", uploadClientIP(r), "files", len(box.Files)) } func (a *App) fileView(box services.Box, file services.File) fileView { return fileView{ ID: file.ID, Name: file.Name, Size: helpers.FormatBytes(file.Size), ContentType: file.ContentType, PreviewKind: file.PreviewKind, URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID), DownloadURL: fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID), ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID), } } func (a *App) isBoxUnlocked(r *http.Request, box services.Box) bool { if !a.uploadService.IsProtected(box) { return true } cookie, err := r.Cookie(unlockCookieName(box.ID)) if err != nil { return false } return cookie.Value == a.uploadService.UnlockToken(box) } func unlockCookieName(boxID string) string { return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID) } // neverExpires reports whether a box's expiry is far enough out to be treated as // "forever" (set via the unlimited / -1 expiry option). func neverExpires(t time.Time) bool { return time.Until(t) > 50*365*24*time.Hour } // boxExpiryLabel formats a box's expiry with the given layout, rendering // "forever" boxes as "Never" instead of a meaningless far-future date. func boxExpiryLabel(t time.Time, layout string) string { if neverExpires(t) { return "Never" } return t.Format(layout) } func absoluteURL(r *http.Request, path string) string { if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path } scheme := "http" if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { scheme = "https" } return fmt.Sprintf("%s://%s%s", scheme, r.Host, path) }