Initial commit on Forgejo
Fresh repository history for elektrine/magpie hosted at https://git.elektrine.com/elektrine/magpie.
This commit is contained in:
commit
ddbd19f153
23 changed files with 5636 additions and 0 deletions
181
backup.go
Normal file
181
backup.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue