2026-04-27 17:49:19 +03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"archive/zip"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
2026-04-28 21:11:37 +03:00
|
|
|
"strings"
|
2026-04-28 21:42:36 +03:00
|
|
|
"sync"
|
2026-04-27 18:18:53 +03:00
|
|
|
"time"
|
2026-04-27 17:49:19 +03:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
"warpbox/lib/boxstore"
|
|
|
|
|
"warpbox/lib/helpers"
|
2026-04-27 17:49:19 +03:00
|
|
|
"warpbox/lib/models"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
const boxAuthCookiePrefix = "warpbox_box_"
|
|
|
|
|
|
2026-04-28 21:42:36 +03:00
|
|
|
var oneTimeDownloadLocks sync.Map
|
|
|
|
|
|
2026-04-29 02:29:49 +03:00
|
|
|
func formatBrowserTime(value time.Time) string {
|
|
|
|
|
if value.IsZero() {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return value.UTC().Format(time.RFC3339)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleIndex(ctx *gin.Context) {
|
2026-04-27 18:18:53 +03:00
|
|
|
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
2026-04-28 21:11:37 +03:00
|
|
|
"RetentionOptions": app.retentionOptions(),
|
|
|
|
|
"DefaultRetention": app.defaultRetentionOption().Key,
|
|
|
|
|
"UploadsEnabled": app.config.GuestUploadsEnabled && app.config.APIEnabled,
|
|
|
|
|
"MaxFileSizeBytes": app.config.GlobalMaxFileSizeBytes,
|
|
|
|
|
"MaxBoxSizeBytes": app.config.GlobalMaxBoxSizeBytes,
|
2026-04-27 18:18:53 +03:00
|
|
|
})
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleShowBox(ctx *gin.Context) {
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
2026-04-27 18:01:02 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
|
2026-04-27 18:18:53 +03:00
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
files, err := boxstore.ListFiles(boxID)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-30 04:24:49 +03:00
|
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
|
|
|
files = stripOneTimeThumbnailState(files)
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
downloadAll := "/box/" + boxID + "/download"
|
2026-04-28 21:11:37 +03:00
|
|
|
if !app.config.ZipDownloadsEnabled || hasManifest && manifest.DisableZip {
|
2026-04-27 18:18:53 +03:00
|
|
|
downloadAll = ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.HTML(http.StatusOK, "box.html", gin.H{
|
2026-04-27 18:18:53 +03:00
|
|
|
"BoxID": boxID,
|
|
|
|
|
"Files": files,
|
|
|
|
|
"FileCount": len(files),
|
|
|
|
|
"DownloadAll": downloadAll,
|
2026-04-28 19:41:23 +03:00
|
|
|
"ZipOnly": hasManifest && manifest.OneTimeDownload,
|
2026-04-28 21:11:37 +03:00
|
|
|
"PollMS": app.config.BoxPollIntervalMS,
|
2026-04-27 18:18:53 +03:00
|
|
|
"RetentionLabel": manifest.RetentionLabel,
|
|
|
|
|
"ExpiresAt": manifest.ExpiresAt,
|
2026-04-29 02:29:49 +03:00
|
|
|
"ExpiresAtISO": formatBrowserTime(manifest.ExpiresAt),
|
2026-04-27 17:49:19 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
func handleBoxLogin(ctx *gin.Context) {
|
|
|
|
|
boxID := ctx.Param("id")
|
|
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if boxstore.IsExpired(manifest) {
|
|
|
|
|
boxstore.DeleteBox(boxID)
|
|
|
|
|
ctx.String(http.StatusGone, "Box expired")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !boxstore.IsPasswordProtected(manifest) || isBoxAuthorized(ctx, boxID, manifest) {
|
|
|
|
|
ctx.Redirect(http.StatusSeeOther, "/box/"+boxID)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderBoxLogin(ctx, boxID, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleBoxLoginPost(ctx *gin.Context) {
|
|
|
|
|
boxID := ctx.Param("id")
|
|
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if boxstore.IsExpired(manifest) {
|
|
|
|
|
boxstore.DeleteBox(boxID)
|
|
|
|
|
ctx.String(http.StatusGone, "Box expired")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !boxstore.VerifyPassword(manifest, ctx.PostForm("password")) {
|
|
|
|
|
renderBoxLogin(ctx, boxID, "The password was not accepted.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxAge := 24 * 60 * 60
|
|
|
|
|
if !manifest.ExpiresAt.IsZero() {
|
|
|
|
|
seconds := int(time.Until(manifest.ExpiresAt).Seconds())
|
|
|
|
|
if seconds > 0 {
|
|
|
|
|
maxAge = seconds
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.SetCookie(boxAuthCookieName(boxID), manifest.AuthToken, maxAge, "/box/"+boxID, "", false, true)
|
|
|
|
|
ctx.Redirect(http.StatusSeeOther, "/box/"+boxID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleBoxStatus(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
2026-04-27 18:01:02 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 04:24:49 +03:00
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, false)
|
2026-04-29 02:29:49 +03:00
|
|
|
if !ok {
|
2026-04-27 18:18:53 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 04:24:49 +03:00
|
|
|
var files []models.BoxFile
|
|
|
|
|
if hasManifest && manifestFilesReady(manifest.Files) {
|
|
|
|
|
files = boxstore.DecorateFiles(boxID, manifest.Files)
|
|
|
|
|
} else {
|
|
|
|
|
var err error
|
|
|
|
|
files, err = boxstore.ListFiles(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusNotFound, gin.H{"error": "Box not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
|
|
|
files = stripOneTimeThumbnailState(files)
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 02:29:49 +03:00
|
|
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "expires_at": formatBrowserTime(manifest.ExpiresAt), "files": files})
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleDownloadBox(ctx *gin.Context) {
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
2026-04-27 18:01:02 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
if !app.config.ZipDownloadsEnabled {
|
|
|
|
|
ctx.String(http.StatusForbidden, "Zip downloads are disabled")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
|
2026-04-27 18:18:53 +03:00
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
|
|
|
app.handleOneTimeDownloadBox(ctx, boxID)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 18:18:53 +03:00
|
|
|
|
|
|
|
|
if hasManifest && manifest.DisableZip {
|
|
|
|
|
ctx.String(http.StatusForbidden, "Zip download disabled for this box")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
files, err := boxstore.ListFiles(boxID)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if !app.writeBoxZip(ctx, boxID, files) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if hasManifest && app.config.RenewOnDownloadEnabled {
|
|
|
|
|
boxstore.RenewManifest(boxID, manifest.RetentionSecs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleOneTimeDownloadBox(ctx *gin.Context, boxID string) {
|
|
|
|
|
lock := oneTimeDownloadLock(boxID)
|
|
|
|
|
lock.Lock()
|
|
|
|
|
defer lock.Unlock()
|
|
|
|
|
defer oneTimeDownloadLocks.Delete(boxID)
|
|
|
|
|
|
|
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-30 03:54:50 +03:00
|
|
|
if !hasManifest || !manifest.OneTimeDownload || manifest.Consumed {
|
|
|
|
|
ctx.String(http.StatusGone, "Box already consumed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:42:36 +03:00
|
|
|
files, err := boxstore.ListFiles(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !allFilesComplete(files) {
|
2026-04-28 19:41:23 +03:00
|
|
|
ctx.String(http.StatusConflict, "Box is not ready yet")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-30 04:24:49 +03:00
|
|
|
|
|
|
|
|
if app.config.OneTimeDownloadRetryOnFailure {
|
|
|
|
|
app.handleRetryableOneTimeZip(ctx, boxID, manifest, files)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest.Consumed = true
|
|
|
|
|
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
|
|
|
|
|
ctx.String(http.StatusInternalServerError, "Could not mark box as consumed")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if !app.writeBoxZip(ctx, boxID, files) {
|
2026-04-30 04:24:49 +03:00
|
|
|
boxstore.DeleteBox(boxID)
|
2026-04-28 21:42:36 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
boxstore.DeleteBox(boxID)
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-28 21:42:36 +03:00
|
|
|
func (app *App) writeBoxZip(ctx *gin.Context, boxID string, files []models.BoxFile) bool {
|
2026-04-30 04:24:49 +03:00
|
|
|
writeBoxZipHeaders(ctx, boxID)
|
|
|
|
|
if err := writeBoxZipTo(ctx.Writer, boxID, files); err != nil {
|
|
|
|
|
ctx.Status(http.StatusInternalServerError)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) handleRetryableOneTimeZip(ctx *gin.Context, boxID string, manifest models.BoxManifest, files []models.BoxFile) {
|
|
|
|
|
tempZip, err := os.CreateTemp("", "warpbox-"+boxID+"-*.zip")
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.String(http.StatusInternalServerError, "Could not prepare ZIP download")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tempPath := tempZip.Name()
|
|
|
|
|
defer os.Remove(tempPath)
|
|
|
|
|
|
|
|
|
|
if err := writeBoxZipTo(tempZip, boxID, files); err != nil {
|
|
|
|
|
tempZip.Close()
|
|
|
|
|
ctx.String(http.StatusInternalServerError, "Could not build ZIP download")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if _, err := tempZip.Seek(0, 0); err != nil {
|
|
|
|
|
tempZip.Close()
|
|
|
|
|
ctx.String(http.StatusInternalServerError, "Could not read ZIP download")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeBoxZipHeaders(ctx, boxID)
|
|
|
|
|
if _, err := io.Copy(ctx.Writer, tempZip); err != nil {
|
|
|
|
|
tempZip.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := tempZip.Close(); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest.Consumed = true
|
|
|
|
|
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
boxstore.DeleteBox(boxID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeBoxZipHeaders(ctx *gin.Context, boxID string) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.Header("Content-Type", "application/zip")
|
|
|
|
|
ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID))
|
2026-04-30 04:24:49 +03:00
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-30 04:24:49 +03:00
|
|
|
func writeBoxZipTo(destination io.Writer, boxID string, files []models.BoxFile) error {
|
|
|
|
|
zipWriter := zip.NewWriter(destination)
|
2026-04-27 17:49:19 +03:00
|
|
|
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
if !file.IsComplete {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-27 18:01:02 +03:00
|
|
|
if err := boxstore.AddFileToZip(zipWriter, boxID, file.Name); err != nil {
|
2026-04-30 04:24:49 +03:00
|
|
|
return err
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-28 19:41:23 +03:00
|
|
|
|
|
|
|
|
if err := zipWriter.Close(); err != nil {
|
2026-04-30 04:24:49 +03:00
|
|
|
return err
|
2026-04-28 19:41:23 +03:00
|
|
|
}
|
2026-04-30 04:24:49 +03:00
|
|
|
return nil
|
2026-04-28 21:42:36 +03:00
|
|
|
}
|
2026-04-28 19:41:23 +03:00
|
|
|
|
2026-04-28 21:42:36 +03:00
|
|
|
func oneTimeDownloadLock(boxID string) *sync.Mutex {
|
|
|
|
|
lock, _ := oneTimeDownloadLocks.LoadOrStore(boxID, &sync.Mutex{})
|
|
|
|
|
return lock.(*sync.Mutex)
|
2026-04-28 19:41:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func allFilesComplete(files []models.BoxFile) bool {
|
|
|
|
|
if len(files) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
if !file.IsComplete {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 04:24:49 +03:00
|
|
|
func manifestFilesReady(files []models.BoxFile) bool {
|
|
|
|
|
if len(files) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
if file.Status != models.FileStatusReady {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stripOneTimeThumbnailState(files []models.BoxFile) []models.BoxFile {
|
|
|
|
|
stripped := make([]models.BoxFile, 0, len(files))
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
file.ThumbnailPath = nil
|
|
|
|
|
file.ThumbnailURL = ""
|
|
|
|
|
if file.ThumbnailStatus == "" {
|
|
|
|
|
file.ThumbnailStatus = models.ThumbnailStatusUnsupported
|
|
|
|
|
}
|
|
|
|
|
stripped = append(stripped, file)
|
|
|
|
|
}
|
|
|
|
|
return stripped
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleDownloadFile(ctx *gin.Context) {
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
2026-04-27 18:01:02 +03:00
|
|
|
filename, ok := helpers.SafeFilename(ctx.Param("filename"))
|
|
|
|
|
if !boxstore.ValidBoxID(boxID) || !ok {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.String(http.StatusBadRequest, "Invalid file")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
|
2026-04-28 19:41:23 +03:00
|
|
|
if !authorized {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
|
|
|
ctx.String(http.StatusForbidden, "Individual downloads disabled for this box")
|
2026-04-27 18:18:53 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
path, ok := boxstore.SafeBoxFilePath(boxID, filename)
|
2026-04-27 17:49:19 +03:00
|
|
|
if !ok {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid file")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "File not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if !boxstore.IsSafeRegularBoxFile(boxID, filename) {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid file")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
|
|
|
|
ctx.FileAttachment(path, filename)
|
2026-04-28 21:11:37 +03:00
|
|
|
if hasManifest && app.config.RenewOnDownloadEnabled {
|
|
|
|
|
boxstore.RenewManifest(boxID, manifest.RetentionSecs)
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleDownloadThumbnail(ctx *gin.Context) {
|
2026-04-28 18:44:16 +03:00
|
|
|
boxID := ctx.Param("id")
|
|
|
|
|
fileID := ctx.Param("file_id")
|
|
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 03:54:50 +03:00
|
|
|
manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
|
|
|
|
|
if !authorized {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
|
|
|
ctx.String(http.StatusForbidden, "Thumbnails disabled for one-time boxes")
|
2026-04-28 18:44:16 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path, ok := boxstore.ThumbnailFilePath(boxID, fileID)
|
|
|
|
|
if !ok {
|
|
|
|
|
ctx.String(http.StatusBadRequest, "Invalid thumbnail")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
|
|
|
ctx.String(http.StatusNotFound, "Thumbnail not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.Header("Content-Type", "image/jpeg")
|
|
|
|
|
ctx.File(path)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleCreateBox(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
app.limitRequestBody(ctx)
|
2026-04-28 21:11:37 +03:00
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
boxID, err := boxstore.NewBoxID()
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var request models.CreateBoxRequest
|
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil && err != io.EOF {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box payload"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:11:37 +03:00
|
|
|
if err := app.validateCreateBoxRequest(&request); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
files, err := boxstore.CreateManifest(boxID, request)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": files})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleManifestFileUpload(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
app.limitRequestBody(ctx)
|
2026-04-28 21:11:37 +03:00
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
|
|
|
|
fileID := ctx.Param("file_id")
|
2026-04-27 18:01:02 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := ctx.FormFile("file")
|
|
|
|
|
if err != nil {
|
2026-04-27 18:01:02 +03:00
|
|
|
boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:11:37 +03:00
|
|
|
if err := app.validateManifestFileUpload(boxID, fileID, file.Size); err != nil {
|
|
|
|
|
boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
savedFile, err := boxstore.SaveManifestUpload(boxID, fileID, file)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
2026-04-27 18:01:02 +03:00
|
|
|
boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleFileStatusUpdate(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
app.limitRequestBody(ctx)
|
2026-04-28 21:11:37 +03:00
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
|
|
|
|
fileID := ctx.Param("file_id")
|
2026-04-28 21:42:36 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) || !helpers.ValidLowerHexID(fileID, 16) {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file"})
|
2026-04-27 17:49:19 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var request models.UpdateFileStatusRequest
|
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status payload"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if request.Status == models.FileStatusReady {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Uploads must complete through the upload endpoint"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := app.rejectExpiredManifestBox(boxID); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusGone, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
file, err := boxstore.MarkFileStatus(boxID, fileID, request.Status)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"file": file})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleDirectBoxUpload(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
app.limitRequestBody(ctx)
|
2026-04-28 21:11:37 +03:00
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
boxID := ctx.Param("id")
|
2026-04-27 18:01:02 +03:00
|
|
|
if !boxstore.ValidBoxID(boxID) {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := ctx.FormFile("file")
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:11:37 +03:00
|
|
|
if err := app.validateIncomingFile(boxID, file.Size); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
savedFile, err := boxstore.SaveUpload(boxID, file)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) handleLegacyUpload(ctx *gin.Context) {
|
|
|
|
|
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
app.limitRequestBody(ctx)
|
2026-04-28 21:11:37 +03:00
|
|
|
|
2026-04-27 17:49:19 +03:00
|
|
|
form, err := ctx.MultipartForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files := form.File["files"]
|
|
|
|
|
if len(files) == 0 {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-28 21:11:37 +03:00
|
|
|
totalSize := int64(0)
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
if err := app.validateFileSize(file.Size); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
|
|
|
|
if err := app.validateBoxSize(totalSize); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
boxID, err := boxstore.NewBoxID()
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:01:02 +03:00
|
|
|
if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil {
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 01:16:17 +03:00
|
|
|
retentionKey := strings.TrimSpace(ctx.PostForm("retention_key"))
|
|
|
|
|
if retentionKey == "" {
|
|
|
|
|
retentionKey = strings.TrimSpace(ctx.PostForm("retention"))
|
|
|
|
|
}
|
|
|
|
|
allowZip := true
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(ctx.PostForm("allow_zip")), "false") {
|
|
|
|
|
allowZip = false
|
|
|
|
|
}
|
|
|
|
|
request := models.CreateBoxRequest{
|
|
|
|
|
RetentionKey: retentionKey,
|
|
|
|
|
Password: ctx.PostForm("password"),
|
|
|
|
|
AllowZip: &allowZip,
|
|
|
|
|
Files: make([]models.CreateBoxFileRequest, 0, len(files)),
|
|
|
|
|
}
|
2026-04-27 17:49:19 +03:00
|
|
|
for _, file := range files {
|
2026-04-29 01:16:17 +03:00
|
|
|
request.Files = append(request.Files, models.CreateBoxFileRequest{Name: file.Filename, Size: file.Size})
|
|
|
|
|
}
|
|
|
|
|
if err := app.validateCreateBoxRequest(&request); err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifestFiles, err := boxstore.CreateManifest(boxID, request)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
savedFiles := make([]models.BoxFile, 0, len(files))
|
|
|
|
|
for index, file := range files {
|
|
|
|
|
savedFile, err := boxstore.SaveManifestUpload(boxID, manifestFiles[index].ID, file)
|
2026-04-27 17:49:19 +03:00
|
|
|
if err != nil {
|
2026-04-29 01:16:17 +03:00
|
|
|
_, _ = boxstore.MarkFileStatus(boxID, manifestFiles[index].ID, models.FileStatusFailed)
|
2026-04-27 17:49:19 +03:00
|
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
savedFiles = append(savedFiles, savedFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": savedFiles})
|
|
|
|
|
}
|
2026-04-27 18:18:53 +03:00
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) authorizeBoxRequest(ctx *gin.Context, boxID string, wantsHTML bool) (models.BoxManifest, bool, bool) {
|
2026-04-27 18:18:53 +03:00
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.BoxManifest{}, false, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if boxstore.IsExpired(manifest) {
|
|
|
|
|
boxstore.DeleteBox(boxID)
|
|
|
|
|
if wantsHTML {
|
|
|
|
|
ctx.String(http.StatusGone, "Box expired")
|
|
|
|
|
} else {
|
|
|
|
|
ctx.JSON(http.StatusGone, gin.H{"error": "Box expired"})
|
|
|
|
|
}
|
|
|
|
|
return manifest, true, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 04:24:49 +03:00
|
|
|
if manifest.OneTimeDownload && manifest.Consumed {
|
|
|
|
|
if wantsHTML {
|
|
|
|
|
ctx.String(http.StatusGone, "Box already consumed")
|
|
|
|
|
} else {
|
|
|
|
|
ctx.JSON(http.StatusGone, gin.H{"error": "Box already consumed"})
|
|
|
|
|
}
|
|
|
|
|
return manifest, true, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
if boxstore.IsPasswordProtected(manifest) && !isBoxAuthorized(ctx, boxID, manifest) {
|
|
|
|
|
if wantsHTML {
|
|
|
|
|
ctx.Redirect(http.StatusSeeOther, "/box/"+boxID+"/login")
|
|
|
|
|
} else {
|
|
|
|
|
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Password required"})
|
|
|
|
|
}
|
|
|
|
|
return manifest, true, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
if app.config.RenewOnAccessEnabled {
|
|
|
|
|
if renewed, err := boxstore.RenewManifest(boxID, manifest.RetentionSecs); err == nil {
|
|
|
|
|
manifest = renewed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
return manifest, true, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isBoxAuthorized(ctx *gin.Context, boxID string, manifest models.BoxManifest) bool {
|
|
|
|
|
token, err := ctx.Cookie(boxAuthCookieName(boxID))
|
|
|
|
|
return err == nil && boxstore.VerifyAuthToken(manifest, token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func boxAuthCookieName(boxID string) string {
|
|
|
|
|
return boxAuthCookiePrefix + boxID
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) requireAPI(ctx *gin.Context) bool {
|
|
|
|
|
if app.config.APIEnabled {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "API access is disabled"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) requireGuestUploads(ctx *gin.Context) bool {
|
|
|
|
|
if app.config.GuestUploadsEnabled {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "Guest uploads are disabled"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateCreateBoxRequest(request *models.CreateBoxRequest) error {
|
|
|
|
|
if request == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !app.retentionAllowed(request.RetentionKey) {
|
|
|
|
|
return fmt.Errorf("Retention option is not allowed")
|
|
|
|
|
}
|
|
|
|
|
if !app.config.ZipDownloadsEnabled {
|
|
|
|
|
allowZip := false
|
|
|
|
|
request.AllowZip = &allowZip
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(request.RetentionKey) == boxstore.OneTimeDownloadRetentionKey && !app.config.OneTimeDownloadsEnabled {
|
|
|
|
|
return fmt.Errorf("One-time downloads are disabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalSize := int64(0)
|
|
|
|
|
for _, file := range request.Files {
|
|
|
|
|
if err := app.validateFileSize(file.Size); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
|
|
|
|
return app.validateBoxSize(totalSize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateIncomingFile(boxID string, size int64) error {
|
|
|
|
|
if err := app.validateFileSize(size); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if app.config.GlobalMaxBoxSizeBytes <= 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files, err := boxstore.ListFiles(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
totalSize := size
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
|
|
|
|
return app.validateBoxSize(totalSize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateManifestFileUpload(boxID string, fileID string, size int64) error {
|
|
|
|
|
if err := app.validateFileSize(size); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return app.validateIncomingFile(boxID, size)
|
|
|
|
|
}
|
2026-04-28 21:42:36 +03:00
|
|
|
if boxstore.IsExpired(manifest) {
|
|
|
|
|
_ = boxstore.DeleteBox(boxID)
|
|
|
|
|
return fmt.Errorf("Box expired")
|
|
|
|
|
}
|
|
|
|
|
if app.config.GlobalMaxBoxSizeBytes <= 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-28 21:11:37 +03:00
|
|
|
totalSize := int64(0)
|
|
|
|
|
found := false
|
|
|
|
|
for _, file := range manifest.Files {
|
|
|
|
|
if file.ID == fileID {
|
|
|
|
|
totalSize += size
|
|
|
|
|
found = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
totalSize += size
|
|
|
|
|
}
|
|
|
|
|
return app.validateBoxSize(totalSize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateFileSize(size int64) error {
|
|
|
|
|
if size < 0 {
|
|
|
|
|
return fmt.Errorf("File size cannot be negative")
|
|
|
|
|
}
|
|
|
|
|
if app.config.GlobalMaxFileSizeBytes > 0 && size > app.config.GlobalMaxFileSizeBytes {
|
|
|
|
|
return fmt.Errorf("File exceeds the global max file size")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateBoxSize(size int64) error {
|
|
|
|
|
if size < 0 {
|
|
|
|
|
return fmt.Errorf("Box size cannot be negative")
|
|
|
|
|
}
|
|
|
|
|
if app.config.GlobalMaxBoxSizeBytes > 0 && size > app.config.GlobalMaxBoxSizeBytes {
|
|
|
|
|
return fmt.Errorf("Box exceeds the global max box size")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:42:36 +03:00
|
|
|
func (app *App) rejectExpiredManifestBox(boxID string) error {
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !boxstore.IsExpired(manifest) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
_ = boxstore.DeleteBox(boxID)
|
|
|
|
|
return fmt.Errorf("Box expired")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) limitRequestBody(ctx *gin.Context) {
|
|
|
|
|
limit := app.maxRequestBodyBytes()
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, limit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) maxRequestBodyBytes() int64 {
|
|
|
|
|
limit := app.config.GlobalMaxBoxSizeBytes
|
|
|
|
|
if limit <= 0 || app.config.GlobalMaxFileSizeBytes > limit {
|
|
|
|
|
limit = app.config.GlobalMaxFileSizeBytes
|
|
|
|
|
}
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return limit + 10*1024*1024
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:11:37 +03:00
|
|
|
func (app *App) retentionAllowed(key string) bool {
|
|
|
|
|
key = strings.TrimSpace(key)
|
|
|
|
|
if key == "" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
for _, option := range app.retentionOptions() {
|
|
|
|
|
if option.Key == key {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) retentionOptions() []models.RetentionOption {
|
|
|
|
|
allOptions := boxstore.RetentionOptions()
|
|
|
|
|
options := make([]models.RetentionOption, 0, len(allOptions))
|
|
|
|
|
for _, option := range allOptions {
|
|
|
|
|
if option.Key == boxstore.OneTimeDownloadRetentionKey && !app.config.OneTimeDownloadsEnabled {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if option.Seconds > 0 && app.config.MaxGuestExpirySeconds > 0 && option.Seconds > app.config.MaxGuestExpirySeconds {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
options = append(options, option)
|
|
|
|
|
}
|
|
|
|
|
if len(options) == 0 {
|
|
|
|
|
return allOptions[:1]
|
|
|
|
|
}
|
|
|
|
|
return options
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) defaultRetentionOption() models.RetentionOption {
|
|
|
|
|
options := app.retentionOptions()
|
|
|
|
|
for _, option := range options {
|
|
|
|
|
if option.Seconds == app.config.DefaultGuestExpirySeconds {
|
|
|
|
|
return option
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return options[0]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:18:53 +03:00
|
|
|
func renderBoxLogin(ctx *gin.Context, boxID string, errorMessage string) {
|
|
|
|
|
ctx.HTML(http.StatusOK, "box_login.html", gin.H{
|
|
|
|
|
"BoxID": boxID,
|
|
|
|
|
"BoxUser": "WarpBox\\" + boxID,
|
|
|
|
|
"ErrorMessage": errorMessage,
|
|
|
|
|
})
|
|
|
|
|
}
|