feat(boxstore): add retention options and box deletion support

Introduce configurable retention options and default selection, store
retention when creating manifests, and add a helper to delete box
directories to enable expiring/cleanup workflows. Update login and upload
styles (new login layout, taller upload window) to support the new UI.feat(boxstore): add retention options and box deletion support

Introduce configurable retention options and default selection, store
retention when creating manifests, and add a helper to delete box
directories to enable expiring/cleanup workflows. Update login and upload
styles (new login layout, taller upload window) to support the new UI.
This commit is contained in:
2026-04-27 18:18:53 +03:00
parent 2f37958c31
commit 041a9798a7
13 changed files with 654 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
@@ -14,8 +15,13 @@ import (
"warpbox/lib/models"
)
const boxAuthCookiePrefix = "warpbox_box_"
func handleIndex(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H{})
ctx.HTML(http.StatusOK, "index.html", gin.H{
"RetentionOptions": boxstore.RetentionOptions(),
"DefaultRetention": boxstore.DefaultRetentionOption().Key,
})
}
func handleShowBox(ctx *gin.Context) {
@@ -25,21 +31,96 @@ func handleShowBox(ctx *gin.Context) {
return
}
manifest, hasManifest, ok := authorizeBoxRequest(ctx, boxID, true)
if !ok {
return
}
files, err := boxstore.ListFiles(boxID)
if err != nil {
ctx.String(http.StatusNotFound, "Box not found")
return
}
downloadAll := "/box/" + boxID + "/download"
if hasManifest && manifest.DisableZip {
downloadAll = ""
}
ctx.HTML(http.StatusOK, "box.html", gin.H{
"BoxID": boxID,
"Files": files,
"FileCount": len(files),
"DownloadAll": "/box/" + boxID + "/download",
"PollMS": helpers.EnvInt("WARPBOX_BOX_POLL_INTERVAL_MS", 5000, 1000),
"BoxID": boxID,
"Files": files,
"FileCount": len(files),
"DownloadAll": downloadAll,
"PollMS": helpers.EnvInt("WARPBOX_BOX_POLL_INTERVAL_MS", 5000, 1000),
"RetentionLabel": manifest.RetentionLabel,
"ExpiresAt": manifest.ExpiresAt,
})
}
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)
}
func handleBoxStatus(ctx *gin.Context) {
boxID := ctx.Param("id")
if !boxstore.ValidBoxID(boxID) {
@@ -47,6 +128,10 @@ func handleBoxStatus(ctx *gin.Context) {
return
}
if _, _, ok := authorizeBoxRequest(ctx, boxID, false); !ok {
return
}
files, err := boxstore.ListFiles(boxID)
if err != nil {
ctx.JSON(http.StatusNotFound, gin.H{"error": "Box not found"})
@@ -63,6 +148,16 @@ func handleDownloadBox(ctx *gin.Context) {
return
}
manifest, hasManifest, ok := authorizeBoxRequest(ctx, boxID, true)
if !ok {
return
}
if hasManifest && manifest.DisableZip {
ctx.String(http.StatusForbidden, "Zip download disabled for this box")
return
}
files, err := boxstore.ListFiles(boxID)
if err != nil {
ctx.String(http.StatusNotFound, "Box not found")
@@ -95,6 +190,10 @@ func handleDownloadFile(ctx *gin.Context) {
return
}
if _, _, authorized := authorizeBoxRequest(ctx, boxID, true); !authorized {
return
}
path, ok := boxstore.SafeBoxFilePath(boxID, filename)
if !ok {
ctx.String(http.StatusBadRequest, "Invalid file")
@@ -127,7 +226,7 @@ func handleCreateBox(ctx *gin.Context) {
return
}
files, err := boxstore.CreateManifest(boxID, request.Files)
files, err := boxstore.CreateManifest(boxID, request)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -243,3 +342,48 @@ func handleLegacyUpload(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": savedFiles})
}
func authorizeBoxRequest(ctx *gin.Context, boxID string, wantsHTML bool) (models.BoxManifest, bool, bool) {
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
}
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
}
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
}
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,
})
}

View File

@@ -14,6 +14,8 @@ func Run(addr string) error {
routing.Register(router, routing.Handlers{
Index: handleIndex,
ShowBox: handleShowBox,
BoxLogin: handleBoxLogin,
BoxLoginPost: handleBoxLoginPost,
BoxStatus: handleBoxStatus,
DownloadBox: handleDownloadBox,
DownloadFile: handleDownloadFile,