Widen the administration interface

This commit is contained in:
2026-09-13 01:17:08 +02:00
parent aad1b931ba
commit eaa0a5d1d7
6 changed files with 300 additions and 104 deletions
+85 -18
View File
@@ -2,6 +2,7 @@ package server
import (
"cmp"
"fmt"
"net/http"
"slices"
"time"
@@ -32,9 +33,19 @@ type adminObject struct {
Size string
Bytes int64
Owner string
Created string
Expires string
Vanity bool
// Times are kept three ways: the value itself, which is what sorting
// compares; an absolute rendering for the tooltip; and a relative one,
// which is what you actually read when deciding whether a file still
// matters. Sorting on the rendered string would tie everything that
// happened within the same minute.
createdAt time.Time
expiresAt *time.Time
Created string
CreatedAgo string
Expires string
ExpiresIn string
}
type adminToken struct {
@@ -48,13 +59,26 @@ type adminToken struct {
// adminSorts maps the sort parameter to a comparison. Restricting to this set
// keeps the parameter from reaching anything that interprets it.
var adminSorts = map[string]func(a, b adminObject) int{
"created": func(a, b adminObject) int { return cmp.Compare(b.Created, a.Created) },
"expires": func(a, b adminObject) int { return cmp.Compare(a.Expires, b.Expires) },
"created": func(a, b adminObject) int { return b.createdAt.Compare(a.createdAt) },
"expires": func(a, b adminObject) int { return compareExpiry(a.expiresAt, b.expiresAt) },
"size": func(a, b adminObject) int { return cmp.Compare(b.Bytes, a.Bytes) },
"name": func(a, b adminObject) int { return cmp.Compare(a.ID, b.ID) },
"name": func(a, b adminObject) int { return cmp.Compare(a.Filename, b.Filename) },
"owner": func(a, b adminObject) int { return cmp.Compare(a.Owner, b.Owner) },
}
// compareExpiry orders soonest first, with "never" last where it belongs.
func compareExpiry(a, b *time.Time) int {
switch {
case a == nil && b == nil:
return 0
case a == nil:
return 1
case b == nil:
return -1
}
return a.Compare(*b)
}
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
lim, err := s.limitsFor(r, credential(r))
switch {
@@ -87,20 +111,24 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
anonymous++
}
objects = append(objects, adminObject{
ID: m.ID,
Filename: m.Filename,
Size: config.FormatBytes(m.Size),
Bytes: m.Size,
Owner: m.Owner,
Created: m.Created.UTC().Format(time.RFC3339),
Expires: expiresSortable(m.Expires),
Vanity: m.Vanity,
ID: m.ID,
Filename: m.Filename,
Size: config.FormatBytes(m.Size),
Bytes: m.Size,
Owner: m.Owner,
Vanity: m.Vanity,
createdAt: m.Created,
expiresAt: m.Expires,
Created: absolute(&m.Created),
CreatedAgo: relative(m.Created, now),
Expires: absolute(m.Expires),
ExpiresIn: expiresRelative(m.Expires, now),
})
}
slices.SortStableFunc(objects, adminSorts[sortBy])
data := adminPage{
page: s.page(r, "Administration", true),
page: s.widePage(r, "Administration"),
Objects: objects,
Tokens: s.adminTokens(),
Sort: sortBy,
@@ -139,11 +167,50 @@ func (s *Server) adminTokens() []adminToken {
return out
}
// expiresSortable renders a deadline so that string ordering is chronological
// and "never" sorts last.
func expiresSortable(t *time.Time) string {
// absolute renders a time for a tooltip, where the reader wants the real value
// rather than a distance from now.
func absolute(t *time.Time) string {
if t == nil {
return "never"
}
return t.UTC().Format(time.RFC3339)
return t.UTC().Format("2006-01-02 15:04 MST")
}
func expiresRelative(t *time.Time, now time.Time) string {
if t == nil {
return "never"
}
return relative(*t, now)
}
// relative renders a time as a short distance from now - "3d ago", "in 4h" -
// which is what a listing is actually read for. The exact time stays available
// in the cell's tooltip.
func relative(t, now time.Time) string {
d := t.Sub(now)
ahead := d > 0
if !ahead {
d = -d
}
var magnitude string
switch {
case d < time.Minute:
magnitude = "now"
case d < time.Hour:
magnitude = fmt.Sprintf("%dm", int(d.Minutes()))
case d < 24*time.Hour:
magnitude = fmt.Sprintf("%dh", int(d.Hours()))
case d < 365*24*time.Hour:
magnitude = fmt.Sprintf("%dd", int(d.Hours()/24))
default:
magnitude = fmt.Sprintf("%dy", int(d.Hours()/24/365))
}
switch {
case magnitude == "now":
return "now"
case ahead:
return "in " + magnitude
default:
return magnitude + " ago"
}
}