package main // A backup taken by hand (novox/hq issue 453): `node-backup.now`, asked by a person before a risky change or by // the mesh before a module moves (ADR 0233, the bus's snapshot before its upgrade). The thinning after every // backup keeps only the newest restore point of each day, so one taken at 02:35 was forgotten by the night at // 03:01 — while the night's may already hold the damage it was taken against. // // So a backup taken by hand is tagged HandTag beside its module's tag, and is kept by restic's own --keep-tag: // with the thinning grouping by tags, it is its own group too, never counted against the nights. It is kept // until a later good night of its module exists, and for the machine's keep-hand-taken-days in any case; past // both, the thinning forgets it by its id (handLetGo). The nights are thinned by their counts alone: no rule // that keeps one keeps every night. import ( "context" "fmt" "slices" "strings" "time" ) // HandTag marks a restore point taken by hand. const HandTag = "hand" // BackUpNow is a backup asked for now, of every module or one: as a night, each restore point tagged HandTag. func (b *Backups) BackUpNow(ctx context.Context, only string) (map[string]Night, error) { return b.backUp(ctx, only, true) } // isHand is whether a restore point was taken by hand. func isHand(s Snapshot) bool { return slices.Contains(s.Tags, HandTag) } // moduleOf is the module a restore point is of; empty when it carries no module's tag. func moduleOf(s Snapshot) string { for _, t := range s.Tags { if m, ok := strings.CutPrefix(t, "module="); ok && m != "" { return m } } return "" } // isNight is whether a restore point is a night's: its module's tag and nothing else — neither taken by hand // nor a retired item's last one. Restic writes a snapshot only for a backup that succeeded, so each is good. func isNight(s Snapshot) bool { return len(s.Tags) == 1 && moduleOf(s) != "" } // latestNights is the time of each module's newest night. func latestNights(snaps []Snapshot) map[string]time.Time { latest := map[string]time.Time{} for _, s := range snaps { if m := moduleOf(s); isNight(s) && s.Time.After(latest[m]) { latest[m] = s.Time } } return latest } // handLetGo is the restore points taken by hand that the thinning forgets: older than days, and with a night of // the same module after them. Every other one taken by hand is kept. func handLetGo(snaps []Snapshot, now time.Time, days int) []Snapshot { latest := latestNights(snaps) cutoff := now.AddDate(0, 0, -days) var out []Snapshot for _, s := range snaps { if isHand(s) && s.Time.Before(cutoff) && latest[moduleOf(s)].After(s.Time) { out = append(out, s) } } return out } // HandKept is one restore point taken by hand, as `backed-up` says it. type HandKept struct { Snapshot string `json:"snapshot"` At time.Time `json:"at"` // KeptAtLeastUntil is when its days are up; WaitsForANight that no later good night of its module exists // yet, and it is kept until one does as well. The first thinning after both forgets it. KeptAtLeastUntil time.Time `json:"keptAtLeastUntil"` WaitsForANight bool `json:"waitsForANight"` Kept string `json:"kept"` } // handKept is a module's restore points taken by hand, oldest first, said with the retention in force. func handKept(module string, snaps []Snapshot, keep InForce) []HandKept { latest := latestNights(snaps) out := []HandKept{} for _, s := range snaps { if !isHand(s) || moduleOf(s) != module { continue } k := HandKept{Snapshot: s.ShortID, At: s.Time, WaitsForANight: !latest[module].After(s.Time)} switch { case keep.Retention == nil: k.Kept = "kept: nothing is thinned while the retention is refused" case k.WaitsForANight: k.KeptAtLeastUntil = s.Time.AddDate(0, 0, keep.Hand) k.Kept = fmt.Sprintf("kept as taken by hand until a later good night of %s exists, and at least until %s", module, k.KeptAtLeastUntil.UTC().Format(time.RFC3339)) default: k.KeptAtLeastUntil = s.Time.AddDate(0, 0, keep.Hand) k.Kept = fmt.Sprintf("kept as taken by hand until %s; the first thinning after forgets it", k.KeptAtLeastUntil.UTC().Format(time.RFC3339)) } out = append(out, k) } slices.SortFunc(out, func(a, b HandKept) int { return a.At.Compare(b.At) }) return out }