// Package jobs provides definitions and features for Backup Jobs themselves. // [BackupJob] is the primary resource here. // [BackupJob.Do] is the main entrypoint for running through the backup process. package jobs import ( "context" "fmt" "log" "strings" "time" "git.gentoo.party/sam/thanks/internal/zfs" ) type BackupJob struct { Source string `yaml:"source"` // the source dataset (e.g., zroot) TargetHost string `yaml:"targetHost"` // SSH-compatible host Target string `yaml:"target"` // the target dataset Keep int `yaml:"keep"` // number of snapshots to keep MaxAge time.Duration `yaml:"maxAge"` // age at which to delete snapshot (if Keep is met) Recursive bool `yaml:"recursive"` // create recursive snapshots Prefix string `yaml:"prefix"` // name each snapshot with this prefix } // func (j *BackupJob) getBaseSnap() { // params := "zfs get -j -d 1 -t snapshot guid,creation %s" // srcCmd := fmt.Sprintf(params, j.Source) // dstCmd := fmt.Sprintf(params, j.Target) // } func (j *BackupJob) listSnapshots(ctx context.Context) ([]zfs.Snapshot, error) { allSnaps, err := zfs.Snapshots(ctx, j.Source) if err != nil { return nil, err } filtered := make([]zfs.Snapshot, 0) for _, snap := range allSnaps { if !strings.Contains(snap.Name, j.Prefix) { continue } filtered = append(filtered, snap) } return filtered, nil } func (j *BackupJob) snapName() string { // ts := time.Now().Format(time.RFC3339) ts := time.Now().UnixMicro() if j.Prefix == "" { return fmt.Sprintf("%d", ts) } else { return fmt.Sprintf("%s%d", j.Prefix, ts) } } func (j *BackupJob) Snapshot(ctx context.Context) (string, error) { snapName := j.snapName() snap := fmt.Sprintf("%s@%s", j.Source, snapName) out, err := zfs.Cmd( ctx, "zfs snapshot %s", snap, ) if err != nil { log.Printf("zfs-snapshot: error: %s", out) return snapName, nil } return snap, err } func (j *BackupJob) FullSend(ctx context.Context, snap string) { out, err := zfs.FullSend( ctx, snap, j.TargetHost, j.Target, ) if err != nil { log.Fatalf("zfs-send: error: %s", out) } } func (j *BackupJob) IncrementalSend(ctx context.Context, prevSnap *zfs.Snapshot, newSnap string) ([]byte, error) { return zfs.IncrementalSend( ctx, prevSnap.SnapshotName, newSnap, j.TargetHost, j.Target, ) } func (j *BackupJob) Retain(ctx context.Context) error { snaps, err := j.listSnapshots(ctx) if err != nil { return fmt.Errorf("retain: failure listing snapshots: %s", err.Error()) } forDeletion := make([]zfs.Snapshot, 0, len(snaps)) now := time.Now() if len(snaps) < j.Keep { return nil } deleteCount := len(snaps) - j.Keep for i, snap := range snaps { age := now.Sub(snap.Creation) if age >= j.MaxAge { forDeletion[i] = snap } if len(forDeletion) == deleteCount { break } } return nil } func (j *BackupJob) Do(ctx context.Context) { currentSnaps, err := j.listSnapshots(ctx) if err != nil { log.Fatal("failed to list existing snapshots") } var prev *zfs.Snapshot if len(currentSnaps) > 0 { prev = ¤tSnaps[len(currentSnaps)-1] } newSnapshot, err := j.Snapshot(ctx) if err != nil { log.Fatalf("error creating new snapshot: %s", err.Error()) } if prev == nil { // Full Send j.FullSend(ctx, newSnapshot) } else { // Inc send j.IncrementalSend(ctx, prev, newSnapshot) } }