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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user