package restic import ( "encoding/json" "fmt" "github.com/rs/zerolog/log" "os/exec" "time" ) type ResticSnapshot struct { Time time.Time `json:"time"` // format 2023-03-01T16:15:34.111513+01:00 Tree string `json:"tree"` Paths []string `json:"paths"` Id string `json:"short_id"` } func (rs ResticSnapshot) ShortTime() string { return rs.Time.Format(ShortTimeFormat) } type Wrapper struct { LatestSnapshots []ResticSnapshot mountCommand *exec.Cmd } func (w *Wrapper) HasSnapshots() bool { return len(w.LatestSnapshots) > 0 } func (w *Wrapper) LastSnapshot() ResticSnapshot { if !w.HasSnapshots() { return ResticSnapshot{ Id: "(no snapshots yet)", Time: time.Time{}, } } return w.LatestSnapshots[len(w.LatestSnapshots)-1] } func resticCmd(args ...string) *exec.Cmd { cmd := exec.Command("restic", args...) log.Debug().Msg(cmd.String()) return cmd } /* Expected JSON format: [ { "time": "2023-03-01T16:15:34.111513+01:00", "tree": "d603aa4c6ce2bdef784dbdcd36461970d7f0cc8083d31f46d23cdb9bef172f0a", "paths": [ "/Users/wgroeneveld" ], "hostname": "Wouters-M1-Air.local", "username": "wgroeneveld", "uid": 501, "gid": 20, "id": "31ae2a213c5750c4f86ebe8a8e989a5d4de2963c911e7513f47ca227723a0d95", "short_id": "31ae2a21" } ] */ func (w *Wrapper) UpdateLatestSnapshots(c *Config) error { cmd := resticCmd("--json", "--password-file", c.PasswordFile(), "-r", c.Repository, "snapshots") out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("restic snapshots cmd: %s: %w", string(out), err) } err = json.Unmarshal(out, &w.LatestSnapshots) if err != nil { return err } log.Debug().Any("snapshots", w.LatestSnapshots).Msg("update") return nil } func (w *Wrapper) MountBackups(c *Config) error { c.CreateMountDirIfDoesntExist() if w.mountCommand != nil && w.mountCommand.Process != nil { // could be killed or terminted due to manual unmount, ignore errors and retry anyway w.mountCommand.Process.Kill() } // Open the folder first: MacFuse could take a second; results in occasional weird errors otherwise. openFolder(c.MountDir()) w.mountCommand = resticCmd("--password-file", c.PasswordFile(), "-r", c.Repository, "mount", c.MountDir()) err := w.mountCommand.Start() // restic's mount is a blocking call if err != nil { return fmt.Errorf("restic mount cmd: %w", err) } return nil } func openFolder(folder string) error { cmd := exec.Command("open", folder) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("open mount dir: %s: %w", string(out), err) } return nil } func (w *Wrapper) Backup(c *Config) error { cmd := resticCmd("--json", "--password-file", c.PasswordFile(), "-r", c.Repository, "--exclude-file", c.ExcludeFile(), "backup", c.Backup) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("restic backup cmd: %s: %w", string(out), err) } log.Debug().Str("out", string(out)).Msg("backup") return nil }