Fresh repository history for elektrine/magpie hosted at https://git.elektrine.com/elektrine/magpie.
181 lines
4.3 KiB
Go
181 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func BackupDir(src string, dst string) error {
|
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
gz := gzip.NewWriter(out)
|
|
defer gz.Close()
|
|
tw := tar.NewWriter(gz)
|
|
defer tw.Close()
|
|
root, err := filepath.Abs(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() {
|
|
return err
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 {
|
|
return fmt.Errorf("refusing to back up symlink %q", path)
|
|
}
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := tar.FileInfoHeader(info, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = filepath.ToSlash(rel)
|
|
if err := tw.WriteHeader(header); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileInfo, statErr := file.Stat()
|
|
currentInfo, lstatErr := os.Lstat(path)
|
|
if statErr != nil || lstatErr != nil {
|
|
_ = file.Close()
|
|
if statErr != nil {
|
|
return statErr
|
|
}
|
|
return lstatErr
|
|
}
|
|
if currentInfo.Mode()&os.ModeSymlink != 0 || !os.SameFile(currentInfo, fileInfo) {
|
|
_ = file.Close()
|
|
return fmt.Errorf("refusing to back up replaced or symlinked file %q", path)
|
|
}
|
|
_, copyErr := io.Copy(tw, file)
|
|
closeErr := file.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
return closeErr
|
|
})
|
|
}
|
|
|
|
func RestoreDir(src string, dst string) error {
|
|
return RestoreDirWithLimit(src, dst, defaultMaxRestoreBytes)
|
|
}
|
|
|
|
func RestoreDirWithLimit(src string, dst string, maxBytes int64) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
gz, err := gzip.NewReader(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gz.Close()
|
|
tr := tar.NewReader(gz)
|
|
root, err := filepath.Abs(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
return err
|
|
}
|
|
var restoredBytes int64
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(header.Name, "..") || strings.HasPrefix(header.Name, "/") {
|
|
return fmt.Errorf("unsafe archive path %q", header.Name)
|
|
}
|
|
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA && header.Typeflag != tar.TypeDir {
|
|
return fmt.Errorf("unsupported archive entry %q", header.Name)
|
|
}
|
|
if header.Size < 0 {
|
|
return fmt.Errorf("invalid archive size for %q", header.Name)
|
|
}
|
|
if header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeRegA {
|
|
restoredBytes += header.Size
|
|
if maxBytes > 0 && restoredBytes > maxBytes {
|
|
return fmt.Errorf("restore exceeds maximum size")
|
|
}
|
|
}
|
|
path := filepath.Join(root, filepath.FromSlash(header.Name))
|
|
if !strings.HasPrefix(path, root+string(os.PathSeparator)) && path != root {
|
|
return fmt.Errorf("unsafe archive path %q", header.Name)
|
|
}
|
|
if err := rejectSymlinkParents(root, path); err != nil {
|
|
return err
|
|
}
|
|
if header.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
return fmt.Errorf("refusing to restore through symlink %q", header.Name)
|
|
}
|
|
out, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, copyErr := io.CopyN(out, tr, header.Size)
|
|
closeErr := out.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
}
|
|
}
|
|
|
|
func rejectSymlinkParents(root string, path string) error {
|
|
current := root
|
|
rel, err := filepath.Rel(root, filepath.Dir(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
return nil
|
|
}
|
|
for _, part := range strings.Split(rel, string(os.PathSeparator)) {
|
|
current = filepath.Join(current, part)
|
|
info, err := os.Lstat(current)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return fmt.Errorf("refusing to restore through symlink parent %q", current)
|
|
}
|
|
}
|
|
return nil
|
|
}
|