f9718e6077
CI / Test (rest) (pull_request) Successful in 40s
CI / Test (store) (pull_request) Successful in 42s
CI / Lint (pull_request) Successful in 11s
CI / Build (windows/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Test (server-http) (pull_request) Successful in 1m32s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m26s
120 lines
3.8 KiB
Go
120 lines
3.8 KiB
Go
package http
|
|
|
|
import (
|
|
stdhttp "net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/oklog/ulid/v2"
|
|
|
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
|
)
|
|
|
|
// snapshotView is the public JSON shape for a snapshot. Matches the
|
|
// wire-protocol Snapshot type plus a `refreshed_at` field so the UI
|
|
// can show "list last refreshed Xm ago" — useful when the projection
|
|
// is stale because the host has been offline for a while.
|
|
type snapshotView struct {
|
|
ID string `json:"id"`
|
|
ShortID string `json:"short_id"`
|
|
Time time.Time `json:"time"`
|
|
Hostname string `json:"hostname"`
|
|
Paths []string `json:"paths"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
SizeBytes int64 `json:"size_bytes,omitempty"`
|
|
FileCount int64 `json:"file_count,omitempty"`
|
|
}
|
|
|
|
type listSnapshotsResponse struct {
|
|
HostID string `json:"host_id"`
|
|
Count int `json:"count"`
|
|
RefreshedAt *time.Time `json:"refreshed_at,omitempty"`
|
|
Stale bool `json:"stale"`
|
|
Snapshots []snapshotView `json:"snapshots"`
|
|
}
|
|
|
|
// handleListHostSnapshots returns the cached snapshot projection for
|
|
// one host. The agent refreshes this by sending `snapshots.report`
|
|
// after each successful backup; this endpoint is a read-only view
|
|
// onto whatever the server most recently received.
|
|
func (s *Server) handleListHostSnapshots(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
|
if _, ok := s.requireUser(r); !ok {
|
|
writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
|
|
return
|
|
}
|
|
|
|
hostID := chi.URLParam(r, "id")
|
|
if hostID == "" {
|
|
writeJSONError(w, stdhttp.StatusBadRequest, "missing_host_id", "")
|
|
return
|
|
}
|
|
|
|
host, err := s.deps.Store.GetHost(r.Context(), hostID)
|
|
if err != nil {
|
|
writeJSONError(w, stdhttp.StatusNotFound, "host_not_found", "")
|
|
return
|
|
}
|
|
|
|
snaps, err := s.deps.Store.ListSnapshotsByHost(r.Context(), hostID)
|
|
if err != nil {
|
|
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
|
return
|
|
}
|
|
|
|
out := listSnapshotsResponse{
|
|
HostID: hostID,
|
|
Count: len(snaps),
|
|
Snapshots: make([]snapshotView, len(snaps)),
|
|
}
|
|
out.RefreshedAt = host.SnapshotRefreshedAt
|
|
mutationAt, err := s.deps.Store.LatestSuccessfulRepoMutation(r.Context(), hostID)
|
|
if err != nil {
|
|
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
|
|
return
|
|
}
|
|
out.Stale = mutationAt != nil && (out.RefreshedAt == nil || out.RefreshedAt.Before(*mutationAt))
|
|
for i, sn := range snaps {
|
|
out.Snapshots[i] = snapshotView{
|
|
ID: sn.ID,
|
|
ShortID: sn.ShortID,
|
|
Time: sn.Time,
|
|
Hostname: sn.Hostname,
|
|
Paths: sn.Paths,
|
|
Tags: sn.Tags,
|
|
SizeBytes: sn.SizeBytes,
|
|
FileCount: sn.FileCount,
|
|
}
|
|
}
|
|
|
|
writeJSON(w, stdhttp.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) handleRefreshHostSnapshots(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
|
user, ok := s.requireUser(r)
|
|
if !ok {
|
|
writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
|
|
return
|
|
}
|
|
hostID := chi.URLParam(r, "id")
|
|
if _, err := s.deps.Store.GetHost(r.Context(), hostID); err != nil {
|
|
writeJSONError(w, stdhttp.StatusNotFound, "host_not_found", "")
|
|
return
|
|
}
|
|
if s.deps.Hub == nil || !s.deps.Hub.Connected(hostID) {
|
|
writeJSONError(w, stdhttp.StatusConflict, "host_offline", "agent is not currently connected")
|
|
return
|
|
}
|
|
env, _ := api.Marshal(api.MsgSnapshotsRefresh, ulid.Make().String(), nil)
|
|
if err := s.deps.Hub.Send(r.Context(), hostID, env); err != nil {
|
|
writeJSONError(w, stdhttp.StatusConflict, "host_offline", err.Error())
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
|
ID: ulid.Make().String(), UserID: &user.ID, Actor: "user",
|
|
Action: "host.snapshots_refresh", TargetKind: ptr("host"), TargetID: &hostID, TS: now,
|
|
})
|
|
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "refresh_requested"})
|
|
}
|