Add admin interface
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"send/internal/config"
|
||||
)
|
||||
|
||||
// adminPage is the one view that shows every object, regardless of who
|
||||
// uploaded it. It exists because "admin" otherwise only means "may delete
|
||||
// anyone's file", with no way to see whose files those are.
|
||||
type adminPage struct {
|
||||
page
|
||||
Objects []adminObject
|
||||
Tokens []adminToken
|
||||
Sort string
|
||||
|
||||
Count int
|
||||
Total string
|
||||
Quota string // empty when there is no quota
|
||||
QuotaPct int
|
||||
FreeDisk string
|
||||
Anonymous int
|
||||
}
|
||||
|
||||
type adminObject struct {
|
||||
ID string
|
||||
Filename string
|
||||
Size string
|
||||
Bytes int64
|
||||
Owner string
|
||||
Created string
|
||||
Expires string
|
||||
Vanity bool
|
||||
}
|
||||
|
||||
type adminToken struct {
|
||||
Name string
|
||||
MaxSize string
|
||||
MaxExpiry string
|
||||
Vanity bool
|
||||
Admin bool
|
||||
}
|
||||
|
||||
// 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) },
|
||||
"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) },
|
||||
"owner": func(a, b adminObject) int { return cmp.Compare(a.Owner, b.Owner) },
|
||||
}
|
||||
|
||||
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
lim, err := s.limitsFor(credential(r))
|
||||
switch {
|
||||
case err != nil:
|
||||
s.fail(w, r, http.StatusUnauthorized, "Unrecognised token.")
|
||||
return
|
||||
case lim.Anonymous():
|
||||
s.fail(w, r, http.StatusUnauthorized, "This page needs an admin token.")
|
||||
return
|
||||
case !lim.Admin:
|
||||
s.fail(w, r, http.StatusForbidden, "That token is not an admin token.")
|
||||
return
|
||||
}
|
||||
|
||||
sortBy := r.URL.Query().Get("sort")
|
||||
if _, ok := adminSorts[sortBy]; !ok {
|
||||
sortBy = "created"
|
||||
}
|
||||
|
||||
now := s.now()
|
||||
objects := make([]adminObject, 0, s.store.Count())
|
||||
anonymous := 0
|
||||
for _, m := range s.store.List() {
|
||||
// Expired objects are logically gone even if the sweeper has not yet
|
||||
// reached them, so they are not listed as though they were still here.
|
||||
if m.Expired(now) {
|
||||
continue
|
||||
}
|
||||
if m.Owner == "" {
|
||||
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,
|
||||
})
|
||||
}
|
||||
slices.SortStableFunc(objects, adminSorts[sortBy])
|
||||
|
||||
data := adminPage{
|
||||
page: s.page(r, "Administration", true),
|
||||
Objects: objects,
|
||||
Tokens: s.adminTokens(),
|
||||
Sort: sortBy,
|
||||
Count: len(objects),
|
||||
Total: config.FormatBytes(s.store.Total()),
|
||||
Anonymous: anonymous,
|
||||
}
|
||||
if s.cfg.MaxTotalBytes != config.Unlimited {
|
||||
data.Quota = config.FormatSize(s.cfg.MaxTotalBytes)
|
||||
data.QuotaPct = int(min(100, s.store.Total()*100/max(1, s.cfg.MaxTotalBytes)))
|
||||
}
|
||||
if free, ok := freeBytes(s.store.DataDir()); ok {
|
||||
data.FreeDisk = config.FormatBytes(free)
|
||||
}
|
||||
s.render(w, http.StatusOK, "admin.html", data)
|
||||
}
|
||||
|
||||
// adminTokens describes the configured credentials. Only names and limits are
|
||||
// exposed; the hashes stay where they are, and minting stays in the CLI, where
|
||||
// it is not reachable over the network at all.
|
||||
func (s *Server) adminTokens() []adminToken {
|
||||
if err := s.tokens.MaybeReload(); err != nil {
|
||||
s.log.Error("reloading token file", "err", err)
|
||||
}
|
||||
var out []adminToken
|
||||
for _, t := range s.tokens.List() {
|
||||
l := t.Limits(s.cfg)
|
||||
out = append(out, adminToken{
|
||||
Name: t.Name,
|
||||
MaxSize: config.FormatSize(l.MaxSize),
|
||||
MaxExpiry: config.FormatDuration(l.MaxExpiry),
|
||||
Vanity: l.AllowVanity,
|
||||
Admin: l.Admin,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// expiresSortable renders a deadline so that string ordering is chronological
|
||||
// and "never" sorts last.
|
||||
func expiresSortable(t *time.Time) string {
|
||||
if t == nil {
|
||||
return "never"
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
Reference in New Issue
Block a user