package main import ( "archive/tar" "compress/gzip" "context" "fmt" "io" "log/slog" "os" "path" "path/filepath" "time" "github.com/bugsnag/bugsnag-go/v2" "github.com/minio/minio-go/v7" ) func (s *service) backupTangledKnot(ctx context.Context) { err := s.backupKnotDB(ctx) if err != nil { bugsnag.Notify(fmt.Errorf("backup knot db: %w", err)) slog.Error("failed to backup knot db", "error", err) } err = s.backupKnotRepos(ctx) if err != nil { bugsnag.Notify(fmt.Errorf("backup knot db: %w", err)) slog.Error("failed to backup knot db", "error", err) } slog.Info("finished tangled knot backup") } func (s *service) backupKnotDB(ctx context.Context) error { dir := os.Getenv("TANGLED_KNOT_DATABASE_DIRECTORY") if dir == "" { slog.Info("TANGLED_KNOT_DATABASE_DIRECTORY env not set - skipping knot DB backup") } filename := path.Join(s.blobDir, fmt.Sprintf("%d-knot.zip", time.Now().UnixMilli())) f, err := os.Create(filename) if err != nil { return fmt.Errorf("creating temp file: %w", err) } defer func() { f.Close() err = os.Remove(filename) if err != nil { slog.Error("failed to delete knot db zip file after uploading", "error", err, "filename", f.Name()) metadata := bugsnag.MetaData{ "file": { "filename": f.Name(), }, } bugsnag.Notify(fmt.Errorf("delete knot db zip file after uploading: %w", err), metadata) } }() compress(dir, f) // reset the reader back to the start so that the minio upload can read the data that's been written. _, err = f.Seek(0, 0) if err != nil { return fmt.Errorf("setting seek on written file: %w", err) } fi, err := f.Stat() if err != nil { return fmt.Errorf("stat written file: %w", err) } _, err = s.minioClient.PutObject(ctx, s.bucketName, "knot-db.zip", f, fi.Size(), minio.PutObjectOptions{}) if err != nil { return fmt.Errorf("put knot db zip file to bucket: %w", err) } return nil } func (s *service) backupKnotRepos(ctx context.Context) error { dir := os.Getenv("TANGLED_KNOT_REPOSITORY_DIRECTORY") if dir == "" { slog.Info("TANGLED_KNOT_REPOSITORY_DIRECTORY env not set - skipping knot repo backup") } filename := path.Join(s.blobDir, fmt.Sprintf("%d-knot-repos.zip", time.Now().UnixMilli())) f, err := os.Create(filename) if err != nil { return fmt.Errorf("creating temp file: %w", err) } defer func() { f.Close() err = os.Remove(filename) if err != nil { slog.Error("failed to delete knot repos zip file after uploading", "error", err, "filename", f.Name()) metadata := bugsnag.MetaData{ "file": { "filename": f.Name(), }, } bugsnag.Notify(fmt.Errorf("delete knot repos zip file after uploading: %w", err), metadata) } }() compress(dir, f) // reset the reader back to the start so that the minio upload can read the data that's been written. _, err = f.Seek(0, 0) if err != nil { return fmt.Errorf("setting seek on written file: %w", err) } fi, err := f.Stat() if err != nil { return fmt.Errorf("stat written file: %w", err) } _, err = s.minioClient.PutObject(ctx, s.bucketName, "knot-repos.zip", f, fi.Size(), minio.PutObjectOptions{}) if err != nil { return fmt.Errorf("put knot repo file to bucket: %w", err) } return nil } func compress(src string, writer io.Writer) error { zipWriter := gzip.NewWriter(writer) tarWriter := tar.NewWriter(zipWriter) filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { header, err := tar.FileInfoHeader(fi, file) if err != nil { return err } // must provide real name // (see https://golang.org/src/archive/tar/common.go?#L626) header.Name = filepath.ToSlash(file) if err := tarWriter.WriteHeader(header); err != nil { return err } // if not a dir, write file content if !fi.IsDir() { data, err := os.Open(file) if err != nil { return err } if _, err := io.Copy(tarWriter, data); err != nil { return err } } return nil }) // produce tar if err := tarWriter.Close(); err != nil { return err } // produce gzip if err := zipWriter.Close(); err != nil { return err } return nil }