Add files page

This commit is contained in:
2026-09-13 11:28:17 +02:00
parent ab0e1b47d8
commit 30826f3671
7 changed files with 247 additions and 81 deletions
+59 -24
View File
@@ -10,16 +10,24 @@ import (
"uncensored-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 {
// filesPage lists uploads. One page serves two readers: an admin sees every
// object plus what the server as a whole is holding, and a token holder sees
// the files uploaded under their own token, which is otherwise information
// they have no way to get back.
type filesPage struct {
page
Objects []adminObject
Tokens []adminToken
Sort string
Count int
Listed string // bytes held by the files actually listed
Count int
// NeedsToken replaces the listing with an invitation to log in. Uploads
// are recorded against the token that made them, so there is nothing to
// show someone who has not presented one.
NeedsToken bool
// The rest is the server's own state, and only an admin sees it.
Tokens []adminToken
Total string
Quota string // empty when there is no quota
QuotaPct int
@@ -79,17 +87,19 @@ func compareExpiry(a, b *time.Time) int {
return a.Compare(*b)
}
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) {
lim, err := s.limitsFor(r, credential(r))
switch {
case err != nil:
if 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.")
}
if lim.Anonymous() {
// Not an error: the page exists, it just has nothing to say without a
// token. An anonymous upload is not recorded against anyone.
s.render(w, http.StatusOK, "files.html", filesPage{
page: s.page(r, "Files", false),
NeedsToken: true,
})
return
}
@@ -100,16 +110,23 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
now := s.now()
objects := make([]adminObject, 0, s.store.Count())
anonymous := 0
anonymous, listed := 0, int64(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
}
// A token holder sees their own uploads and nothing else. Anonymous
// files belong to no token, so they stay with the admins, which is
// exactly who may delete them.
if !lim.Admin && m.Owner != lim.Name {
continue
}
if m.Owner == "" {
anonymous++
}
listed += m.Size
objects = append(objects, adminObject{
ID: m.ID,
Filename: m.Filename,
@@ -127,15 +144,33 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
}
slices.SortStableFunc(objects, adminSorts[sortBy])
data := adminPage{
page: s.widePage(r, "Administration"),
Objects: objects,
Tokens: s.adminTokens(),
Sort: sortBy,
Count: len(objects),
Total: config.FormatBytes(s.store.Total()),
Anonymous: anonymous,
title := "Your files"
if lim.Admin {
title = "All files"
}
// s.page resolves the session cookie, which is the right thing for the
// header but not for this body: the page has to describe the credential it
// was actually read with, or an admin presenting a bearer token is shown a
// plain user's view of a listing that was built for an admin.
head := s.widePage(r, title)
head.User, head.Admin = lim.Name, lim.Admin
data := filesPage{
page: head,
Objects: objects,
Sort: sortBy,
Count: len(objects),
Listed: config.FormatBytes(listed),
}
if !lim.Admin {
s.render(w, http.StatusOK, "files.html", data)
return
}
// Everything below is the server's own state rather than anyone's files.
data.Tokens = s.adminTokens()
data.Total = config.FormatBytes(s.store.Total())
data.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)))
@@ -143,7 +178,7 @@ func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
if free, ok := freeBytes(s.store.DataDir()); ok {
data.FreeDisk = config.FormatBytes(free)
}
s.render(w, http.StatusOK, "admin.html", data)
s.render(w, http.StatusOK, "files.html", data)
}
// adminTokens describes the configured credentials. Only names and limits are