Files
docker-compose-backup/internal/backup/copier.go
T
2026-07-04 17:20:28 +08:00

140 lines
2.9 KiB
Go

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())
}