Files: Move… opens a folder picker, and a move across filesystems copies then removes
The picker walks the machine like the tab does — back, forward, up, breadcrumbs, a path box, a new folder — and "Move here" puts the file or folder in the folder being looked at; it says why it can't when it can't (already here, into itself, a name clash). POST /api/files/move is a rename when the two ends share a filesystem and otherwise a task: on a NAS every move between datasets is a different filesystem, and a rename there is EXDEV. The copy is built under a hidden temporary name beside its destination, renamed into place once complete, and only then is the original removed, so a failure or a cancel leaves the original as it was. Modes, times, symlinks and, as root, owners are kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e5d3fb2afd
commit
fb3b8e4b4c
+37
-2
@@ -15,6 +15,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -62,6 +63,7 @@ type FS interface {
|
||||
Mkdir(ctx context.Context, path string) error
|
||||
Touch(ctx context.Context, path string) error
|
||||
Rename(ctx context.Context, from, to string) error
|
||||
Move(ctx context.Context, from, to string, progress io.Writer) error // Rename, or across filesystems a copy and a removal; long, so callers run it as a task
|
||||
Delete(ctx context.Context, path string, recursive bool) error
|
||||
Chmod(ctx context.Context, path string, perm os.FileMode) error
|
||||
}
|
||||
@@ -385,6 +387,30 @@ func (s Sudo) Touch(ctx context.Context, path string) error {
|
||||
func (s Sudo) Rename(ctx context.Context, from, to string) error {
|
||||
return s.runJSON(ctx, nil, "rename", from, to)
|
||||
}
|
||||
|
||||
// Move as root: the helper does the copying and talks on stdout.
|
||||
func (s Sudo) Move(ctx context.Context, from, to string, progress io.Writer) error {
|
||||
c := s.cmd(ctx, "move", from, to)
|
||||
c.Stdout = progress
|
||||
var errb strings.Builder
|
||||
c.Stderr = &errb
|
||||
if err := c.Run(); err != nil {
|
||||
if msg := strings.TrimSpace(errb.String()); msg != "" {
|
||||
return errors.New(msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCmd is the command a task runs for a long move: this binary's helper, as root when
|
||||
// the backend is.
|
||||
func MoveCmd(ctx context.Context, fsys FS, self, from, to string) *exec.Cmd {
|
||||
if s, ok := fsys.(Sudo); ok {
|
||||
return s.cmd(ctx, "move", from, to)
|
||||
}
|
||||
return exec.CommandContext(ctx, self, "fs", "move", from, to)
|
||||
}
|
||||
func (s Sudo) Delete(ctx context.Context, path string, recursive bool) error {
|
||||
if recursive {
|
||||
return s.runJSON(ctx, nil, "delete", "-r", path)
|
||||
@@ -399,7 +425,7 @@ func (s Sudo) Chmod(ctx context.Context, path string, perm os.FileMode) error {
|
||||
func Helper(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
|
||||
fail := func(err error) int { fmt.Fprintln(stderr, err.Error()); return 1 }
|
||||
if len(args) < 2 {
|
||||
return fail(errors.New("usage: hamlab fs <list|stat|read|write|mkdir|touch|rename|delete|chmod> …"))
|
||||
return fail(errors.New("usage: hamlab fs <list|stat|read|write|mkdir|touch|rename|move|delete|chmod> …"))
|
||||
}
|
||||
ctx := context.Background()
|
||||
var l Local
|
||||
@@ -452,7 +478,7 @@ func Helper(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer)
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
case "rename":
|
||||
case "rename", "move":
|
||||
from, err := clean(1)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
@@ -461,6 +487,15 @@ func Helper(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if op == "move" {
|
||||
// A cancelled task is a SIGTERM: stop copying and take the partial copy away.
|
||||
mctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer stop()
|
||||
if err := l.Move(mctx, from, to, stdout); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if err := l.Rename(ctx, from, to); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user