feat: move or copy dialog with folder suggestions and a rule for what is already there
This commit is contained in:
1 parent
89d5bfd3ab
commit
e983fc179a
5 files changed
+337
-6
No files matched your search
@@ -80,6 +80,10 @@ func dispatchFS(env proto.TaskEnvelope) (any, error) {
|
||||
var a proto.FSChmodArgs
|
||||
_ = json.Unmarshal(env.Args, &a)
|
||||
return fsChmod(a)
|
||||
case proto.TaskFSCopy:
|
||||
var a proto.FSCopyArgs
|
||||
_ = json.Unmarshal(env.Args, &a)
|
||||
return fsCopy(a)
|
||||
default:
|
||||
return nil, fmt.Errorf("неизвестная операция %q", env.Type)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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))
|
||||
}
|
||||
Reference in new issue
Block a user