From e2235c24231f8c33fa0b727b4b25fcd62729ccee Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Tue, 8 Apr 2025 15:51:46 -0700 Subject: Copy recursive --- internal/cp/cp.go | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 internal/cp/cp.go (limited to 'internal') diff --git a/internal/cp/cp.go b/internal/cp/cp.go new file mode 100644 index 0000000..c19627f --- /dev/null +++ b/internal/cp/cp.go @@ -0,0 +1,172 @@ +package cp + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" +) + +type Progress interface { + Max(int64) + Progress(int64) + FileStart(string) + FileDone(string, error) +} + +func size(files []string) int64 { + var n int64 = 0 + for _, f := range files { + filepath.WalkDir(f, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + switch d.Type() { + case 0: // regular file + stat, err := d.Info() + if err != nil { + return nil + } + n += 1 + stat.Size() + case os.ModeSymlink, os.ModeDir: + n++ + } + return nil + }) + } + return n +} + +func fileExists(path string) bool { + _, err := os.Lstat(path) + return !errors.Is(err, os.ErrNotExist) +} + +func copyRegularFile(src, dst string, force bool, progress Progress) (err error) { + progress.FileStart(src) + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + stat, err := in.Stat() + if err != nil { + return err + } + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode().Perm()) + if err != nil { + if !force || !fileExists(dst) { + return err + } + if err := os.RemoveAll(dst); err != nil { + return err + } + out, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode().Perm()) + if err != nil { + return err + } + } + defer out.Close() + for { + n, err := io.CopyN(out, in, 1024*1024) + if n > 0 { + progress.Progress(n) + } + if err != nil { + if err == io.EOF { + break + } + return err + } + } + if err := out.Close(); err != nil { + return err + } + progress.Progress(1) + return nil +} + +func copySpecialFile(src string, d fs.DirEntry, dst string, force bool, progress Progress) error { + switch d.Type() { + case fs.ModeSymlink: + target, err := os.Readlink(src) + if err != nil { + return err + } + if err := os.Symlink(target, dst); err != nil { + if !force || !fileExists(dst) { + return err + } + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := os.Symlink(target, dst); err != nil { + return err + } + } + progress.Progress(1) + case fs.ModeDir: + stat, err := d.Info() + if err != nil { + return err + } + if err := os.Mkdir(dst, stat.Mode().Perm()); err != nil { + if !force || !fileExists(dst) { + return err + } + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := os.Mkdir(dst, stat.Mode().Perm()); err != nil { + return err + } + } + progress.Progress(1) + default: + return fmt.Errorf("%s: unknown file type %s", src, d.Type()) + } + return nil +} + +func Copy(srcs []string, dstRoot string, force bool, progress Progress) { + go func() { + progress.Max(size(srcs)) + }() + const maxConcurrency = 10 + // sem acts as a semaphore to limit the number of concurrent file copies + sem := make(chan struct{}, maxConcurrency) + for _, srcRoot := range srcs { + filepath.WalkDir(srcRoot, func(src string, d fs.DirEntry, err error) error { + if err != nil { + progress.FileDone(src, err) + return nil + } + relPath, err := filepath.Rel(srcRoot, src) + if err != nil { + progress.FileDone(src, err) + return nil + } + dst := filepath.Join(dstRoot, relPath) + if d.Type().IsRegular() { + sem <- struct{}{} + go func() { + defer func() { <-sem }() + err := copyRegularFile(src, dst, force, progress) + progress.FileDone(src, err) + }() + } else { + if err := copySpecialFile(src, d, dst, force, progress); err != nil { + progress.FileDone(src, err) + return nil + } + } + return nil + }) + } + for range maxConcurrency { + sem <- struct{}{} + } +} -- cgit v1.3.1