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
@@ -80,6 +80,7 @@ type Server struct {
|
||||
fsLocal files.FS
|
||||
fsSudo files.FS // nil without passwordless sudo
|
||||
thumbs *thumbSvc
|
||||
self string // this binary, for helpers run as tasks
|
||||
nodes *nodes.Manager
|
||||
fingerprint string
|
||||
version string
|
||||
@@ -99,6 +100,7 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
if err != nil {
|
||||
self = "hamlab"
|
||||
}
|
||||
s.self = self
|
||||
s.space = space.New(self, filepath.Join(cfg.DataDir(), "space"))
|
||||
s.thumbs = newThumbSvc(cfg.DataDir(), self)
|
||||
s.hostName() // read it once now so the first request doesn't have to
|
||||
@@ -301,6 +303,7 @@ func (s *Server) routes() *http.ServeMux {
|
||||
m.HandleFunc("POST /api/files/mkdir", s.auth(s.filesMkdir))
|
||||
m.HandleFunc("POST /api/files/touch", s.auth(s.filesTouch))
|
||||
m.HandleFunc("POST /api/files/rename", s.auth(s.filesRename))
|
||||
m.HandleFunc("POST /api/files/move", s.auth(s.filesMove))
|
||||
m.HandleFunc("POST /api/files/delete", s.auth(s.filesDelete))
|
||||
m.HandleFunc("POST /api/files/chmod", s.auth(s.filesChmod))
|
||||
m.HandleFunc("GET /api/files/thumb", s.auth(s.filesThumb))
|
||||
@@ -2054,6 +2057,57 @@ func (s *Server) filesRename(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// filesMove: POST {path, to} — to is the full destination path. A rename when the two
|
||||
// ends share a filesystem (204); otherwise the copy-and-remove runs as a task (202 with the
|
||||
// task), since a folder between datasets can take as long as it takes.
|
||||
func (s *Server) filesMove(w http.ResponseWriter, r *http.Request) {
|
||||
fsys, ok := s.fsFor(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body struct{ Path, To string }
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 64<<10)).Decode(&body); err != nil {
|
||||
jsonErr(w, 400, "bad request")
|
||||
return
|
||||
}
|
||||
from, err := files.Clean(body.Path)
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
to, err := files.Clean(body.To)
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if from == to {
|
||||
jsonErr(w, 400, "it is already there")
|
||||
return
|
||||
}
|
||||
if files.Inside(from, to) {
|
||||
jsonErr(w, 400, "a folder can't be moved into itself")
|
||||
return
|
||||
}
|
||||
err = fsys.Rename(r.Context(), from, to)
|
||||
if err == nil {
|
||||
log.Printf("files: move %s -> %s (from %s)", from, to, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
if !files.IsCrossDevice(err) {
|
||||
fsErr(w, err)
|
||||
return
|
||||
}
|
||||
cmd := files.MoveCmd(context.Background(), fsys, s.self, from, to)
|
||||
t, err := s.tasks.Start("files:move", "move "+filepath.Base(from)+" to "+filepath.Dir(to), cmd)
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("files: move %s -> %s across filesystems as task %s (from %s)", from, to, t.ID, clientIP(r))
|
||||
jsonOK(w, 202, t.Snapshot(false))
|
||||
}
|
||||
|
||||
func (s *Server) filesDelete(w http.ResponseWriter, r *http.Request) {
|
||||
s.filesOp(w, r, func(fsys files.FS, body map[string]any, p string) error {
|
||||
rec, _ := body["recursive"].(bool)
|
||||
|
||||
Reference in New Issue
Block a user