2026-05-25 16:26:47 +03:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-31 02:14:10 +03:00
|
|
|
"bytes"
|
2026-05-25 16:26:47 +03:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-05-31 02:14:10 +03:00
|
|
|
"io"
|
2026-05-25 16:26:47 +03:00
|
|
|
"net/http"
|
|
|
|
|
"os"
|
2026-05-25 16:52:57 +03:00
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2026-05-25 16:26:47 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"warpbox.dev/backend/libs/helpers"
|
2026-05-25 16:52:57 +03:00
|
|
|
"warpbox.dev/backend/libs/services"
|
2026-05-25 16:26:47 +03:00
|
|
|
"warpbox.dev/backend/libs/web"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type downloadPageData struct {
|
|
|
|
|
Box boxView
|
|
|
|
|
Files []fileView
|
|
|
|
|
ZipURL string
|
2026-05-25 16:52:57 +03:00
|
|
|
Locked bool
|
|
|
|
|
Obfuscated bool
|
|
|
|
|
CanPreview bool
|
2026-05-25 16:26:47 +03:00
|
|
|
DownloadCount int
|
|
|
|
|
MaxDownloads int
|
|
|
|
|
ExpiresLabel string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type boxView struct {
|
|
|
|
|
ID string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fileView struct {
|
2026-05-25 16:52:57 +03:00
|
|
|
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
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
|
|
|
|
if err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("download page missing box", "source", "download", "severity", "warn", "code", 4040, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r))
|
2026-05-25 16:26:47 +03:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := a.uploadService.CanDownload(box); err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("download page unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error())
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusForbidden, "download.html", web.PageData{
|
2026-05-25 16:26:47 +03:00
|
|
|
Title: "Download unavailable",
|
|
|
|
|
Description: "This Warpbox link is no longer available.",
|
|
|
|
|
Data: downloadPageData{
|
|
|
|
|
Box: boxView{ID: box.ID},
|
|
|
|
|
ExpiresLabel: err.Error(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box)
|
2026-05-25 16:26:47 +03:00
|
|
|
|
|
|
|
|
files := make([]fileView, 0, len(box.Files))
|
2026-05-25 16:52:57 +03:00
|
|
|
if !(locked && box.Obfuscate) {
|
|
|
|
|
for _, file := range box.Files {
|
|
|
|
|
files = append(files, a.fileView(box, file))
|
|
|
|
|
}
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 22:40:48 +03:00
|
|
|
expiresLabel := boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST")
|
2026-05-31 17:57:56 +03:00
|
|
|
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."
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusOK, "download.html", web.PageData{
|
2026-05-31 17:57:56 +03:00
|
|
|
Title: title,
|
|
|
|
|
Description: description,
|
|
|
|
|
ImageURL: absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID)),
|
2026-05-25 16:26:47 +03:00
|
|
|
Data: downloadPageData{
|
|
|
|
|
Box: boxView{ID: box.ID},
|
|
|
|
|
Files: files,
|
|
|
|
|
ZipURL: fmt.Sprintf("/d/%s/zip", box.ID),
|
2026-05-25 16:52:57 +03:00
|
|
|
Locked: locked,
|
|
|
|
|
Obfuscated: box.Obfuscate,
|
2026-05-25 16:26:47 +03:00
|
|
|
DownloadCount: box.DownloadCount,
|
|
|
|
|
MaxDownloads: box.MaxDownloads,
|
2026-05-31 17:57:56 +03:00
|
|
|
ExpiresLabel: expiresLabel,
|
2026-05-25 16:26:47 +03:00
|
|
|
},
|
|
|
|
|
})
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Info("download page viewed", "source", "download", "severity", "user_activity", "code", 2003, "box_id", box.ID, "ip", uploadClientIP(r), "locked", locked)
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 17:57:56 +03:00
|
|
|
func plural(n int) string {
|
|
|
|
|
if n == 1 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return "s"
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:26:47 +03:00
|
|
|
func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
2026-05-25 16:52:57 +03:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusOK, "preview.html", web.PageData{
|
2026-05-25 16:52:57 +03:00
|
|
|
Title: title,
|
|
|
|
|
Description: description,
|
|
|
|
|
ImageURL: imageURL,
|
|
|
|
|
Data: previewPageData{
|
|
|
|
|
Box: boxView{ID: box.ID},
|
|
|
|
|
File: view,
|
|
|
|
|
Locked: locked,
|
|
|
|
|
DownloadURL: view.DownloadURL,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-05-31 21:52:56 +03:00
|
|
|
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))
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("protected file download blocked", "source", "download", "severity", "warn", "code", 4013, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r))
|
2026-05-25 16:52:57 +03:00
|
|
|
http.Error(w, "password required", http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1")
|
2026-05-31 21:52:56 +03:00
|
|
|
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")
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-05-31 17:57:56 +03:00
|
|
|
a.servePlaceholderThumbnail(w, r)
|
2026-05-25 16:52:57 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file)
|
|
|
|
|
if err != nil {
|
2026-05-31 17:57:56 +03:00
|
|
|
// 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)
|
2026-05-25 16:52:57 +03:00
|
|
|
return
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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))
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 17:57:56 +03:00
|
|
|
// 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"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) {
|
2026-05-25 16:26:47 +03:00
|
|
|
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
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")) {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("box unlock failed", "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID, "ip", uploadClientIP(r))
|
2026-05-25 16:52:57 +03:00
|
|
|
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,
|
|
|
|
|
})
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Info("box unlocked", "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID, "ip", uploadClientIP(r))
|
2026-05-25 16:52:57 +03:00
|
|
|
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 {
|
2026-05-31 21:52:56 +03:00
|
|
|
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))
|
2026-05-25 16:52:57 +03:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return services.Box{}, services.File{}, false
|
|
|
|
|
}
|
2026-05-25 16:26:47 +03:00
|
|
|
if err := a.uploadService.CanDownload(box); err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
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())
|
2026-05-25 16:26:47 +03:00
|
|
|
http.Error(w, err.Error(), statusForDownloadError(err))
|
2026-05-25 16:52:57 +03:00
|
|
|
return services.Box{}, services.File{}, false
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := a.uploadService.FindFile(box, r.PathValue("fileID"))
|
|
|
|
|
if err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
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))
|
2026-05-25 16:26:47 +03:00
|
|
|
http.NotFound(w, r)
|
2026-05-25 16:52:57 +03:00
|
|
|
return services.Box{}, services.File{}, false
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
return box, file, true
|
|
|
|
|
}
|
2026-05-25 16:26:47 +03:00
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) {
|
2026-05-31 02:14:10 +03:00
|
|
|
object, err := a.uploadService.OpenFileObject(r.Context(), box, file)
|
2026-05-25 16:26:47 +03:00
|
|
|
if err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
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())
|
2026-05-25 16:26:47 +03:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
defer object.Body.Close()
|
2026-05-25 16:26:47 +03:00
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", file.ContentType)
|
2026-05-25 16:52:57 +03:00
|
|
|
if attachment {
|
|
|
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name))
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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)
|
|
|
|
|
}
|
2026-05-25 16:26:47 +03:00
|
|
|
|
|
|
|
|
if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
|
2026-05-25 16:52:57 +03:00
|
|
|
a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error())
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
func readSeekCloser(source io.ReadCloser) io.ReadSeeker {
|
|
|
|
|
data, err := io.ReadAll(source)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return bytes.NewReader(nil)
|
|
|
|
|
}
|
|
|
|
|
return bytes.NewReader(data)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:26:47 +03:00
|
|
|
func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
|
|
|
|
if err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("zip request missing box", "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r))
|
2026-05-25 16:26:47 +03:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := a.uploadService.CanDownload(box); err != nil {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("zip request unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error())
|
2026-05-25 16:26:47 +03:00
|
|
|
http.Error(w, err.Error(), statusForDownloadError(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) {
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Warn("protected zip download blocked", "source", "download", "severity", "warn", "code", 4014, "box_id", box.ID, "ip", uploadClientIP(r))
|
2026-05-25 16:52:57 +03:00
|
|
|
http.Error(w, "password required", http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-25 16:26:47 +03:00
|
|
|
|
|
|
|
|
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 {
|
2026-05-25 16:52:57 +03:00
|
|
|
a.logger.Error("zip download failed", "source", "download", "severity", "error", "code", 5002, "box_id", box.ID, "error", err.Error())
|
2026-05-25 16:26:47 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
|
2026-05-25 16:52:57 +03:00
|
|
|
a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error())
|
|
|
|
|
}
|
2026-05-31 21:52:56 +03:00
|
|
|
a.logger.Info("zip downloaded", "source", "download", "severity", "user_activity", "code", 2006, "box_id", box.ID, "ip", uploadClientIP(r), "files", len(box.Files))
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 22:40:48 +03:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
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"
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
return fmt.Sprintf("%s://%s%s", scheme, r.Host, path)
|
2026-05-25 16:26:47 +03:00
|
|
|
}
|