454 lines
12 KiB
Go
454 lines
12 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"sort"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"warpbox.dev/backend/libs/helpers"
|
||
|
|
"warpbox.dev/backend/libs/services"
|
||
|
|
"warpbox.dev/backend/libs/web"
|
||
|
|
)
|
||
|
|
|
||
|
|
const adminFilesPageSize = 25
|
||
|
|
|
||
|
|
type adminFilesData struct {
|
||
|
|
Stats services.AdminStats
|
||
|
|
Section string
|
||
|
|
PageTitle string
|
||
|
|
Boxes []adminBoxView
|
||
|
|
Query string
|
||
|
|
Sort string
|
||
|
|
Dir string
|
||
|
|
Page int
|
||
|
|
TotalPages int
|
||
|
|
Total int
|
||
|
|
RangeFrom int
|
||
|
|
RangeTo int
|
||
|
|
Columns []adminFilesColumn
|
||
|
|
PageLinks []adminFilesPageLink
|
||
|
|
HasPrev bool
|
||
|
|
HasNext bool
|
||
|
|
PrevHref string
|
||
|
|
NextHref string
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminFilesColumn struct {
|
||
|
|
Label string
|
||
|
|
Href string
|
||
|
|
Sorted bool
|
||
|
|
Ascending bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminFilesPageLink struct {
|
||
|
|
Page int
|
||
|
|
Href string
|
||
|
|
Active bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminBoxEditData struct {
|
||
|
|
Section string
|
||
|
|
PageTitle string
|
||
|
|
Box adminBoxDetail
|
||
|
|
Files []adminBoxEditFile
|
||
|
|
Notice string
|
||
|
|
Error string
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminBoxDetail struct {
|
||
|
|
ID string
|
||
|
|
Owner string
|
||
|
|
CreatedAt string
|
||
|
|
ExpiresLabel string
|
||
|
|
ExpiresInput string
|
||
|
|
NeverExpires bool
|
||
|
|
MaxDownloads int
|
||
|
|
DownloadCount int
|
||
|
|
FileCount int
|
||
|
|
TotalSize string
|
||
|
|
BackendID string
|
||
|
|
Protected bool
|
||
|
|
Obfuscated bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type adminBoxEditFile struct {
|
||
|
|
ID string
|
||
|
|
Name string
|
||
|
|
Size string
|
||
|
|
ContentType string
|
||
|
|
ThumbnailURL string
|
||
|
|
DownloadURL string
|
||
|
|
HasPreview bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// adminFileRow is the sortable/filterable representation of a box.
|
||
|
|
type adminFileRow struct {
|
||
|
|
ID string
|
||
|
|
Owner string
|
||
|
|
CreatedAt time.Time
|
||
|
|
ExpiresAt time.Time
|
||
|
|
FileCount int
|
||
|
|
DownloadCount int
|
||
|
|
MaxDownloads int
|
||
|
|
TotalSize int64
|
||
|
|
TotalSizeLabel string
|
||
|
|
Protected bool
|
||
|
|
Expired bool
|
||
|
|
}
|
||
|
|
|
||
|
|
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.uploadService.AdminBoxes(0)
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "unable to load boxes", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
ownerCache := map[string]string{}
|
||
|
|
rows := make([]adminFileRow, 0, len(boxes))
|
||
|
|
for _, box := range boxes {
|
||
|
|
rows = append(rows, adminFileRow{
|
||
|
|
ID: box.ID,
|
||
|
|
Owner: a.boxOwnerLabel(box.OwnerID, ownerCache),
|
||
|
|
CreatedAt: box.CreatedAt,
|
||
|
|
ExpiresAt: box.ExpiresAt,
|
||
|
|
FileCount: box.FileCount,
|
||
|
|
DownloadCount: box.DownloadCount,
|
||
|
|
MaxDownloads: box.MaxDownloads,
|
||
|
|
TotalSize: box.TotalSize,
|
||
|
|
TotalSizeLabel: box.TotalSizeLabel,
|
||
|
|
Protected: box.Protected,
|
||
|
|
Expired: box.Expired,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
query := strings.TrimSpace(r.URL.Query().Get("q"))
|
||
|
|
if query != "" {
|
||
|
|
needle := strings.ToLower(query)
|
||
|
|
filtered := rows[:0:0]
|
||
|
|
for _, row := range rows {
|
||
|
|
if strings.Contains(strings.ToLower(row.ID), needle) || strings.Contains(strings.ToLower(row.Owner), needle) {
|
||
|
|
filtered = append(filtered, row)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rows = filtered
|
||
|
|
}
|
||
|
|
|
||
|
|
sortKey := adminFilesSortKey(r.URL.Query().Get("sort"))
|
||
|
|
dir := r.URL.Query().Get("dir")
|
||
|
|
if dir != "asc" {
|
||
|
|
dir = "desc"
|
||
|
|
}
|
||
|
|
sortAdminFileRows(rows, sortKey, dir)
|
||
|
|
|
||
|
|
total := len(rows)
|
||
|
|
totalPages := (total + adminFilesPageSize - 1) / adminFilesPageSize
|
||
|
|
if totalPages < 1 {
|
||
|
|
totalPages = 1
|
||
|
|
}
|
||
|
|
page := 1
|
||
|
|
if parsed, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && parsed > 1 {
|
||
|
|
page = parsed
|
||
|
|
}
|
||
|
|
if page > totalPages {
|
||
|
|
page = totalPages
|
||
|
|
}
|
||
|
|
start := (page - 1) * adminFilesPageSize
|
||
|
|
if start > total {
|
||
|
|
start = total
|
||
|
|
}
|
||
|
|
end := start + adminFilesPageSize
|
||
|
|
if end > total {
|
||
|
|
end = total
|
||
|
|
}
|
||
|
|
|
||
|
|
views := make([]adminBoxView, 0, end-start)
|
||
|
|
for _, row := range rows[start:end] {
|
||
|
|
views = append(views, adminBoxView{
|
||
|
|
ID: row.ID,
|
||
|
|
Owner: row.Owner,
|
||
|
|
CreatedAt: row.CreatedAt.Format("Jan 2, 2006 15:04"),
|
||
|
|
ExpiresAt: boxExpiryLabel(row.ExpiresAt, "Jan 2, 2006 15:04"),
|
||
|
|
FileCount: row.FileCount,
|
||
|
|
TotalSizeLabel: row.TotalSizeLabel,
|
||
|
|
DownloadCount: row.DownloadCount,
|
||
|
|
MaxDownloads: row.MaxDownloads,
|
||
|
|
Protected: row.Protected,
|
||
|
|
Expired: row.Expired,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
rangeFrom := 0
|
||
|
|
if total > 0 {
|
||
|
|
rangeFrom = start + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
a.renderPage(w, r, http.StatusOK, "admin_files.html", web.PageData{
|
||
|
|
Title: "Admin files",
|
||
|
|
Description: "Manage Warpbox uploads.",
|
||
|
|
CurrentUser: a.currentPublicUser(r),
|
||
|
|
Data: adminFilesData{
|
||
|
|
Stats: stats,
|
||
|
|
Section: "files",
|
||
|
|
PageTitle: "Files",
|
||
|
|
Boxes: views,
|
||
|
|
Query: query,
|
||
|
|
Sort: sortKey,
|
||
|
|
Dir: dir,
|
||
|
|
Page: page,
|
||
|
|
TotalPages: totalPages,
|
||
|
|
Total: total,
|
||
|
|
RangeFrom: rangeFrom,
|
||
|
|
RangeTo: end,
|
||
|
|
Columns: adminFilesColumns(query, sortKey, dir),
|
||
|
|
PageLinks: adminFilesPageLinks(query, sortKey, dir, page, totalPages),
|
||
|
|
HasPrev: page > 1,
|
||
|
|
HasNext: page < totalPages,
|
||
|
|
PrevHref: adminFilesHref(query, sortKey, dir, page-1),
|
||
|
|
NextHref: adminFilesHref(query, sortKey, dir, page+1),
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) boxOwnerLabel(ownerID string, cache map[string]string) string {
|
||
|
|
if ownerID == "" {
|
||
|
|
return "Anonymous"
|
||
|
|
}
|
||
|
|
if label, ok := cache[ownerID]; ok {
|
||
|
|
return label
|
||
|
|
}
|
||
|
|
label := "User"
|
||
|
|
if user, err := a.authService.UserByID(ownerID); err == nil {
|
||
|
|
label = user.Email
|
||
|
|
}
|
||
|
|
cache[ownerID] = label
|
||
|
|
return label
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminFilesSortKey(value string) string {
|
||
|
|
switch value {
|
||
|
|
case "id", "owner", "files", "size", "downloads", "expires", "created":
|
||
|
|
return value
|
||
|
|
default:
|
||
|
|
return "created"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func sortAdminFileRows(rows []adminFileRow, sortKey, dir string) {
|
||
|
|
less := func(i, j int) bool {
|
||
|
|
a, b := rows[i], rows[j]
|
||
|
|
switch sortKey {
|
||
|
|
case "id":
|
||
|
|
return strings.ToLower(a.ID) < strings.ToLower(b.ID)
|
||
|
|
case "owner":
|
||
|
|
return strings.ToLower(a.Owner) < strings.ToLower(b.Owner)
|
||
|
|
case "files":
|
||
|
|
return a.FileCount < b.FileCount
|
||
|
|
case "size":
|
||
|
|
return a.TotalSize < b.TotalSize
|
||
|
|
case "downloads":
|
||
|
|
return a.DownloadCount < b.DownloadCount
|
||
|
|
case "expires":
|
||
|
|
return a.ExpiresAt.Before(b.ExpiresAt)
|
||
|
|
default:
|
||
|
|
return a.CreatedAt.Before(b.CreatedAt)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
||
|
|
if dir == "desc" {
|
||
|
|
return less(j, i)
|
||
|
|
}
|
||
|
|
return less(i, j)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminFilesColumns(query, sortKey, dir string) []adminFilesColumn {
|
||
|
|
defs := []struct{ Key, Label string }{
|
||
|
|
{"id", "Box"},
|
||
|
|
{"owner", "Owner"},
|
||
|
|
{"files", "Files"},
|
||
|
|
{"size", "Size"},
|
||
|
|
{"downloads", "Downloads"},
|
||
|
|
{"created", "Created"},
|
||
|
|
{"expires", "Expires"},
|
||
|
|
}
|
||
|
|
columns := make([]adminFilesColumn, 0, len(defs))
|
||
|
|
for _, def := range defs {
|
||
|
|
sorted := sortKey == def.Key
|
||
|
|
nextDir := "asc"
|
||
|
|
if sorted && dir == "asc" {
|
||
|
|
nextDir = "desc"
|
||
|
|
}
|
||
|
|
columns = append(columns, adminFilesColumn{
|
||
|
|
Label: def.Label,
|
||
|
|
Href: adminFilesHref(query, def.Key, nextDir, 1),
|
||
|
|
Sorted: sorted,
|
||
|
|
Ascending: dir == "asc",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return columns
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminFilesPageLinks(query, sortKey, dir string, page, totalPages int) []adminFilesPageLink {
|
||
|
|
links := make([]adminFilesPageLink, 0, 5)
|
||
|
|
const window = 2
|
||
|
|
for p := page - window; p <= page+window; p++ {
|
||
|
|
if p < 1 || p > totalPages {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
links = append(links, adminFilesPageLink{
|
||
|
|
Page: p,
|
||
|
|
Href: adminFilesHref(query, sortKey, dir, p),
|
||
|
|
Active: p == page,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return links
|
||
|
|
}
|
||
|
|
|
||
|
|
func adminFilesHref(query, sortKey, dir string, page int) string {
|
||
|
|
values := url.Values{}
|
||
|
|
if query != "" {
|
||
|
|
values.Set("q", query)
|
||
|
|
}
|
||
|
|
if sortKey != "" && sortKey != "created" {
|
||
|
|
values.Set("sort", sortKey)
|
||
|
|
}
|
||
|
|
if dir != "" && dir != "desc" {
|
||
|
|
values.Set("dir", dir)
|
||
|
|
}
|
||
|
|
if page > 1 {
|
||
|
|
values.Set("page", strconv.Itoa(page))
|
||
|
|
}
|
||
|
|
if len(values) == 0 {
|
||
|
|
return "/admin/files"
|
||
|
|
}
|
||
|
|
return "/admin/files?" + values.Encode()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) AdminEditBox(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !a.requireAdmin(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
||
|
|
if err != nil {
|
||
|
|
http.NotFound(w, r)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var totalSize int64
|
||
|
|
files := make([]adminBoxEditFile, 0, len(box.Files))
|
||
|
|
for _, file := range box.Files {
|
||
|
|
totalSize += file.Size
|
||
|
|
files = append(files, adminBoxEditFile{
|
||
|
|
ID: file.ID,
|
||
|
|
Name: file.Name,
|
||
|
|
Size: helpers.FormatBytes(file.Size),
|
||
|
|
ContentType: file.ContentType,
|
||
|
|
ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID),
|
||
|
|
DownloadURL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
|
||
|
|
HasPreview: file.PreviewKind == "image" || file.PreviewKind == "video",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
never := neverExpires(box.ExpiresAt)
|
||
|
|
expiresInput := ""
|
||
|
|
if !never {
|
||
|
|
expiresInput = box.ExpiresAt.UTC().Format("2006-01-02T15:04")
|
||
|
|
}
|
||
|
|
|
||
|
|
cache := map[string]string{}
|
||
|
|
a.renderPage(w, r, http.StatusOK, "admin_box_edit.html", web.PageData{
|
||
|
|
Title: "Edit box",
|
||
|
|
Description: "Edit a Warpbox upload.",
|
||
|
|
CurrentUser: a.currentPublicUser(r),
|
||
|
|
Data: adminBoxEditData{
|
||
|
|
Section: "files",
|
||
|
|
PageTitle: "Edit box",
|
||
|
|
Notice: r.URL.Query().Get("notice"),
|
||
|
|
Error: r.URL.Query().Get("error"),
|
||
|
|
Files: files,
|
||
|
|
Box: adminBoxDetail{
|
||
|
|
ID: box.ID,
|
||
|
|
Owner: a.boxOwnerLabel(box.OwnerID, cache),
|
||
|
|
CreatedAt: box.CreatedAt.Format("Jan 2, 2006 15:04 MST"),
|
||
|
|
ExpiresLabel: boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST"),
|
||
|
|
ExpiresInput: expiresInput,
|
||
|
|
NeverExpires: never,
|
||
|
|
MaxDownloads: box.MaxDownloads,
|
||
|
|
DownloadCount: box.DownloadCount,
|
||
|
|
FileCount: len(box.Files),
|
||
|
|
TotalSize: helpers.FormatBytes(totalSize),
|
||
|
|
BackendID: a.uploadService.BoxStorageBackendID(box),
|
||
|
|
Protected: a.uploadService.IsProtected(box),
|
||
|
|
Obfuscated: box.Obfuscate,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) AdminUpdateBox(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
boxID := r.PathValue("boxID")
|
||
|
|
if err := r.ParseForm(); err != nil {
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+read+form", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var expiresAt time.Time
|
||
|
|
if r.FormValue("never_expires") == "on" {
|
||
|
|
expiresAt = time.Now().UTC().AddDate(100, 0, 0)
|
||
|
|
} else {
|
||
|
|
parsed, err := time.Parse("2006-01-02T15:04", strings.TrimSpace(r.FormValue("expires_at")))
|
||
|
|
if err != nil {
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Invalid+expiration+date", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
expiresAt = parsed.UTC()
|
||
|
|
}
|
||
|
|
|
||
|
|
maxDownloads := parsePositiveInt(r.FormValue("max_downloads"))
|
||
|
|
removePassword := r.FormValue("remove_password") == "on"
|
||
|
|
|
||
|
|
if err := a.uploadService.AdminUpdateBox(boxID, expiresAt, maxDownloads, removePassword); err != nil {
|
||
|
|
a.logger.Warn("admin box update failed", "source", "admin", "severity", "warn", "code", 4306, "box_id", boxID, "error", err.Error())
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+save+changes", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
a.logger.Info("admin box updated", "source", "admin", "severity", "user_activity", "code", 2306, "ip", uploadClientIP(r), "box_id", boxID)
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?notice=Changes+saved", http.StatusSeeOther)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) AdminDeleteBoxFile(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
boxID := r.PathValue("boxID")
|
||
|
|
fileID := r.PathValue("fileID")
|
||
|
|
boxDeleted, err := a.uploadService.RemoveFileFromBox(boxID, fileID)
|
||
|
|
if err != nil {
|
||
|
|
a.logger.Warn("admin file delete failed", "source", "admin", "severity", "warn", "code", 4305, "box_id", boxID, "file_id", fileID, "error", err.Error())
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+remove+file", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
a.logger.Info("admin removed box file", "source", "admin", "severity", "user_activity", "code", 2305, "ip", uploadClientIP(r), "box_id", boxID, "file_id", fileID)
|
||
|
|
if boxDeleted {
|
||
|
|
http.Redirect(w, r, "/admin/files?notice=Box+deleted+(last+file+removed)", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?notice=File+removed", http.StatusSeeOther)
|
||
|
|
}
|