Files
pages/internal/adminapi/system.go
T
2026-08-15 07:13:00 +00:00

126 lines
3.6 KiB
Go

package adminapi
import (
"net/http"
"time"
"github.com/iceBear67/simplepages/api"
"github.com/iceBear67/simplepages/internal/httpx"
"github.com/iceBear67/simplepages/internal/version"
)
// whoami handles GET /api/v1/whoami. Any valid key may call it; a CI job uses
// it to check that the token it was handed is the one it expected.
func (s *Server) whoami(w http.ResponseWriter, r *http.Request) error {
ident, err := s.identity(r)
if err != nil {
return err
}
out := api.WhoAmI{
KeyID: ident.KeyID,
Scope: string(ident.Scope),
Name: ident.Name,
ExpiresAt: copyTime(ident.ExpiresAt),
}
if ident.ProjectID != nil {
// The identity carries the row id, not the name; the name is what a
// human wants to see. A missing row here would mean the project was
// deleted between authentication and now, in which case reporting an
// empty name is more honest than failing the request.
if p, err := s.DB.ProjectByID(r.Context(), *ident.ProjectID); err == nil {
out.Project = p.Name
}
}
noStore(w)
httpx.WriteJSON(w, http.StatusOK, out)
return nil
}
// systemInfo handles GET /api/v1/system/info, admin only.
func (s *Server) systemInfo(w http.ResponseWriter, r *http.Request) error {
counts, err := s.DB.Counts(r.Context())
if err != nil {
return err
}
schema, err := s.DB.SchemaVersion(r.Context())
if err != nil {
return err
}
linkMode := s.LinkMode
if linkMode == "" {
linkMode = "unknown"
}
uptime := int64(0)
if !s.Started.IsZero() {
uptime = int64(time.Since(s.Started).Seconds())
}
httpx.WriteJSON(w, http.StatusOK, api.SystemInfo{
Version: version.Short(),
UptimeS: uptime,
Projects: counts.Projects,
Deployments: counts.Deployments,
Blobs: counts.Blobs,
CASBytes: counts.CASBytes,
LinkMode: linkMode,
SchemaVer: schema,
})
return nil
}
// gc handles POST /api/v1/gc, admin only.
//
// A collection pass also runs on a timer; this exists so an operator who needs
// the disk back does not have to wait for it, and so that a dry run can answer
// "what would you delete" before anything is deleted.
func (s *Server) gc(w http.ResponseWriter, r *http.Request) error {
var req api.GCRequest
if r.ContentLength != 0 {
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &req); err != nil {
return err
}
}
stats, err := s.Deploy.Collect(r.Context(), req.DryRun)
if err != nil {
return err
}
httpx.WriteJSON(w, http.StatusOK, stats)
return nil
}
// fsck handles POST /api/v1/fsck, admin only.
//
// Refcounts are maintained by triggers, so on a healthy server this always
// reports zero drift. It exists for the cases outside normal operation — a
// database restored from a backup, a schema touched by hand — because the
// collector trusts those counters, and a count that reads low is the one way
// this design can lose data.
func (s *Server) fsck(w http.ResponseWriter, r *http.Request) error {
var req api.FsckRequest
if r.ContentLength != 0 {
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &req); err != nil {
return err
}
}
rep, err := s.DB.Fsck(r.Context(), req.Repair)
if err != nil {
return err
}
out := api.FsckReport{
Blobs: rep.Blobs,
DriftCount: rep.DriftCount,
Repaired: rep.Repaired,
Drift: make([]api.BlobDrift, 0, len(rep.Drift)),
}
for _, d := range rep.Drift {
out.Drift = append(out.Drift, api.BlobDrift{
Digest: d.Digest.String(), Stored: d.Stored, Actual: d.Actual,
})
}
if rep.DriftCount > 0 {
s.Log.WarnContext(r.Context(), "blob reference counts disagree with the manifests",
"blobs", rep.Blobs, "drift", rep.DriftCount, "repaired", rep.Repaired)
}
httpx.WriteJSON(w, http.StatusOK, out)
return nil
}