feat(config): add expired box cleanup functionality

Adds dedicated config setting and cleanup logic for
managing expired content boxes via admin tools and CLI.
This commit is contained in:
2026-05-03 23:51:13 +03:00
parent 43baade930
commit 8ba4eb90b4
15 changed files with 260 additions and 6 deletions

View File

@@ -84,22 +84,41 @@ func (app *App) handleAdminBoxesAction(ctx *gin.Context) {
return
}
if len(request.BoxIDs) == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Select one or more boxes first"})
return
}
switch request.Action {
case "delete", "expire", "bump":
case "delete", "expire", "bump", "cleanup_expired":
default:
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Unknown action"})
return
}
if request.Action != "cleanup_expired" && len(request.BoxIDs) == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Select one or more boxes first"})
return
}
if request.Action == "bump" && request.DeltaSeconds <= 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing bump duration"})
return
}
if request.Action == "cleanup_expired" {
result, err := app.runExpiredCleanup("admin")
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Expired cleanup job failed"})
return
}
boxes, listErr := app.listAdminBoxes()
if listErr != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Cleanup finished, but boxes could not be reloaded"})
return
}
ctx.JSON(http.StatusOK, gin.H{
"ok": len(result.Warnings) == 0,
"message": fmt.Sprintf("Expired cleanup done: deleted %d box(es), skipped %d", result.Deleted, result.Skipped),
"warnings": result.Warnings,
"boxes": boxes,
})
return
}
processed := 0
warnings := make([]string, 0)
@@ -299,6 +318,8 @@ func adminBoxesActionMessage(action string, processed int, deltaSeconds int64) s
return fmt.Sprintf("Expired %d box(es)", processed)
case "bump":
return fmt.Sprintf("Extended %d box(es) by %s", processed, adminBoxesDeltaLabel(deltaSeconds))
case "cleanup_expired":
return fmt.Sprintf("Expired cleanup processed %d box(es)", processed)
default:
return "Action complete"
}

View File

@@ -451,6 +451,8 @@ func settingsCategoryForKey(key string) string {
return "storage"
case config.SettingBoxPollIntervalMS, config.SettingThumbnailBatchSize, config.SettingThumbnailIntervalSeconds:
return "workers"
case config.SettingExpiredCleanupIntervalSecs:
return "workers"
default:
return "accounts"
}
@@ -489,6 +491,7 @@ func settingsDescription(key string) string {
config.SettingSecurityUploadWindowSecs: "Window used for per-IP upload throttling.",
config.SettingSecurityUploadMaxRequests: "Max upload requests per IP per upload window.",
config.SettingSecurityUploadMaxGB: "Max upload volume in GB per IP per upload window.",
config.SettingExpiredCleanupIntervalSecs: "Background interval for deleting expired boxes. Set 0 to disable periodic cleanup.",
}
return descriptions[key]
}

View File

@@ -279,6 +279,7 @@ func clearAdminSettingsEnv(t *testing.T) {
"WARPBOX_SECURITY_UPLOAD_MAX_GB",
"WARPBOX_SECURITY_UPLOAD_MAX_MB",
"WARPBOX_SECURITY_UPLOAD_MAX_BYTES",
"WARPBOX_EXPIRED_CLEANUP_INTERVAL_SECONDS",
} {
t.Setenv(name, "")
}

62
lib/server/cleanup.go Normal file
View File

@@ -0,0 +1,62 @@
package server
import (
"log"
"strings"
"time"
"warpbox/lib/boxstore"
)
func (app *App) runExpiredCleanup(trigger string) (boxstore.CleanupExpiredResult, error) {
result, err := boxstore.CleanupExpiredBoxes()
if err != nil {
log.Printf("warpbox cleanup[%s] failed: %v", trigger, err)
app.logActivity("boxes.cleanup.failed", "high", "Expired boxes cleanup failed", nil, map[string]string{
"trigger": trigger,
"error": err.Error(),
})
return result, err
}
meta := map[string]string{
"trigger": trigger,
"scanned": intToString(result.Scanned),
"deleted": intToString(result.Deleted),
"skipped": intToString(result.Skipped),
}
if len(result.DeletedIDs) > 0 {
limit := len(result.DeletedIDs)
if limit > 20 {
limit = 20
}
meta["deleted_ids"] = strings.Join(result.DeletedIDs[:limit], ",")
}
if len(result.Warnings) > 0 {
limit := len(result.Warnings)
if limit > 3 {
limit = 3
}
meta["warnings"] = strings.Join(result.Warnings[:limit], " | ")
}
app.logActivity("boxes.cleanup", "medium", "Expired boxes cleanup run completed", nil, meta)
log.Printf("warpbox cleanup[%s] scanned=%d deleted=%d skipped=%d", trigger, result.Scanned, result.Deleted, result.Skipped)
return result, nil
}
func (app *App) startExpiredCleanupWorker() {
if app == nil || app.config == nil {
return
}
go func() {
for {
interval := app.config.ExpiredCleanupIntervalSeconds
if interval <= 0 {
time.Sleep(30 * time.Second)
continue
}
time.Sleep(time.Duration(interval) * time.Second)
_, _ = app.runExpiredCleanup("worker")
}
}()
}

View File

@@ -104,6 +104,7 @@ func Run(addr string) error {
compressed.Static("/static", "./static")
boxstore.StartThumbnailWorker(cfg.ThumbnailBatchSize, time.Duration(cfg.ThumbnailIntervalSeconds)*time.Second)
app.startExpiredCleanupWorker()
return router.Run(addr)
}