317 lines
8.9 KiB
Go
317 lines
8.9 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
|
||
|
|
"warpbox/lib/boxstore"
|
||
|
|
)
|
||
|
|
|
||
|
|
type adminBoxesActionRequest struct {
|
||
|
|
Action string `json:"action"`
|
||
|
|
BoxIDs []string `json:"box_ids"`
|
||
|
|
DeltaSeconds int64 `json:"delta_seconds,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminBoxFileView struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
SizeLabel string `json:"size_label"`
|
||
|
|
MimeType string `json:"mime_type"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
StatusLabel string `json:"status_label"`
|
||
|
|
DownloadPath string `json:"download_path"`
|
||
|
|
ThumbnailURL string `json:"thumbnail_url"`
|
||
|
|
IsComplete bool `json:"is_complete"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminBoxView struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
StatusLabel string `json:"status_label"`
|
||
|
|
FileCount int `json:"file_count"`
|
||
|
|
CompleteFiles int `json:"complete_files"`
|
||
|
|
PendingFiles int `json:"pending_files"`
|
||
|
|
FailedFiles int `json:"failed_files"`
|
||
|
|
TotalSizeLabel string `json:"total_size_label"`
|
||
|
|
CreatedAtLabel string `json:"created_at_label"`
|
||
|
|
CreatedAtISO string `json:"created_at_iso"`
|
||
|
|
ExpiresAtLabel string `json:"expires_at_label"`
|
||
|
|
ExpiresAtISO string `json:"expires_at_iso"`
|
||
|
|
RetentionLabel string `json:"retention_label"`
|
||
|
|
PasswordProtected bool `json:"password_protected"`
|
||
|
|
OneTimeDownload bool `json:"one_time_download"`
|
||
|
|
ZipDisabled bool `json:"zip_disabled"`
|
||
|
|
ZipAvailable bool `json:"zip_available"`
|
||
|
|
Consumed bool `json:"consumed"`
|
||
|
|
HasManifest bool `json:"has_manifest"`
|
||
|
|
OpenURL string `json:"open_url"`
|
||
|
|
ZipURL string `json:"zip_url"`
|
||
|
|
Flags []string `json:"flags"`
|
||
|
|
Files []adminBoxFileView `json:"files"`
|
||
|
|
SearchText string `json:"search_text"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (app *App) handleAdminBoxes(ctx *gin.Context) {
|
||
|
|
if !app.adminLoginEnabled() {
|
||
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
boxes, err := app.listAdminBoxes()
|
||
|
|
if err != nil {
|
||
|
|
ctx.String(http.StatusInternalServerError, "Could not load boxes")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.HTML(http.StatusOK, "admin/boxes.html", gin.H{
|
||
|
|
"AdminUsername": app.config.AdminUsername,
|
||
|
|
"AdminEmail": app.config.AdminEmail,
|
||
|
|
"ActivePage": "boxes",
|
||
|
|
"Boxes": boxes,
|
||
|
|
"ZipDownloadsOn": app.config.ZipDownloadsEnabled,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (app *App) handleAdminBoxesAction(ctx *gin.Context) {
|
||
|
|
var request adminBoxesActionRequest
|
||
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action payload"})
|
||
|
|
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":
|
||
|
|
default:
|
||
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Unknown action"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if request.Action == "bump" && request.DeltaSeconds <= 0 {
|
||
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing bump duration"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
processed := 0
|
||
|
|
warnings := make([]string, 0)
|
||
|
|
|
||
|
|
for _, boxID := range request.BoxIDs {
|
||
|
|
if !boxstore.ValidBoxID(boxID) {
|
||
|
|
warnings = append(warnings, fmt.Sprintf("%s: invalid box id", boxID))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
var err error
|
||
|
|
switch request.Action {
|
||
|
|
case "delete":
|
||
|
|
err = boxstore.DeleteBox(boxID)
|
||
|
|
case "expire":
|
||
|
|
_, err = boxstore.ExpireBox(boxID)
|
||
|
|
case "bump":
|
||
|
|
_, err = boxstore.BumpBoxExpiry(boxID, time.Duration(request.DeltaSeconds)*time.Second)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
warnings = append(warnings, fmt.Sprintf("%s: %v", boxID, err))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
processed++
|
||
|
|
}
|
||
|
|
|
||
|
|
boxes, err := app.listAdminBoxes()
|
||
|
|
if err != nil {
|
||
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Action finished, but boxes could not be reloaded"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
status := http.StatusOK
|
||
|
|
if processed == 0 && len(warnings) > 0 {
|
||
|
|
status = http.StatusBadRequest
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.JSON(status, gin.H{
|
||
|
|
"ok": len(warnings) == 0,
|
||
|
|
"message": adminBoxesActionMessage(request.Action, processed, request.DeltaSeconds),
|
||
|
|
"warnings": warnings,
|
||
|
|
"boxes": boxes,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (app *App) listAdminBoxes() ([]adminBoxView, error) {
|
||
|
|
summaries, err := boxstore.ListBoxSummaries()
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
boxes := make([]adminBoxView, 0, len(summaries))
|
||
|
|
for _, summary := range summaries {
|
||
|
|
boxView, err := app.buildAdminBoxView(summary.ID)
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
boxes = append(boxes, boxView)
|
||
|
|
}
|
||
|
|
|
||
|
|
sort.Slice(boxes, func(i, j int) bool {
|
||
|
|
return boxes[i].CreatedAtISO > boxes[j].CreatedAtISO
|
||
|
|
})
|
||
|
|
return boxes, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (app *App) buildAdminBoxView(boxID string) (adminBoxView, error) {
|
||
|
|
summary, err := boxstore.BoxSummary(boxID)
|
||
|
|
if err != nil {
|
||
|
|
return adminBoxView{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
files, err := boxstore.ListFiles(boxID)
|
||
|
|
if err != nil {
|
||
|
|
return adminBoxView{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest, manifestErr := boxstore.ReadManifest(boxID)
|
||
|
|
hasManifest := manifestErr == nil
|
||
|
|
|
||
|
|
boxView := adminBoxView{
|
||
|
|
ID: summary.ID,
|
||
|
|
FileCount: summary.FileCount,
|
||
|
|
TotalSizeLabel: summary.TotalSizeLabel,
|
||
|
|
CreatedAtLabel: adminTimeLabel(summary.CreatedAt),
|
||
|
|
CreatedAtISO: formatBrowserTime(summary.CreatedAt),
|
||
|
|
ExpiresAtLabel: "Not set",
|
||
|
|
ExpiresAtISO: formatBrowserTime(summary.ExpiresAt),
|
||
|
|
RetentionLabel: "Legacy / unmanaged",
|
||
|
|
PasswordProtected: summary.PasswordProtected,
|
||
|
|
OneTimeDownload: summary.OneTimeDownload,
|
||
|
|
HasManifest: hasManifest,
|
||
|
|
OpenURL: "/box/" + summary.ID,
|
||
|
|
Files: make([]adminBoxFileView, 0, len(files)),
|
||
|
|
}
|
||
|
|
|
||
|
|
if !summary.ExpiresAt.IsZero() {
|
||
|
|
boxView.ExpiresAtLabel = adminTimeLabel(summary.ExpiresAt)
|
||
|
|
}
|
||
|
|
|
||
|
|
searchParts := []string{summary.ID, summary.TotalSizeLabel}
|
||
|
|
for _, file := range files {
|
||
|
|
if file.IsComplete {
|
||
|
|
boxView.CompleteFiles++
|
||
|
|
}
|
||
|
|
if file.Status == "failed" {
|
||
|
|
boxView.FailedFiles++
|
||
|
|
}
|
||
|
|
if !file.IsComplete && file.Status != "failed" {
|
||
|
|
boxView.PendingFiles++
|
||
|
|
}
|
||
|
|
|
||
|
|
boxView.Files = append(boxView.Files, adminBoxFileView{
|
||
|
|
Name: file.Name,
|
||
|
|
SizeLabel: file.SizeLabel,
|
||
|
|
MimeType: file.MimeType,
|
||
|
|
Status: file.Status,
|
||
|
|
StatusLabel: file.StatusLabel,
|
||
|
|
DownloadPath: file.DownloadPath,
|
||
|
|
ThumbnailURL: file.ThumbnailURL,
|
||
|
|
IsComplete: file.IsComplete,
|
||
|
|
})
|
||
|
|
searchParts = append(searchParts, file.Name, file.MimeType, file.StatusLabel)
|
||
|
|
}
|
||
|
|
|
||
|
|
if hasManifest {
|
||
|
|
boxView.RetentionLabel = manifest.RetentionLabel
|
||
|
|
boxView.ZipDisabled = manifest.DisableZip
|
||
|
|
boxView.Consumed = manifest.Consumed
|
||
|
|
} else {
|
||
|
|
boxView.ZipDisabled = false
|
||
|
|
}
|
||
|
|
|
||
|
|
boxView.ZipAvailable = app.config.ZipDownloadsEnabled && !boxView.ZipDisabled && !boxView.Consumed && boxView.FileCount > 0 && boxView.PendingFiles == 0
|
||
|
|
if boxView.ZipAvailable {
|
||
|
|
boxView.ZipURL = "/box/" + summary.ID + "/download"
|
||
|
|
}
|
||
|
|
|
||
|
|
boxView.Status, boxView.StatusLabel = deriveAdminBoxStatus(hasManifest, summary.Expired, boxView.PendingFiles, boxView.FailedFiles, boxView.Consumed)
|
||
|
|
boxView.Flags = deriveAdminBoxFlags(boxView)
|
||
|
|
searchParts = append(searchParts, boxView.StatusLabel, boxView.RetentionLabel)
|
||
|
|
boxView.SearchText = strings.ToLower(strings.Join(searchParts, " "))
|
||
|
|
|
||
|
|
return boxView, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func deriveAdminBoxStatus(hasManifest bool, expired bool, pendingFiles int, failedFiles int, consumed bool) (string, string) {
|
||
|
|
switch {
|
||
|
|
case !hasManifest:
|
||
|
|
return "legacy", "Legacy"
|
||
|
|
case consumed:
|
||
|
|
return "consumed", "Consumed"
|
||
|
|
case expired:
|
||
|
|
return "expired", "Expired"
|
||
|
|
case pendingFiles > 0:
|
||
|
|
return "uploading", "Uploading"
|
||
|
|
case failedFiles > 0:
|
||
|
|
return "attention", "Needs review"
|
||
|
|
default:
|
||
|
|
return "ready", "Ready"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func deriveAdminBoxFlags(box adminBoxView) []string {
|
||
|
|
flags := make([]string, 0, 5)
|
||
|
|
if box.PasswordProtected {
|
||
|
|
flags = append(flags, "protected")
|
||
|
|
}
|
||
|
|
if box.OneTimeDownload {
|
||
|
|
flags = append(flags, "one-time")
|
||
|
|
}
|
||
|
|
if box.ZipDisabled {
|
||
|
|
flags = append(flags, "zip off")
|
||
|
|
}
|
||
|
|
if !box.HasManifest {
|
||
|
|
flags = append(flags, "legacy")
|
||
|
|
}
|
||
|
|
if box.Consumed {
|
||
|
|
flags = append(flags, "consumed")
|
||
|
|
}
|
||
|
|
return flags
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminTimeLabel(value time.Time) string {
|
||
|
|
if value.IsZero() {
|
||
|
|
return "Not set"
|
||
|
|
}
|
||
|
|
return value.UTC().Format("2006-01-02 15:04 UTC")
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminBoxesActionMessage(action string, processed int, deltaSeconds int64) string {
|
||
|
|
switch action {
|
||
|
|
case "delete":
|
||
|
|
return fmt.Sprintf("Deleted %d box(es)", processed)
|
||
|
|
case "expire":
|
||
|
|
return fmt.Sprintf("Expired %d box(es)", processed)
|
||
|
|
case "bump":
|
||
|
|
return fmt.Sprintf("Extended %d box(es) by %s", processed, adminBoxesDeltaLabel(deltaSeconds))
|
||
|
|
default:
|
||
|
|
return "Action complete"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminBoxesDeltaLabel(deltaSeconds int64) string {
|
||
|
|
switch deltaSeconds {
|
||
|
|
case 24 * 60 * 60:
|
||
|
|
return "24h"
|
||
|
|
case 7 * 24 * 60 * 60:
|
||
|
|
return "7d"
|
||
|
|
default:
|
||
|
|
return (time.Duration(deltaSeconds) * time.Second).String()
|
||
|
|
}
|
||
|
|
}
|