Skip to content

feat: sync only the files and folders you picked - #20

Merged
ralyodio merged 1 commit into
mainfrom
feat/sync-only-selected
Aug 30, 2026
Merged

feat: sync only the files and folders you picked#20
ralyodio merged 1 commit into
mainfrom
feat/sync-only-selected

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Both surfaces could sync a directory. Neither could sync three things out of it — the one thing an SFTP client makes trivial.

Desktop: tick entries in the source pane and the transfer button changes from Sync to web-01 to Send 2 → web-01. Only those move. Nothing ticked keeps the old meaning: the whole directory. The panes already tracked a selection; it just had nothing to do with transfers.

CLI: --only NAME, repeatable.

diskpush ./site/ prod:/var/www/ --only index.html --only assets
diskpush mirror ./cache/ prod:/srv/cache/ --only sessions

Mechanism

Names go to rsync as a NUL-separated --files-from list rather than as extra source arguments. Both work; the list is not bounded by the command-line length limit, so four hundred files is the same shape as one, and with --from0 it can express every name a filesystem allows — including ones containing a newline, which no line-based list can.

Two behaviours of --files-from, both established against rsync 3.4.1 rather than read off the flag summary, and both of which would otherwise have shipped as bugs:

  • It turns recursion OFF, and --archive does not turn it back on. A selected folder arrives as an empty directory. buildRsyncArgs now restates --recursive whenever a files-from list is in play.
  • --delete stays scoped to the listed entries. mirror --only cache removes destination files inside cache/ and leaves the rest of the destination alone — worth stating because it is not the behaviour you would fear.

Safety

A selection is a choice among what the source directory holds, so a name is the only thing it can be. writeSelectionList refuses .., absolute paths and NUL bytes, and the IPC contract types the field as entry names, so a compromised renderer cannot turn a selection into a path.

Also moves EntryNameSchema above its first use. TransferRequestSchema now references it, and a const is not hoisted — declaring it afterwards is a temporal-dead-zone ReferenceError at import time, which typechecks perfectly and kills the main process on launch. Caught by loading the built module, not by reading it.

Verified against real files

One file; a folder (must arrive with contents); several at once including a name with spaces; --dry-run changing nothing; .. and absolute paths refused; no temp list left behind; and mirror --only leaving an unrelated destination file untouched.

The desktop change was screenshotted: two rows ticked, 2 selected · 24.6 KB in the pane footer, SEND 2 → web-01 on the armed button and SYNC TO Local on the other — right, because the count belongs to the source pane.

504 tests, up from 491. Typecheck and pnpm smoke:desktop green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4

Both surfaces could sync a directory. Neither could sync three things out of
it, which is the one thing an SFTP client makes trivial.

Desktop: tick entries in the source pane and the transfer button changes from
"Sync to web-01" to "Send 2 -> web-01". Only those entries move. Nothing
ticked keeps the old meaning, the whole directory. The panes already tracked a
selection; it just had nothing to do with transfers.

CLI: `--only NAME`, repeatable.

    diskpush ./site/ prod:/var/www/ --only index.html --only assets
    diskpush mirror ./cache/ prod:/srv/cache/ --only sessions

The names go to rsync as a NUL-separated `--files-from` list rather than as
extra source arguments. Both work; the list is not bounded by the command-line
length limit, so four hundred files is the same shape as one, and with
`--from0` it can express every name a filesystem allows — including the ones
containing a newline, which no line-based list can.

Two behaviours of `--files-from`, both established against rsync 3.4.1 rather
than read off the flag summary, and both of which would have shipped as bugs:

  - It turns recursion OFF, and `--archive` does not turn it back on. A
    selected folder arrives as an EMPTY DIRECTORY. `buildRsyncArgs` now
    restates `--recursive` whenever a files-from list is in play.
  - `--delete` stays scoped to the listed entries. `mirror --only cache`
    removes destination files inside `cache/` and leaves the rest of the
    destination alone — the behaviour you would want, and worth stating
    because it is not the one you would fear.

A selection is a choice among what the source directory holds, so a name is
the only thing it can be: `writeSelectionList` refuses `..`, absolute paths
and NUL bytes, and the IPC contract types the field as entry names, so a
compromised renderer cannot turn a selection into a path.

Also moves `EntryNameSchema` above its first use. `TransferRequestSchema` now
references it, and a `const` is not hoisted — declaring it afterwards is a
temporal-dead-zone ReferenceError at import time, which typechecks perfectly
and kills the main process on launch. Caught by loading the built module, not
by reading it.

Verified against real files end to end: one file; a folder, which must arrive
with its contents; several at once including a name with spaces; `--dry-run`
changing nothing; `..` and absolute paths refused; no temp list left behind;
and `mirror --only` leaving an unrelated destination file untouched. The
desktop change was screenshotted — two rows ticked, "2 selected" in the pane
footer, "SEND 2 web-01" on the armed button and "SYNC TO Local" on the other,
which is right because the count belongs to the source pane.

504 tests, up from 491.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4
@ralyodio
ralyodio merged commit 79e8ab6 into main Aug 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant