Skip to content

feat(pull,push): work on devices without SFTP - #14

Merged
mariotaku merged 1 commit into
mainfrom
feat/stream-transfer-for-push-pull
Aug 16, 2026
Merged

feat(pull,push): work on devices without SFTP#14
mariotaku merged 1 commit into
mainfrom
feat/stream-transfer-for-push-pull

Conversation

@mariotaku

@mariotaku mariotaku commented Aug 16, 2026

Copy link
Copy Markdown
Member

Follow-up to #13. ares-push and ares-pull opened SFTP directly, so a device
set to "files": "stream" failed with request subsystem failed. ares-install
was the only tool that used the FileTransfer fallback.

Transfer

FileTransfer opens a transport per call, and Session::sftp() is a full
sftp_new + sftp_init round trip. Pushing 500 files meant 500 handshakes.

The new Transfer picks SFTP or a stream over exec channels once and keeps
it for its whole life. FileTransfer now delegates to it, so there is one
implementation, and ares-install is unchanged apart from a &mut that was
never needed.

It also gains what the #13 path rules need:

Method SFTP Stream
stat → dir/file/other/missing sftp_stat [ -d ] / [ -f ] / [ -e ] || [ -L ]
read_dir sftp_readdir, re-stat for symlinks for f in * .* with the same tests
mkdir walks parents, was one level only mkdir -p, unchanged

The stream side needs only a POSIX shell, so the device needs no find, which
@webosose/ares-cli shells out to. It reads one directory per command rather
than one find for the tree, so a deep pull costs more round trips.

Transfer::open also falls back to streaming when a device claims SFTP but the
session will not start.

Errors

TransferError now names the SFTP status — "permission denied" instead of
Sftp error code 3 — and is_permission_denied() lets ares-push suggest
another path.

Test: unit

cargo test --workspace — 77 tests, including the new listing and path-kind
parsers.

Test: against a real SSH server

A sandboxed sshd on 127.0.0.1:2222 and a scratch device list, 30 checks per
transport covering every row of the #13 path table, names with spaces, made
parents, the refusal cases, and -i:

  • SFTP subsystem on, device set to sftp — all pass
  • SFTP subsystem on, device set to stream — all pass
  • SFTP subsystem removed from sshd — both devices still pass, which
    exercises the stream path and the automatic fallback

The same server made the main build fail:

Failed to start SFTP session on t-stream: RequestDenied: request subsystem failed

Test: every busybox LG ships

The busybox version per firmware comes from packages.json in
compat-checker-rs (webOS 7 and
up rename the package to lib32-busybox):

webOS busybox webOS busybox
1.2.0, 1.4.0, 2.2.3 1.20.2 6.4.0 1.29.3
3.4.0, 3.9.2 1.22.1 7.4.0, 8.3.0 1.31.1
4.4.2, 4.10.0 1.24.1 9.2.0, 10.2.0 1.35.0
5.3.1 1.29.2 11.2.0 1.36.1

Both snippets were run under all eight, plus 1.27.2, 1.33.1, 1.37.0, dash
and bash. 1.20.2 through 1.29.2 were built from source as an ash-only static
binary, the rest came from the busybox images. Every one passes:

  • symlink to a file reads as a file, symlink to a directory as a directory
  • broken symlink reads as neither, so the walk skips it
  • names with spaces and dotfiles survive the listing
  • an empty directory gives no output and exit 0
  • a missing directory exits 1

🤖 Generated with Claude Code

ares-push and ares-pull opened SFTP directly, so a device set to
"files": "stream" failed with "request subsystem failed". Only ares-install
went through the FileTransfer fallback.

Add Transfer, an open transport that picks SFTP or a stream over exec
channels once and keeps it. Copying many files now costs one SFTP handshake
instead of one per file, where FileTransfer opens a session per call.

Transfer also gains what the path rules need: stat, which answers dir, file,
other or missing, and read_dir. The stream side asks a POSIX shell, checked
against busybox ash and dash, so the device needs no find. mkdir now makes
missing parents over SFTP too, matching what the stream side already did with
mkdir -p.

TransferError names the SFTP status instead of printing "Sftp error code 3",
and is_permission_denied lets a caller suggest another path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mariotaku
mariotaku marked this pull request as ready for review August 16, 2026 15:22
@mariotaku
mariotaku merged commit 6b3c105 into main Aug 16, 2026
4 checks passed
@mariotaku
mariotaku deleted the feat/stream-transfer-for-push-pull branch August 18, 2026 15:44
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