feat: add admin console, cleanup, and thumbnail workers
- Implement a token-authenticated admin console at `/admin` with overview metrics and file management. - Add a background worker to periodically clean up expired boxes based on `WARPBOX_CLEANUP_EVERY`. - Add a background worker to generate image and video thumbnails based on `WARPBOX_THUMBNAIL_EVERY`. - Update file storage paths to use `@each@` and `@thumb@` prefixes to separate original files from thumbnails. - Add severity fields to startup logs and update configuration templates.
This commit is contained in:
198
backend/libs/handlers/admin.go
Normal file
198
backend/libs/handlers/admin.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/services"
|
||||
"warpbox.dev/backend/libs/web"
|
||||
)
|
||||
|
||||
const adminCookieName = "warpbox_admin"
|
||||
|
||||
type adminPageData struct {
|
||||
Stats services.AdminStats
|
||||
Boxes []adminBoxView
|
||||
Error string
|
||||
}
|
||||
|
||||
type adminBoxView struct {
|
||||
ID string
|
||||
CreatedAt string
|
||||
ExpiresAt string
|
||||
FileCount int
|
||||
TotalSizeLabel string
|
||||
DownloadCount int
|
||||
MaxDownloads int
|
||||
Protected bool
|
||||
Expired bool
|
||||
}
|
||||
|
||||
func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if a.isAdmin(r) {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
a.renderAdminLogin(w, http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
a.renderAdminLogin(w, http.StatusBadRequest, "Unable to read login form.")
|
||||
return
|
||||
}
|
||||
if a.cfg.AdminToken == "" || r.FormValue("token") != a.cfg.AdminToken {
|
||||
a.logger.Warn("admin login failed", "source", "admin", "severity", "warn", "code", 4301)
|
||||
a.renderAdminLogin(w, http.StatusUnauthorized, "Invalid admin token.")
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: adminCookieName,
|
||||
Value: adminCookieValue(a.cfg.AdminToken),
|
||||
Path: "/admin",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: r.TLS != nil,
|
||||
Expires: time.Now().Add(12 * time.Hour),
|
||||
})
|
||||
a.logger.Info("admin login", "source", "admin", "severity", "user_activity", "code", 2301)
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) AdminLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: adminCookieName,
|
||||
Value: "",
|
||||
Path: "/admin",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) AdminDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := a.uploadService.AdminStats()
|
||||
if err != nil {
|
||||
http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
boxes, err := a.adminBoxes(8)
|
||||
if err != nil {
|
||||
http.Error(w, "unable to load recent boxes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
||||
Title: "Admin overview",
|
||||
Description: "Warpbox admin overview.",
|
||||
Data: adminPageData{
|
||||
Stats: stats,
|
||||
Boxes: boxes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := a.uploadService.AdminStats()
|
||||
if err != nil {
|
||||
http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
boxes, err := a.adminBoxes(100)
|
||||
if err != nil {
|
||||
http.Error(w, "unable to load boxes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
||||
Title: "Admin files",
|
||||
Description: "Manage Warpbox uploads.",
|
||||
Data: adminPageData{
|
||||
Stats: stats,
|
||||
Boxes: boxes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) AdminDeleteBox(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
boxID := r.PathValue("boxID")
|
||||
if err := a.uploadService.DeleteBox(boxID); err != nil {
|
||||
a.logger.Warn("admin delete failed", "source", "admin", "severity", "warn", "code", 4302, "box_id", boxID, "error", err.Error())
|
||||
http.Error(w, "unable to delete box", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/files", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) renderAdminLogin(w http.ResponseWriter, status int, message string) {
|
||||
a.renderer.Render(w, status, "admin_login.html", web.PageData{
|
||||
Title: "Admin login",
|
||||
Description: "Sign in to the Warpbox admin console.",
|
||||
Data: adminPageData{
|
||||
Error: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) adminBoxes(limit int) ([]adminBoxView, error) {
|
||||
boxes, err := a.uploadService.AdminBoxes(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows := make([]adminBoxView, 0, len(boxes))
|
||||
for _, box := range boxes {
|
||||
rows = append(rows, adminBoxView{
|
||||
ID: box.ID,
|
||||
CreatedAt: box.CreatedAt.Format("Jan 2 15:04"),
|
||||
ExpiresAt: box.ExpiresAt.Format("Jan 2 15:04"),
|
||||
FileCount: box.FileCount,
|
||||
TotalSizeLabel: box.TotalSizeLabel,
|
||||
DownloadCount: box.DownloadCount,
|
||||
MaxDownloads: box.MaxDownloads,
|
||||
Protected: box.Protected,
|
||||
Expired: box.Expired,
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
if a.isAdmin(r) {
|
||||
return true
|
||||
}
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) isAdmin(r *http.Request) bool {
|
||||
if a.cfg.AdminToken == "" {
|
||||
return false
|
||||
}
|
||||
cookie, err := r.Cookie(adminCookieName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cookie.Value == adminCookieValue(a.cfg.AdminToken)
|
||||
}
|
||||
|
||||
func adminCookieValue(token string) string {
|
||||
sum := sha256.Sum256([]byte("warpbox-admin:" + token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -27,9 +27,18 @@ func NewApp(cfg config.Config, logger *slog.Logger, renderer *web.Renderer, uplo
|
||||
|
||||
func (a *App) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /", a.Home)
|
||||
mux.HandleFunc("GET /admin/login", a.AdminLogin)
|
||||
mux.HandleFunc("POST /admin/login", a.AdminLoginPost)
|
||||
mux.HandleFunc("POST /admin/logout", a.AdminLogout)
|
||||
mux.HandleFunc("GET /admin", a.AdminDashboard)
|
||||
mux.HandleFunc("GET /admin/files", a.AdminFiles)
|
||||
mux.HandleFunc("POST /admin/boxes/{boxID}/delete", a.AdminDeleteBox)
|
||||
mux.HandleFunc("GET /d/{boxID}", a.DownloadPage)
|
||||
mux.HandleFunc("POST /d/{boxID}/unlock", a.UnlockBox)
|
||||
mux.HandleFunc("GET /d/{boxID}/zip", a.DownloadZip)
|
||||
mux.HandleFunc("GET /d/{boxID}/f/{fileID}", a.DownloadFile)
|
||||
mux.HandleFunc("GET /d/{boxID}/f/{fileID}/download", a.DownloadFileContent)
|
||||
mux.HandleFunc("GET /d/{boxID}/thumb/{fileID}", a.Thumbnail)
|
||||
mux.HandleFunc("GET /healthz", a.Health)
|
||||
mux.HandleFunc("GET /api/v1/health", a.Health)
|
||||
mux.HandleFunc("POST /api/v1/upload", a.Upload)
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/helpers"
|
||||
"warpbox.dev/backend/libs/services"
|
||||
"warpbox.dev/backend/libs/web"
|
||||
)
|
||||
|
||||
@@ -15,6 +18,9 @@ type downloadPageData struct {
|
||||
Box boxView
|
||||
Files []fileView
|
||||
ZipURL string
|
||||
Locked bool
|
||||
Obfuscated bool
|
||||
CanPreview bool
|
||||
DownloadCount int
|
||||
MaxDownloads int
|
||||
ExpiresLabel string
|
||||
@@ -25,11 +31,21 @@ type boxView struct {
|
||||
}
|
||||
|
||||
type fileView struct {
|
||||
ID string
|
||||
Name string
|
||||
Size string
|
||||
ContentType string
|
||||
URL string
|
||||
ID string
|
||||
Name string
|
||||
Size string
|
||||
ContentType string
|
||||
PreviewKind string
|
||||
URL string
|
||||
DownloadURL string
|
||||
ThumbnailURL string
|
||||
}
|
||||
|
||||
type previewPageData struct {
|
||||
Box boxView
|
||||
File fileView
|
||||
Locked bool
|
||||
DownloadURL string
|
||||
}
|
||||
|
||||
func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -39,7 +55,7 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := a.uploadService.CanDownload(box); err != nil {
|
||||
a.renderer.Render(w, http.StatusForbidden, "download.gohtml", web.PageData{
|
||||
a.renderer.Render(w, http.StatusForbidden, "download.html", web.PageData{
|
||||
Title: "Download unavailable",
|
||||
Description: "This Warpbox link is no longer available.",
|
||||
Data: downloadPageData{
|
||||
@@ -49,25 +65,24 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box)
|
||||
|
||||
files := make([]fileView, 0, len(box.Files))
|
||||
for _, file := range box.Files {
|
||||
files = append(files, fileView{
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: helpers.FormatBytes(file.Size),
|
||||
ContentType: file.ContentType,
|
||||
URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
|
||||
})
|
||||
if !(locked && box.Obfuscate) {
|
||||
for _, file := range box.Files {
|
||||
files = append(files, a.fileView(box, file))
|
||||
}
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "download.gohtml", web.PageData{
|
||||
a.renderer.Render(w, http.StatusOK, "download.html", web.PageData{
|
||||
Title: "Download files",
|
||||
Description: "Download files shared through Warpbox.",
|
||||
Data: downloadPageData{
|
||||
Box: boxView{ID: box.ID},
|
||||
Files: files,
|
||||
ZipURL: fmt.Sprintf("/d/%s/zip", box.ID),
|
||||
Locked: locked,
|
||||
Obfuscated: box.Obfuscate,
|
||||
DownloadCount: box.DownloadCount,
|
||||
MaxDownloads: box.MaxDownloads,
|
||||
ExpiresLabel: box.ExpiresAt.Format("Jan 2, 2006 15:04 MST"),
|
||||
@@ -76,22 +91,114 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
box, file, ok := a.loadFileForRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box)
|
||||
view := a.fileView(box, file)
|
||||
title := file.Name
|
||||
description := fmt.Sprintf("%s shared via Warpbox", helpers.FormatBytes(file.Size))
|
||||
imageURL := absoluteURL(r, view.ThumbnailURL)
|
||||
if locked && box.Obfuscate {
|
||||
title = "Protected Warpbox file"
|
||||
description = "This shared file is password protected."
|
||||
imageURL = absoluteURL(r, "/static/img/file-placeholder.webp")
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "preview.html", web.PageData{
|
||||
Title: title,
|
||||
Description: description,
|
||||
ImageURL: imageURL,
|
||||
Data: previewPageData{
|
||||
Box: boxView{ID: box.ID},
|
||||
File: view,
|
||||
Locked: locked,
|
||||
DownloadURL: view.DownloadURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
box, file, ok := a.loadFileForRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) {
|
||||
http.Error(w, "password required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1")
|
||||
}
|
||||
|
||||
func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
box, file, ok := a.loadFileForRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) {
|
||||
http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp"))
|
||||
return
|
||||
}
|
||||
|
||||
path := a.uploadService.ThumbnailPath(box, file)
|
||||
if path == "" {
|
||||
http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp"))
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) {
|
||||
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !a.uploadService.VerifyPassword(box, r.FormValue("password")) {
|
||||
a.logger.Warn("box unlock failed", "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID)
|
||||
http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: unlockCookieName(box.ID),
|
||||
Value: a.uploadService.UnlockToken(box),
|
||||
Path: "/d/" + box.ID,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: r.TLS != nil,
|
||||
Expires: box.ExpiresAt,
|
||||
})
|
||||
a.logger.Info("box unlocked", "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID)
|
||||
http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (services.Box, services.File, bool) {
|
||||
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return services.Box{}, services.File{}, false
|
||||
}
|
||||
if err := a.uploadService.CanDownload(box); err != nil {
|
||||
http.Error(w, err.Error(), statusForDownloadError(err))
|
||||
return
|
||||
return services.Box{}, services.File{}, false
|
||||
}
|
||||
|
||||
file, err := a.uploadService.FindFile(box, r.PathValue("fileID"))
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
return services.Box{}, services.File{}, false
|
||||
}
|
||||
return box, file, true
|
||||
}
|
||||
|
||||
func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) {
|
||||
path := a.uploadService.FilePath(box, file)
|
||||
source, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -107,11 +214,13 @@ func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", file.ContentType)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name))
|
||||
if attachment {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name))
|
||||
}
|
||||
http.ServeContent(w, r, file.Name, stat.ModTime(), source)
|
||||
|
||||
if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
a.logger.Warn("failed to record file download", "source", "download", "code", 4002, "box_id", box.ID, "error", err.Error())
|
||||
a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +234,59 @@ func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), statusForDownloadError(err))
|
||||
return
|
||||
}
|
||||
if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) {
|
||||
http.Error(w, "password required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", "warpbox-"+box.ID+".zip"))
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
|
||||
if err := a.uploadService.WriteZip(w, box); err != nil {
|
||||
a.logger.Error("zip download failed", "source", "download", "code", 5002, "box_id", box.ID, "error", err.Error())
|
||||
a.logger.Error("zip download failed", "source", "download", "severity", "error", "code", 5002, "box_id", box.ID, "error", err.Error())
|
||||
return
|
||||
}
|
||||
if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
a.logger.Warn("failed to record zip download", "source", "download", "code", 4003, "box_id", box.ID, "error", err.Error())
|
||||
a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) fileView(box services.Box, file services.File) fileView {
|
||||
return fileView{
|
||||
ID: file.ID,
|
||||
Name: file.Name,
|
||||
Size: helpers.FormatBytes(file.Size),
|
||||
ContentType: file.ContentType,
|
||||
PreviewKind: file.PreviewKind,
|
||||
URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
|
||||
DownloadURL: fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID),
|
||||
ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) isBoxUnlocked(r *http.Request, box services.Box) bool {
|
||||
if !a.uploadService.IsProtected(box) {
|
||||
return true
|
||||
}
|
||||
cookie, err := r.Cookie(unlockCookieName(box.ID))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cookie.Value == a.uploadService.UnlockToken(box)
|
||||
}
|
||||
|
||||
func unlockCookieName(boxID string) string {
|
||||
return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID)
|
||||
}
|
||||
|
||||
func absoluteURL(r *http.Request, path string) string {
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
return path
|
||||
}
|
||||
scheme := "http"
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s", scheme, r.Host, path)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type homeData struct {
|
||||
}
|
||||
|
||||
func (a *App) Home(w http.ResponseWriter, r *http.Request) {
|
||||
a.renderer.Render(w, http.StatusOK, "home.gohtml", web.PageData{
|
||||
a.renderer.Render(w, http.StatusOK, "home.html", web.PageData{
|
||||
Title: "Upload your files",
|
||||
Description: "Upload and share files through a self-hosted Warpbox instance.",
|
||||
Data: homeData{
|
||||
|
||||
@@ -18,11 +18,13 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
files := r.MultipartForm.File["file"]
|
||||
result, err := a.uploadService.CreateBox(files, services.UploadOptions{
|
||||
MaxDays: parseInt(r.FormValue("max_days")),
|
||||
MaxDownloads: parseInt(r.FormValue("max_downloads")),
|
||||
MaxDays: parseInt(r.FormValue("max_days")),
|
||||
MaxDownloads: parseInt(r.FormValue("max_downloads")),
|
||||
Password: r.FormValue("password"),
|
||||
ObfuscateMetadata: r.FormValue("obfuscate_metadata") == "on",
|
||||
})
|
||||
if err != nil {
|
||||
a.logger.Warn("upload failed", "source", "user-upload", "code", 4001, "error", err.Error())
|
||||
a.logger.Warn("upload failed", "source", "user-upload", "severity", "warn", "code", 4001, "error", err.Error())
|
||||
helpers.WriteJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user