140 lines
2.9 KiB
Go
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())
|
|
} |