-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprune.go
More file actions
104 lines (94 loc) · 3.36 KB
/
Copy pathprune.go
File metadata and controls
104 lines (94 loc) · 3.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package app
import (
"context"
"time"
"github.com/nixrajput/siphon/internal/dumps"
)
// PruneOpts configures a prune run. Policy is resolved by the caller (the CLI
// maps config + flags into it), so this layer stays config-agnostic. Profile
// scopes the catalog to one profile's dumps ("" = all profiles). Apply performs
// deletions; otherwise the run is a dry-run that only computes the plan.
type PruneOpts struct {
Profile string
Policy dumps.RetentionPolicy
Apply bool
}
// ChainOutcome is one chain's place in the plan, flattened for reporting.
type ChainOutcome struct {
Root string
DumpIDs []string
SizeBytes int64
Pruned bool // true = scheduled for (or performed) deletion
Deleted []string // dump IDs actually deleted (Apply only)
Errors []string // per-dump deletion failures (Apply only)
}
// PruneResult is the structured outcome the CLI renders.
type PruneResult struct {
Profile string
Apply bool
Outcomes []ChainOutcome
Reclaimed int64 // bytes freed (sum of successfully deleted dumps; Apply only)
Failed int // count of dumps that failed to delete
}
// Prune groups the catalog into chains, plans retention over them, and (when
// Apply) deletes the pruned chains. It is synchronous (like Verify): prune is a
// list + a few deletes, not a long stream, so it returns a structured result
// directly rather than over the job channel.
//
// Deletion is chain-aware and leaf-inward: within a pruned chain, incrementals
// are removed before the base, so an interrupted prune leaves at worst a
// complete shorter chain — never a base missing under a surviving incremental.
func Prune(ctx context.Context, d Deps, opt PruneOpts) (*PruneResult, error) {
all, err := d.Dumps.List(ctx)
if err != nil {
return nil, err
}
// Scope to the requested profile before grouping, so chains and the plan
// reflect only this profile's backups.
if opt.Profile != "" {
filtered := make([]dumps.Meta, 0, len(all))
for _, m := range all {
if m.Profile == opt.Profile {
filtered = append(filtered, m)
}
}
all = filtered
}
plan := dumps.Plan(dumps.GroupChains(all), opt.Policy, time.Now())
result := &PruneResult{Profile: opt.Profile, Apply: opt.Apply}
for _, ch := range plan.Keep {
result.Outcomes = append(result.Outcomes, chainOutcome(ch, false))
}
for _, ch := range plan.Prune {
oc := chainOutcome(ch, true)
if opt.Apply {
applyChainDeletion(ctx, d, ch, &oc, result)
}
result.Outcomes = append(result.Outcomes, oc)
}
return result, nil
}
// applyChainDeletion deletes a pruned chain's members leaf-inward (incrementals
// before the base). A per-dump failure is collected and does not abort the rest
// of the chain or the run — a single stuck object should not block reclaiming
// the others.
func applyChainDeletion(ctx context.Context, d Deps, ch dumps.Chain, oc *ChainOutcome, result *PruneResult) {
for i := len(ch.Members) - 1; i >= 0; i-- {
m := ch.Members[i]
if err := d.Dumps.Delete(ctx, m.ID); err != nil {
oc.Errors = append(oc.Errors, m.ID+": "+err.Error())
result.Failed++
continue
}
oc.Deleted = append(oc.Deleted, m.ID)
result.Reclaimed += m.SizeBytes
}
}
func chainOutcome(ch dumps.Chain, pruned bool) ChainOutcome {
oc := ChainOutcome{Root: ch.Root, Pruned: pruned}
for _, m := range ch.Members {
oc.DumpIDs = append(oc.DumpIDs, m.ID)
oc.SizeBytes += m.SizeBytes
}
return oc
}