package agent import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "syscall" "github.com/x-grk/longpolling-remote-access/internal/proto" ) // fsCopy copies or moves Src to Dst, following Conflict when Dst exists. func fsCopy(a proto.FSCopyArgs) (any, error) { src, dst := filepath.Clean(a.Src), filepath.Clean(a.Dst) if a.Src == "" || a.Dst == "" { return nil, errors.New("не указан путь") } if src == dst { return nil, errors.New("откуда и куда - одно и то же") } if strings.HasPrefix(dst, src+string(filepath.Separator)) { return nil, errors.New("нельзя положить папку внутрь неё самой") } if _, err := os.Lstat(src); err != nil { return nil, err } if _, err := os.Stat(filepath.Dir(dst)); err != nil { return nil, fmt.Errorf("нет папки %s", filepath.Dir(dst)) } c := &copier{move: a.Move, conflict: a.Conflict} if err := c.place(src, dst); err != nil { return map[string]int{"done": c.done, "skipped": c.skipped}, err } return map[string]int{"done": c.done, "skipped": c.skipped}, nil } type copier struct { move bool conflict string done, skipped int } // place puts src at dst. Two folders merge when the rule allows it; anything // else that is in the way stops, is replaced or is kept, by the rule. func (c *copier) place(src, dst string) error { si, err := os.Lstat(src) if err != nil { return err } di, err := os.Lstat(dst) if err == nil { if c.conflict != "replace" && c.conflict != "skip" { return fmt.Errorf("%s уже существует", dst) } if si.IsDir() && di.IsDir() { entries, err := os.ReadDir(src) if err != nil { return err } for _, e := range entries { if err := c.place(filepath.Join(src, e.Name()), filepath.Join(dst, e.Name())); err != nil { return err } } if c.move { // Whatever was skipped stays where it was. _ = os.Remove(src) } return nil } if c.conflict == "skip" { c.skipped++ return nil } if err := os.RemoveAll(dst); err != nil { return err } } else if !errors.Is(err, fs.ErrNotExist) { return err } if c.move { err := os.Rename(src, dst) if err == nil { c.done++ return nil } // Another file system: copy, then take the original away. if !errors.Is(err, syscall.EXDEV) { return err } if err := c.copyTree(src, dst); err != nil { return err } return os.RemoveAll(src) } return c.copyTree(src, dst) } // copyTree copies a file, a symlink or a whole folder, keeping modes and // times. func (c *copier) copyTree(src, dst string) error { return filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } rel, _ := filepath.Rel(src, p) to := filepath.Join(dst, rel) info, err := os.Lstat(p) if err != nil { return err } switch { case info.Mode()&fs.ModeSymlink != 0: link, err := os.Readlink(p) if err != nil { return err } if err := os.Symlink(link, to); err != nil { return err } case info.IsDir(): if err := os.Mkdir(to, info.Mode().Perm()); err != nil && !errors.Is(err, fs.ErrExist) { return err } case info.Mode().IsRegular(): if err := copyFile(p, to, info); err != nil { return err } default: return nil // sockets, devices: not copied } c.done++ if st, ok := info.Sys().(*syscall.Stat_t); ok { _ = os.Lchown(to, int(st.Uid), int(st.Gid)) } if info.Mode()&fs.ModeSymlink == 0 { _ = os.Chtimes(to, info.ModTime(), info.ModTime()) } return nil }) } func copyFile(src, dst string, info fs.FileInfo) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode().Perm()) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() os.Remove(dst) return err } if err := out.Close(); err != nil { return err } return os.Chmod(dst, info.Mode()&(fs.ModePerm|fs.ModeSetuid|fs.ModeSetgid|fs.ModeSticky)) }