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:
Mitchell-Johnson
2026-10-11 16:27:55 -07:00
co-authored by Claude Fable 5.1
parent e5d3fb2afd
commit fb3b8e4b4c
7 changed files with 567 additions and 6 deletions
+12
View File
@@ -371,6 +371,18 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
.fthumb.video::after { content: '▶'; position: absolute; right: 2px; bottom: 2px; font-size: 9px; line-height: 1; padding: 2px 3px; border-radius: 3px; background: rgba(0,0,0,.6); color: #fff; }
.fthumb .ficon { font-size: 18px; }
.thumb-note { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 6px 10px 0; }
/* move: the folder picker */
.modal.mover { width: min(560px, 100%); height: min(76vh, 100%); overflow: hidden; }
.mover-nav { display: flex; align-items: center; gap: 2px; }
.mover-nav .icon-btn { font-size: 18px; min-width: 30px; line-height: 1; }
.mover-nav .icon-btn:disabled { opacity: .3; }
.mover-nav .crumbs { flex: 1; min-width: 0; margin-left: 4px; }
.modal.mover > .mover-list { flex: 1 1 auto; min-height: 80px; overflow-y: auto; margin: 0 -16px; border-top: 1px solid var(--border); }
.mover-row { display: flex; align-items: center; gap: 8px; padding: 9px 16px; cursor: pointer; border-bottom: 1px solid var(--border); min-height: 38px; }
.mover-row:hover { background: var(--bg3); }
.mover-row.self { opacity: .5; cursor: default; }
.mover-row .fname { flex: 1; }
.mover-dest { overflow-wrap: anywhere; }
.thumb-note .btn { padding-top: 3px; padding-bottom: 3px; }
.files-bar .task-out { margin: 6px 10px 0; max-height: 160px; }
@media (max-width: 800px) {