feat(setting): Implemented the settings administrative menu
This commit is contained in:
316
lib/server/admin_boxes.go
Normal file
316
lib/server/admin_boxes.go
Normal file
@@ -0,0 +1,316 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user