feat: docker compose backup
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DirectoryCopier copies a directory tree with optional exclusions.
|
||||
type DirectoryCopier struct {
|
||||
Exclude []string
|
||||
Context context.Context // optional; checked during copy
|
||||
}
|
||||
|
||||
// Copy recursively copies src to dst, skipping paths that match any
|
||||
// exclusion pattern (filepath.Match semantics). Skips special files
|
||||
// (sockets, FIFOs, devices) that cannot be meaningfully copied.
|
||||
func (c *DirectoryCopier) Copy(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Context != nil {
|
||||
select {
|
||||
case <-c.Context.Done():
|
||||
return c.Context.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rel(%s, %s): %w", src, path, err)
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, pattern := range c.Exclude {
|
||||
matched, err := filepath.Match(pattern, rel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
baseMatched, _ := filepath.Match(pattern, d.Name())
|
||||
if matched || baseMatched {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return copyDirMetadata(path, target, d)
|
||||
}
|
||||
|
||||
if isSpecial(d) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return copyFile(path, target, d)
|
||||
})
|
||||
}
|
||||
|
||||
func copyDirMetadata(src, dst string, entry fs.DirEntry) error {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dst, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(dst, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chtimes(dst, info.ModTime(), info.ModTime())
|
||||
}
|
||||
|
||||
// isSpecial returns true if the entry is a socket, FIFO, device, or other
|
||||
// non-regular, non-symlink, non-directory file.
|
||||
func isSpecial(d fs.DirEntry) bool {
|
||||
if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 {
|
||||
return false
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return true // can't stat → skip it
|
||||
}
|
||||
mode := info.Mode()
|
||||
return mode&(os.ModeSocket|os.ModeNamedPipe|os.ModeDevice|os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, entry fs.DirEntry) error {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
link, err := os.Readlink(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(dst)
|
||||
return os.Symlink(link, dst)
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(dstFile, srcFile)
|
||||
closeErr := dstFile.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
if err := os.Chmod(dst, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chtimes(dst, info.ModTime(), info.ModTime())
|
||||
}
|
||||
Reference in New Issue
Block a user