2020-04-19 06:26:23 +00:00
|
|
|
package backup
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/tar"
|
|
|
|
"context"
|
2020-12-16 05:56:53 +00:00
|
|
|
"emperror.dev/errors"
|
2020-06-13 17:40:26 +00:00
|
|
|
"github.com/apex/log"
|
2020-12-08 16:13:48 +00:00
|
|
|
"github.com/juju/ratelimit"
|
2020-04-19 06:26:23 +00:00
|
|
|
gzip "github.com/klauspost/pgzip"
|
2020-12-08 16:13:48 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
2020-04-19 06:26:23 +00:00
|
|
|
"github.com/remeh/sizedwaitgroup"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"io"
|
|
|
|
"os"
|
2020-08-24 00:46:35 +00:00
|
|
|
"runtime"
|
2020-04-19 06:26:23 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Archive struct {
|
|
|
|
sync.Mutex
|
|
|
|
|
|
|
|
TrimPrefix string
|
|
|
|
Files *IncludedFiles
|
|
|
|
}
|
|
|
|
|
2020-07-15 19:11:12 +00:00
|
|
|
// Creates an archive at dst with all of the files defined in the included files struct.
|
2020-09-27 18:16:38 +00:00
|
|
|
func (a *Archive) Create(dst string, ctx context.Context) error {
|
2020-07-15 19:11:12 +00:00
|
|
|
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
2020-04-19 06:26:23 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2020-12-08 16:13:48 +00:00
|
|
|
// Select a writer based off of the WriteLimit configuration option.
|
|
|
|
var writer io.Writer
|
|
|
|
if writeLimit := config.Get().System.Backups.WriteLimit; writeLimit < 1 {
|
|
|
|
// If there is no write limit, use the file as the writer.
|
|
|
|
writer = f
|
|
|
|
} else {
|
|
|
|
// Token bucket with a capacity of "writeLimit" MiB, adding "writeLimit" MiB/s
|
|
|
|
bucket := ratelimit.NewBucketWithRate(float64(writeLimit)*1024*1024, int64(writeLimit)*1024*1024)
|
|
|
|
|
|
|
|
// Wrap the file writer with the token bucket limiter.
|
|
|
|
writer = ratelimit.Writer(f, bucket)
|
|
|
|
}
|
|
|
|
|
2020-08-24 00:46:35 +00:00
|
|
|
maxCpu := runtime.NumCPU() / 2
|
|
|
|
if maxCpu > 4 {
|
|
|
|
maxCpu = 4
|
|
|
|
}
|
|
|
|
|
2020-12-08 16:13:48 +00:00
|
|
|
gzw, err := gzip.NewWriterLevel(writer, gzip.BestSpeed)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithMessage(err, "failed to create gzip writer")
|
|
|
|
}
|
|
|
|
if err := gzw.SetConcurrency(1<<20, maxCpu); err != nil {
|
|
|
|
return errors.WithMessage(err, "failed to set gzip concurrency")
|
|
|
|
}
|
2020-08-24 00:46:35 +00:00
|
|
|
|
2020-08-24 00:18:40 +00:00
|
|
|
defer gzw.Flush()
|
2020-04-19 06:26:23 +00:00
|
|
|
defer gzw.Close()
|
|
|
|
|
|
|
|
tw := tar.NewWriter(gzw)
|
2020-08-24 00:18:40 +00:00
|
|
|
defer tw.Flush()
|
2020-04-19 06:26:23 +00:00
|
|
|
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.
|
2020-08-25 03:45:54 +00:00
|
|
|
for _, p := range a.Files.All() {
|
|
|
|
p := p
|
2020-04-19 06:26:23 +00:00
|
|
|
g.Go(func() error {
|
|
|
|
wg.Add()
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2020-11-28 23:57:10 +00:00
|
|
|
return ctx.Err()
|
2020-04-19 06:26:23 +00:00
|
|
|
default:
|
2020-08-25 03:45:54 +00:00
|
|
|
return a.addToArchive(p, tw)
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-07-15 19:11:12 +00:00
|
|
|
if rerr := os.Remove(dst); rerr != nil && !os.IsNotExist(rerr) {
|
|
|
|
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
|
2020-09-27 18:16:38 +00:00
|
|
|
return nil
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Adds a single file to the existing tar archive writer.
|
2020-08-25 03:45:54 +00:00
|
|
|
func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
2020-04-19 06:26:23 +00:00
|
|
|
f, err := os.Open(p)
|
|
|
|
if err != nil {
|
2020-08-28 02:57:35 +00:00
|
|
|
// If you try to backup something that no longer exists (got deleted somewhere during the process
|
|
|
|
// but not by this process), just skip over it and don't kill the entire backup.
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2020-08-25 03:45:54 +00:00
|
|
|
s, err := f.Stat()
|
|
|
|
if err != nil {
|
2020-08-28 02:57:35 +00:00
|
|
|
// Same as above, don't kill the process just because the file no longer exists.
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-08-25 03:45:54 +00:00
|
|
|
}
|
|
|
|
|
2020-11-28 23:57:10 +00:00
|
|
|
name := strings.TrimPrefix(p, a.TrimPrefix)
|
|
|
|
header, err := tar.FileInfoHeader(s, name)
|
2020-11-18 23:30:34 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "failed to get tar#FileInfoHeader for "+name)
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
2020-11-28 23:57:10 +00:00
|
|
|
header.Name = name
|
2020-04-19 06:26:23 +00:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
2020-07-15 19:11:12 +00:00
|
|
|
if err := w.WriteHeader(header); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
|
2020-08-24 00:18:40 +00:00
|
|
|
buf := make([]byte, 4*1024)
|
2020-11-19 01:44:25 +00:00
|
|
|
if _, err := io.CopyBuffer(w, io.LimitReader(f, header.Size), buf); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "failed to copy "+header.Name+" to archive")
|
2020-04-19 06:26:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|