package backup import ( "archive/tar" "context" "github.com/apex/log" gzip "github.com/klauspost/pgzip" "github.com/remeh/sizedwaitgroup" "golang.org/x/sync/errgroup" "io" "os" "strings" "sync" ) type Archive struct { sync.Mutex TrimPrefix string Files *IncludedFiles } // Creates an archive at dst with all of the files defined in the included files struct. func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) { f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return nil, err } defer f.Close() gzw := gzip.NewWriter(f) defer gzw.Close() tw := tar.NewWriter(gzw) defer tw.Close() wg := sizedwaitgroup.New(10) g, ctx := errgroup.WithContext(ctx) // Iterate over all of the files to be included and put them into the archive. This is // done as a concurrent goroutine to speed things along. If an error is encountered at // any step, the entire process is aborted. for p, s := range a.Files.All() { if (*s).IsDir() { continue } pa := p st := s g.Go(func() error { wg.Add() defer wg.Done() select { case <-ctx.Done(): return ctx.Err() default: return a.addToArchive(pa, st, tw) } }) } // Block until the entire routine is completed. if err := g.Wait(); err != nil { f.Close() // Attempt to remove the archive if there is an error, report that error to // the logger if it fails. if rerr := os.Remove(dst); rerr != nil && !os.IsNotExist(rerr) { log.WithField("location", dst).Warn("failed to delete corrupted backup archive") } return nil, err } st, err := f.Stat() if err != nil { return nil, err } return st, nil } // Adds a single file to the existing tar archive writer. func (a *Archive) addToArchive(p string, s *os.FileInfo, w *tar.Writer) error { f, err := os.Open(p) if err != nil { return err } defer f.Close() st := *s header := &tar.Header{ // Trim the long server path from the name of the file so that the resulting // archive is exactly how the user would see it in the panel file manager. Name: strings.TrimPrefix(p, a.TrimPrefix), Size: st.Size(), Mode: int64(st.Mode()), ModTime: st.ModTime(), } // These actions must occur sequentially, even if this function is called multiple // in parallel. You'll get some nasty panic's otherwise. a.Lock() defer a.Unlock() if err := w.WriteHeader(header); err != nil { return err } if _, err := io.Copy(w, f); err != nil { return err } return nil }