package main import ( "fmt" "log" "os" "os/exec" "strings" "time" ) func maybeAddArgString(args []string, name, value string) []string { if value != "" { return append(args, name, value) } return args } func maybeAddArgInt(args []string, name string, value int) []string { if value > 0 { return append(args, name, fmt.Sprint(value)) } return args } func maybeAddArgBool(args []string, name string, value bool) []string { if value { return append(args, name) } return args } func maybeAddArgsList(args []string, name string, value []string) []string { for _, v := range value { args = append(args, name, v) } return args } type CommandOptions interface { ToArgs() []string } type NoOpts struct{} func (NoOpts) ToArgs() []string { return []string{} } type BackupOpts struct { Exclude []string `hcl:"Exclude,optional"` Include []string `hcl:"Include,optional"` Tags []string `hcl:"Tags,optional"` Host string `hcl:"Host,optional"` } func (bo BackupOpts) ToArgs() (args []string) { args = maybeAddArgsList(args, "--exclude", bo.Exclude) args = maybeAddArgsList(args, "--include", bo.Include) args = maybeAddArgsList(args, "--tag", bo.Tags) args = maybeAddArgString(args, "--host", bo.Host) return } type RestoreOpts struct { Exclude []string `hcl:"Exclude,optional"` Include []string `hcl:"Include,optional"` Host []string `hcl:"Host,optional"` Tags []string `hcl:"Tags,optional"` Path string `hcl:"Path,optional"` Target string `hcl:"Target,optional"` Verify bool `hcl:"Verify,optional"` } func (ro RestoreOpts) ToArgs() (args []string) { args = maybeAddArgsList(args, "--exclude", ro.Exclude) args = maybeAddArgsList(args, "--include", ro.Include) args = maybeAddArgsList(args, "--host", ro.Host) args = maybeAddArgsList(args, "--tag", ro.Tags) args = maybeAddArgString(args, "--path", ro.Path) args = maybeAddArgString(args, "--target", ro.Target) args = maybeAddArgBool(args, "--verify", ro.Verify) return } type TagList []string func (t TagList) String() string { return strings.Join(t, ",") } type ForgetOpts struct { KeepLast int `hcl:"KeepLast,optional"` KeepHourly int `hcl:"KeepHourly,optional"` KeepDaily int `hcl:"KeepDaily,optional"` KeepWeekly int `hcl:"KeepWeekly,optional"` KeepMonthly int `hcl:"KeepMonthly,optional"` KeepYearly int `hcl:"KeepYearly,optional"` KeepWithin time.Duration `hcl:"KeepWithin,optional"` KeepWithinHourly time.Duration `hcl:"KeepWithinHourly,optional"` KeepWithinDaily time.Duration `hcl:"KeepWithinDaily,optional"` KeepWithinWeekly time.Duration `hcl:"KeepWithinWeekly,optional"` KeepWithinMonthly time.Duration `hcl:"KeepWithinMonthly,optional"` KeepWithinYearly time.Duration `hcl:"KeepWithinYearly,optional"` Tags []TagList `hcl:"Tags,optional"` KeepTags []TagList `hcl:"KeepTags,optional"` Prune bool `hcl:"Prune,optional"` } func (fo ForgetOpts) ToArgs() (args []string) { args = maybeAddArgInt(args, "--keep-last", fo.KeepLast) args = maybeAddArgInt(args, "--keep-hourly", fo.KeepHourly) args = maybeAddArgInt(args, "--keep-daily", fo.KeepDaily) args = maybeAddArgInt(args, "--keep-weekly", fo.KeepWeekly) args = maybeAddArgInt(args, "--keep-monthly", fo.KeepMonthly) args = maybeAddArgInt(args, "--keep-yearly", fo.KeepYearly) // Add keep-within-* if fo.KeepWithin > 0 { args = append(args, "--keep-within", fo.KeepWithin.String()) } if fo.KeepWithinHourly > 0 { args = append(args, "--keep-within-hourly", fo.KeepWithinHourly.String()) } if fo.KeepWithinDaily > 0 { args = append(args, "--keep-within-daily", fo.KeepWithinDaily.String()) } if fo.KeepWithinWeekly > 0 { args = append(args, "--keep-within-weekly", fo.KeepWithinWeekly.String()) } if fo.KeepWithinMonthly > 0 { args = append(args, "--keep-within-monthly", fo.KeepWithinMonthly.String()) } if fo.KeepWithinYearly > 0 { args = append(args, "--keep-within-yearly", fo.KeepWithinYearly.String()) } // Add tags for _, tagList := range fo.Tags { args = append(args, "--tag", tagList.String()) } for _, tagList := range fo.KeepTags { args = append(args, "--keep-tag", tagList.String()) } // Add prune options args = maybeAddArgBool(args, "--prune", fo.Prune) return args } type ResticGlobalOpts struct { CaCertFile string `hcl:"CaCertFile,optional"` CacheDir string `hcl:"CacheDir,optional"` PasswordFile string `hcl:"PasswordFile,optional"` TLSClientCertFile string `hcl:"TlsClientCertFile,optional"` LimitDownload int `hcl:"LimitDownload,optional"` LimitUpload int `hcl:"LimitUpload,optional"` VerboseLevel int `hcl:"VerboseLevel,optional"` CleanupCache bool `hcl:"CleanupCache,optional"` NoCache bool `hcl:"NoCache,optional"` NoLock bool `hcl:"NoLock,optional"` } func (glo ResticGlobalOpts) ToArgs() (args []string) { args = maybeAddArgString(args, "--cacert", glo.CaCertFile) args = maybeAddArgString(args, "--cache-dir", glo.CacheDir) args = maybeAddArgString(args, "--password-file", glo.PasswordFile) args = maybeAddArgString(args, "--tls-client-cert", glo.TLSClientCertFile) args = maybeAddArgInt(args, "--limit-download", glo.LimitDownload) args = maybeAddArgInt(args, "--limit-upload", glo.LimitUpload) args = maybeAddArgInt(args, "--verbose", glo.VerboseLevel) args = maybeAddArgBool(args, "--cleanup-cache", glo.CleanupCache) args = maybeAddArgBool(args, "--no-cache", glo.NoCache) args = maybeAddArgBool(args, "--no-lock", glo.NoLock) return args } type Restic struct { Logger *log.Logger Repo string Env map[string]string Passphrase string GlobalOpts *ResticGlobalOpts Cwd string } func (rcmd Restic) BuildEnv() []string { if rcmd.Env == nil { rcmd.Env = map[string]string{} } if rcmd.Passphrase != "" { rcmd.Env["RESTIC_PASSWORD"] = rcmd.Passphrase } envList := os.Environ() for name, value := range rcmd.Env { envList = append(envList, fmt.Sprintf("%s=%s", name, value)) } return envList } func (rcmd Restic) RunRestic(command string, options CommandOptions, commandArgs ...string) error { args := []string{} if rcmd.GlobalOpts != nil { args = rcmd.GlobalOpts.ToArgs() } args = append(args, "--repo", rcmd.Repo, command) args = append(args, options.ToArgs()...) args = append(args, commandArgs...) cmd := exec.Command("restic", args...) cmd.Stdout = NewLogWriter(rcmd.Logger) cmd.Stderr = cmd.Stdout cmd.Env = rcmd.BuildEnv() cmd.Dir = rcmd.Cwd if err := cmd.Run(); err != nil { return fmt.Errorf("error running restic %s: %w", command, err) } return nil } func (rcmd Restic) Backup(files []string, opts BackupOpts) error { return rcmd.RunRestic("backup", opts, files...) } func (rcmd Restic) Restore(snapshot string, opts RestoreOpts) error { return rcmd.RunRestic("restore", opts, snapshot) } func (rcmd Restic) Forget(forgetOpts ForgetOpts) error { return rcmd.RunRestic("forget", forgetOpts) } func (rcmd Restic) Check() error { return rcmd.RunRestic("check", NoOpts{}) } func (rcmd Restic) EnsureInit() error { if err := rcmd.RunRestic("snapshots", NoOpts{}); err != nil { return rcmd.RunRestic("init", NoOpts{}) } return nil }