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