Files
felhom.eu/hub/internal/store/offsite_snapshots.go
T
admin 7d64038f02
gates / gates (push) Successful in 6m45s
Hub v0.158.0 code (built, NOT deployed): agent-only vouch, R-895 snapshot alarms, the hub's own 90-day access log (R-1140 A); installer --rotate-recovery-only (R-1141); 02/03/05/09 + break-glass
Hub: the R-120 golden guard judges only a CHANGED golden selection; offsite_snapshots_vanished /
_unexplained (operator-only, hub-origin) on the 07:10 listing; internal/accesslog writes path-only
lines to /data/access-log, 90 days. Installer: --rotate-recovery-only runs step 4b alone (rides
the next installer tag). 03: the orphan check runs after a failed backup (agent 6545ccc). 02: the
allow-list is empty, appdata_folders (controller v0.329.0). All red-proofed.
2026-10-17 23:31:34 +02:00

97 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"encoding/json"
"sort"
"time"
)
// R-895 slice 2 (hub v0.158.0): the hub's OWN listing of a box's off-site `snapshots/` directory, kept between two
// daily audits so the next one can tell which snapshot files vanished and which appeared. Names and upload times only
// — never contents (`07` §8a custody). One hub_settings row per customer (≈ 70 names × ~90 bytes), the same per-key
// pattern as ForeignKeyArchiveSignature.
const offsiteSnapshotListingKeyPrefix = "offsite_snapshot_listing:"
// OffsiteSnapshotListing is the stored baseline: when it was listed, and each snapshot id with its upload time.
type OffsiteSnapshotListing struct {
ListedAt time.Time `json:"listed_at"`
Entries map[string]time.Time `json:"entries"`
}
// GetOffsiteSnapshotListing returns the stored baseline; ok=false when there is none (or it does not parse — a
// baseline that cannot be read is no baseline, and no baseline means no verdict).
func (s *Store) GetOffsiteSnapshotListing(customerID string) (OffsiteSnapshotListing, bool) {
raw := s.getSetting(offsiteSnapshotListingKeyPrefix + customerID)
if raw == "" {
return OffsiteSnapshotListing{}, false
}
var l OffsiteSnapshotListing
if err := json.Unmarshal([]byte(raw), &l); err != nil || l.ListedAt.IsZero() {
return OffsiteSnapshotListing{}, false
}
if l.Entries == nil {
l.Entries = map[string]time.Time{}
}
return l, true
}
// SetOffsiteSnapshotListing replaces the stored baseline.
func (s *Store) SetOffsiteSnapshotListing(customerID string, l OffsiteSnapshotListing) error {
b, err := json.Marshal(l)
if err != nil {
return err
}
return s.setSetting(offsiteSnapshotListingKeyPrefix+customerID, string(b))
}
// ClearOffsiteSnapshotListing drops the baseline (the repository was set aside or purged — the next listing is a
// fresh baseline, never a comparison against a different repository).
func (s *Store) ClearOffsiteSnapshotListing(customerID string) error {
_, err := s.db.Exec(`DELETE FROM hub_settings WHERE key = ?`, offsiteSnapshotListingKeyPrefix+customerID)
return err
}
// OffsiteRunTimesSince returns the distinct off-site run times (`offsite.last_run`, written by the box at the END of
// every run — scheduled, manual, catch-up or retry) carried by the customer's controller reports received after
// `since`, oldest first. A run is reported in every report until the next run, so a 15-minute report cadence sees
// every run that is at least one report apart. Capped at 5000 reports (≈ 50 days at the measured cadence).
func (s *Store) OffsiteRunTimesSince(customerID string, since time.Time) ([]time.Time, error) {
rows, err := s.db.Query(`SELECT report_json FROM reports WHERE customer_id = ? AND received_at > ? ORDER BY id LIMIT 5000`,
customerID, since.UTC().Format("2006-01-02 15:04:05"))
if err != nil {
return nil, err
}
defer rows.Close()
seen := map[time.Time]bool{}
var out []time.Time
for rows.Next() {
var reportJSON string
if err := rows.Scan(&reportJSON); err != nil {
return nil, err
}
var p struct {
Offsite *struct {
LastRun string `json:"last_run"`
} `json:"offsite"`
}
if json.Unmarshal([]byte(reportJSON), &p) != nil || p.Offsite == nil || p.Offsite.LastRun == "" {
continue
}
t, err := time.Parse(time.RFC3339, p.Offsite.LastRun)
if err != nil {
continue
}
t = t.UTC()
if !seen[t] {
seen[t] = true
out = append(out, t)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool { return out[i].Before(out[j]) })
return out, nil
}