package main import ( "bytes" "fmt" "io" "io/ioutil" "local/args" "local/storage" "log" "os" "os/exec" "path" "path/filepath" "sort" "time" ) type Config struct { n int conf string rclone string ns string root string store string cmd string } type LastN struct { store storage.DB conf Config } func main() { conf := config() log.Println(conf) storage, err := storage.New(storage.TypeFromString(conf.store), conf.conf, path.Join(conf.rclone+":", conf.ns)) if err != nil { panic(err) } lastn := &LastN{ conf: conf, store: storage, } actions := []func() error{} switch conf.cmd { case "backup": actions = append(actions, lastn.push, lastn.clean) case "list": actions = append(actions, lastn.list) case "clean": actions = append(actions, lastn.clean) case "restore": actions = append(actions, lastn.restore) default: panic(fmt.Sprintf("not impl: %s")) } for _, action := range actions { if err := action(); err != nil { panic(err) } } } func config() Config { as := args.NewArgSet() as.Append(args.INT, "n", "number of backups to retain", 5) as.Append(args.STRING, "conf", "path to rclone conf", path.Join(os.Getenv("HOME"), "/.config/rclone/rclone.conf")) as.Append(args.STRING, "root", "path to root", "./public") as.Append(args.STRING, "ns", "ns for backups", path.Join("lastn", "dev")) as.Append(args.STRING, "rclone", "rclone backend name", "blapointe-drive-enc") as.Append(args.STRING, "store", "type of store, like [map rclone]", "map") as.Append(args.STRING, "cmd", "[backup, restore, list, clean]", "backup") if err := as.Parse(); err != nil { panic(err) } root, err := filepath.Abs(as.Get("root").GetString()) if err != nil { panic(err) } return Config{ n: as.Get("n").GetInt(), conf: as.Get("conf").GetString(), rclone: as.Get("rclone").GetString(), root: root, ns: as.Get("ns").GetString(), store: as.Get("store").GetString(), cmd: as.Get("cmd").GetString(), } } func (lastN *LastN) push() error { root := lastN.conf.root store := lastN.store root, err := filepath.Abs(root) if err != nil { return err } archive := path.Join( os.TempDir(), fmt.Sprintf( "%s.tar", time.Now().Format("2006.01.02.15.04.05"), ), ) cmd := exec.Command( "tar", "-czf", archive, "-C", path.Dir(root), path.Base(root), ) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("%v: %s", err, out) } b, err := ioutil.ReadFile(archive) if err != nil { return err } log.Println("Created backup", path.Base(archive)) return store.Set(path.Base(archive), b) } func (lastN *LastN) clean() error { n := lastN.conf.n store := lastN.store backups, err := store.List(nil) if err != nil { return err } sort.Strings(backups) for i := 0; i < len(backups)-n; i++ { log.Println("Pruning old backup", backups[i]) err := store.Set(backups[i], nil) if err != nil { return err } } return nil } func (lastN *LastN) list() error { store := lastN.store backups, err := store.List(nil) if err != nil { return err } sort.Strings(backups) for _, backup := range backups { log.Println(backup) } return nil } func (lastN *LastN) restore() error { root := lastN.conf.root + "-restore" os.RemoveAll(root) if err := os.MkdirAll(root, os.ModePerm); err != nil { return err } store := lastN.store backups, err := store.List(nil) if err != nil { return fmt.Errorf("cannot list: %v", err) } sort.Strings(backups) backup := backups[len(backups)-1] b, err := store.Get(backup) if err != nil { return fmt.Errorf("cannot get %s: %v", backup, err) } log.Printf("restoring %s (%v) in %s", backup, len(b), root) cmd := exec.Command( "tar", "-C", root, "-xzf", "-", ) stdin, err := cmd.StdinPipe() if err != nil { return fmt.Errorf("cannot get stdin: %v", err) } go func() { defer stdin.Close() io.Copy(stdin, bytes.NewReader(b)) }() out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed tar -xf: %v: %s", err, out) } return nil }