167 lines
4.0 KiB
Go
167 lines
4.0 KiB
Go
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))
|
|
}
|