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 }