feat(boxstore): add one-time download retention mode

Introduce a `one-time` retention option and persist it on the manifest as `one_time_download`. One-time download boxes bypass retention expiry scheduling, force zip downloads, and reject download attempts until all files are complete to prevent partial retrievals.feat(boxstore): add one-time download retention mode

Introduce a `one-time` retention option and persist it on the manifest as `one_time_download`. One-time download boxes bypass retention expiry scheduling, force zip downloads, and reject download attempts until all files are complete to prevent partial retrievals.
This commit is contained in:
2026-04-28 19:41:23 +03:00
parent f1600faa8d
commit 9dececcc7d
7 changed files with 116 additions and 22 deletions

View File

@@ -52,6 +52,7 @@ func handleShowBox(ctx *gin.Context) {
"Files": files,
"FileCount": len(files),
"DownloadAll": downloadAll,
"ZipOnly": hasManifest && manifest.OneTimeDownload,
"PollMS": helpers.EnvInt("WARPBOX_BOX_POLL_INTERVAL_MS", 5000, 1000),
"RetentionLabel": manifest.RetentionLabel,
"ExpiresAt": manifest.ExpiresAt,
@@ -163,12 +164,21 @@ func handleDownloadBox(ctx *gin.Context) {
ctx.String(http.StatusNotFound, "Box not found")
return
}
if hasManifest && manifest.OneTimeDownload && !allFilesComplete(files) {
ctx.String(http.StatusConflict, "Box is not ready yet")
return
}
ctx.Header("Content-Type", "application/zip")
ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID))
zipWriter := zip.NewWriter(ctx.Writer)
defer zipWriter.Close()
zipClosed := false
defer func() {
if !zipClosed {
zipWriter.Close()
}
}()
for _, file := range files {
if !file.IsComplete {
@@ -180,6 +190,31 @@ func handleDownloadBox(ctx *gin.Context) {
return
}
}
if err := zipWriter.Close(); err != nil {
zipClosed = true
ctx.Status(http.StatusInternalServerError)
return
}
zipClosed = true
if hasManifest && manifest.OneTimeDownload {
boxstore.DeleteBox(boxID)
}
}
func allFilesComplete(files []models.BoxFile) bool {
if len(files) == 0 {
return false
}
for _, file := range files {
if !file.IsComplete {
return false
}
}
return true
}
func handleDownloadFile(ctx *gin.Context) {
@@ -190,7 +225,12 @@ func handleDownloadFile(ctx *gin.Context) {
return
}
if _, _, authorized := authorizeBoxRequest(ctx, boxID, true); !authorized {
manifest, hasManifest, authorized := authorizeBoxRequest(ctx, boxID, true)
if !authorized {
return
}
if hasManifest && manifest.OneTimeDownload {
ctx.String(http.StatusForbidden, "Individual downloads disabled for this box")
return
}