forked from toolshed/abra
		
	fix: backups get gzip, absolute paths, single archive file
This commit is contained in:
		| @ -1,9 +1,11 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @ -19,6 +21,9 @@ import ( | ||||
| 	"github.com/docker/cli/cli/command" | ||||
| 	"github.com/docker/docker/api/types" | ||||
| 	"github.com/docker/docker/api/types/filters" | ||||
| 	"github.com/docker/docker/pkg/archive" | ||||
| 	"github.com/docker/docker/pkg/system" | ||||
| 	"github.com/klauspost/pgzip" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli" | ||||
| ) | ||||
| @ -150,34 +155,56 @@ func runBackup(app config.App, serviceName string, bkConfig backupConfig) error | ||||
| 		} | ||||
|  | ||||
| 		if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil { | ||||
| 			return err | ||||
| 			return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd) | ||||
| 	} | ||||
|  | ||||
| 	var tempBackupPaths []string | ||||
| 	for _, remoteBackupPath := range bkConfig.backupPaths { | ||||
| 		timestamp := strconv.Itoa(time.Now().Nanosecond()) | ||||
| 		sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_") | ||||
| 		localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, timestamp)) | ||||
| 		logrus.Debugf("backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath) | ||||
| 		logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath) | ||||
|  | ||||
| 		logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath) | ||||
|  | ||||
| 		content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error()) | ||||
| 			if err := cleanupTempArchives(tempBackupPaths); err != nil { | ||||
| 				return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) | ||||
| 			} | ||||
| 			return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error()) | ||||
| 		} | ||||
| 		defer content.Close() | ||||
|  | ||||
| 		body, err := ioutil.ReadAll(content) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		_, srcBase := archive.SplitPathDirEntry(remoteBackupPath) | ||||
| 		preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath) | ||||
| 		if err := copyToFile(localBackupPath, preArchive); err != nil { | ||||
| 			logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error()) | ||||
| 			if err := cleanupTempArchives(tempBackupPaths); err != nil { | ||||
| 				return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) | ||||
| 			} | ||||
| 			return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		if err := ioutil.WriteFile(localBackupPath, body, 0644); err != nil { | ||||
| 			return err | ||||
| 		tempBackupPaths = append(tempBackupPaths, localBackupPath) | ||||
| 	} | ||||
|  | ||||
| 		logrus.Infof("backed up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath) | ||||
| 	logrus.Infof("compressing and merging archives...") | ||||
|  | ||||
| 	if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil { | ||||
| 		logrus.Debugf("failed to merge archive files: %s", err.Error()) | ||||
| 		if err := cleanupTempArchives(tempBackupPaths); err != nil { | ||||
| 			return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) | ||||
| 		} | ||||
| 		return fmt.Errorf("failed to merge archive files: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if err := cleanupTempArchives(tempBackupPaths); err != nil { | ||||
| 		return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if bkConfig.postHookCmd != "" { | ||||
| @ -203,3 +230,141 @@ func runBackup(app config.App, serviceName string, bkConfig backupConfig) error | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func copyToFile(outfile string, r io.Reader) error { | ||||
| 	tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".tar_temp") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	tmpPath := tmpFile.Name() | ||||
|  | ||||
| 	_, err = io.Copy(tmpFile, r) | ||||
| 	tmpFile.Close() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		os.Remove(tmpPath) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err = os.Rename(tmpPath, outfile); err != nil { | ||||
| 		os.Remove(tmpPath) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func cleanupTempArchives(tarPaths []string) error { | ||||
| 	for _, tarPath := range tarPaths { | ||||
| 		if err := os.RemoveAll(tarPath); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("remove temporary archive file %s", tarPath) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func mergeArchives(tarPaths []string, serviceName string) error { | ||||
| 	var out io.Writer | ||||
| 	var cout *pgzip.Writer | ||||
|  | ||||
| 	timestamp := strconv.Itoa(time.Now().Nanosecond()) | ||||
| 	localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, timestamp)) | ||||
|  | ||||
| 	fout, err := os.Create(localBackupPath) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Failed to open %s: %s", localBackupPath, err) | ||||
| 	} | ||||
|  | ||||
| 	defer fout.Close() | ||||
| 	out = fout | ||||
|  | ||||
| 	cout = pgzip.NewWriter(out) | ||||
| 	out = cout | ||||
|  | ||||
| 	tw := tar.NewWriter(out) | ||||
|  | ||||
| 	for _, tarPath := range tarPaths { | ||||
| 		if err := addTar(tw, tarPath); err != nil { | ||||
| 			return fmt.Errorf("failed to merge %s: %v", tarPath, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := tw.Close(); err != nil { | ||||
| 		return fmt.Errorf("failed to close tar writer %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if cout != nil { | ||||
| 		if err := cout.Flush(); err != nil { | ||||
| 			return fmt.Errorf("failed to flush: %s", err) | ||||
| 		} else if err = cout.Close(); err != nil { | ||||
| 			return fmt.Errorf("failed to close compressed writer: %s", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	logrus.Infof("backed up %s to %s", serviceName, localBackupPath) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func addTar(tw *tar.Writer, pth string) (err error) { | ||||
| 	var tr *tar.Reader | ||||
| 	var rc io.ReadCloser | ||||
| 	var hdr *tar.Header | ||||
|  | ||||
| 	if tr, rc, err = openTarFile(pth); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		if hdr, err = tr.Next(); err != nil { | ||||
| 			if err == io.EOF { | ||||
| 				err = nil | ||||
| 			} | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tw.WriteHeader(hdr); err != nil { | ||||
| 			break | ||||
| 		} else if _, err = io.Copy(tw, tr); err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if err == nil { | ||||
| 		err = rc.Close() | ||||
| 	} else { | ||||
| 		rc.Close() | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) { | ||||
| 	var fin *os.File | ||||
| 	var n int | ||||
| 	buff := make([]byte, 1024) | ||||
|  | ||||
| 	if fin, err = os.Open(pth); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if n, err = fin.Read(buff); err != nil { | ||||
| 		fin.Close() | ||||
| 		return | ||||
| 	} else if n == 0 { | ||||
| 		fin.Close() | ||||
| 		err = fmt.Errorf("%s is empty", pth) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _, err = fin.Seek(0, 0); err != nil { | ||||
| 		fin.Close() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	rc = fin | ||||
| 	tr = tar.NewReader(rc) | ||||
|  | ||||
| 	return tr, rc, nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -38,6 +38,7 @@ require ( | ||||
| 	github.com/gorilla/mux v1.8.0 // indirect | ||||
| 	github.com/hashicorp/go-retryablehttp v0.7.1 | ||||
| 	github.com/kevinburke/ssh_config v1.2.0 | ||||
| 	github.com/klauspost/pgzip v1.2.5 | ||||
| 	github.com/libdns/gandi v1.0.2 | ||||
| 	github.com/libdns/libdns v0.2.1 | ||||
| 	github.com/moby/sys/mount v0.2.0 // indirect | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -656,7 +656,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI | ||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||
| github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
| github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
| github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw= | ||||
| github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | ||||
| github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= | ||||
| github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||
|  | ||||
		Reference in New Issue
	
	Block a user