From fa4e8bcb76b6afa4c61406a480466f2f97b5fe9b Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 27 Apr 2021 11:22:18 +1000 Subject: [PATCH] Backup & Restore files - copy volumes and included bind files to folder, restore files from same folder. --- backup.go | 78 +++++++++++++++++++++++++++++++++++++++++++----------- go.mod | 1 + restore.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 16 deletions(-) diff --git a/backup.go b/backup.go index b059250..3b6cc5f 100644 --- a/backup.go +++ b/backup.go @@ -18,6 +18,7 @@ import ( "github.com/docker/go-connections/nat" "github.com/kennygrant/sanitize" "github.com/spf13/cobra" + "github.com/zloylos/grsync" ) // Backup is used to gather all of a container's metadata, so we can encode it @@ -31,6 +32,7 @@ type Backup struct { var ( optLaunch = "" optTar = false + optOut = false optAll = false optStopped = false @@ -178,29 +180,72 @@ func backup(ID string) error { return err } - for _, m := range conf.Mounts { - // fmt.Printf("Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination) - err := filepath.Walk(m.Source, collectFile) + var backupFiles = conf.Mounts + + if optOut { + outDir := filename + ".backup" + for _, m := range backupFiles { + source := m.Source + dest := outDir + source + if info, err := os.Stat(source); err == nil && info.IsDir() { + source += "/" + dest += "/" + } + fmt.Println("Backing up '" + source + "' -> './" + dest + "'") + task := grsync.NewTask( + source, + dest, + grsync.RsyncOptions{}, + ) + + go func() { + for { + state := task.State() + fmt.Printf( + "\rprogress: %.2f / rem. %d / tot. %d", + state.Progress, + state.Remain, + state.Total, + ) + time.Sleep(time.Second) + } + }() + + err = task.Run() + if err != nil { + fmt.Println(task.Log().Stderr) + return err + } + fmt.Printf("\r") + } + + } else { + + paths = []string{} + for _, m := range backupFiles { + // fmt.Printf("Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination) + err := filepath.Walk(m.Source, collectFile) + if err != nil { + return err + } + } + + filelist, err := os.Create(filename + ".backup.files") if err != nil { return err } - } - - filelist, err := os.Create(filename + ".backup.files") - if err != nil { - return err - } - defer filelist.Close() + defer filelist.Close() - _, err = filelist.WriteString(filename + ".backup.json\n") - if err != nil { - return err - } - for _, s := range paths { - _, err := filelist.WriteString(s + "\n") + _, err = filelist.WriteString(filename + ".backup.json\n") if err != nil { return err } + for _, s := range paths { + _, err := filelist.WriteString(s + "\n") + if err != nil { + return err + } + } } fmt.Println("Created backup:", filename+".backup.json") @@ -241,6 +286,7 @@ func backupAll() error { func init() { backupCmd.Flags().StringVarP(&optLaunch, "launch", "l", "", "launch external program with file-list as argument") backupCmd.Flags().BoolVarP(&optTar, "tar", "t", false, "create tar backups") + backupCmd.Flags().BoolVarP(&optOut, "outdir", "o", false, "copy all files to folder") backupCmd.Flags().BoolVarP(&optAll, "all", "a", false, "backup all running containers") backupCmd.Flags().BoolVarP(&optStopped, "stopped", "s", false, "in combination with --all: also backup stopped containers") RootCmd.AddCommand(backupCmd) diff --git a/go.mod b/go.mod index 192c840..be01d8c 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect golang.org/x/net v0.0.0-20190502183928-7f726cade0ab // indirect + github.com/zloylos/grsync v1.3.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/grpc v1.20.1 // indirect gotest.tools v2.2.0+incompatible // indirect diff --git a/restore.go b/restore.go index b32aece..ec047d9 100644 --- a/restore.go +++ b/restore.go @@ -8,10 +8,12 @@ import ( "io/ioutil" "os" "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/spf13/cobra" + "github.com/zloylos/grsync" ) var ( @@ -158,6 +160,11 @@ func restore(filename string) error { return err } + err = restoreFiles(filename, id, backup) + if err != nil { + return err + } + if optStart { return startContainer(id) } @@ -196,6 +203,74 @@ func createContainer(backup Backup) (string, error) { return resp.ID, nil } +func restoreFiles(filename string, id string, backup Backup) error { + conf, err := cli.ContainerInspect(ctx, id) + if err != nil { + return err + } + + tt := map[string]string{} + for _, oldPath := range backup.Mounts { + for _, hostPath := range conf.Mounts { + if oldPath.Destination == hostPath.Destination { + tt[oldPath.Source] = hostPath.Source + break + } + } + } + + outDir := strings.TrimRight(filename, ".json") + for oldPath, newPath := range tt { + skip := false + for _, exclude := range optExclude { + if strings.HasPrefix(oldPath, exclude) { + skip = true + break + } + } + + if skip { + fmt.Println("Skipping './" + backupFiles + "'") + continue + } + + backupFiles := outDir + oldPath + fmt.Println("Restoring './" + backupFiles + "' -> '" + newPath + "'") + + if info, err := os.Stat(backupFiles); err == nil && info.IsDir() { + backupFiles += "/" + newPath += "/" + } + + task := grsync.NewTask( + backupFiles, + newPath, + grsync.RsyncOptions{}, + ) + + go func() { + for { + state := task.State() + fmt.Printf( + "\rprogress: %.2f / rem. %d / tot. %d", + state.Progress, + state.Remain, + state.Total, + ) + time.Sleep(time.Second) + } + }() + + err = task.Run() + if err != nil { + return err + } + fmt.Printf("\r") + } + + return nil +} + func startContainer(id string) error { fmt.Println("Starting container:", id[:12]) @@ -226,5 +301,6 @@ func startContainer(id string) error { func init() { restoreCmd.Flags().BoolVarP(&optStart, "start", "s", false, "start restored container") + restoreCmd.Flags().StringArrayVarP(&optExclude, "exclude", "e", []string{}, "skip restoring files that start with this, can use multiple times") RootCmd.AddCommand(restoreCmd) }