try copy() instead of using shell cp - #61
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the SSHFS copy implementation to prefer an SFTP server-side copy operation (when supported) before falling back to running cp over an SSH shell session.
Changes:
- Attempt
channel.copy()using the SFTP “copy-data” extension and fall back to shellcpif unsupported. - Keep existing shell-based
cpexecution path as a compatibility fallback.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Server-side copy over SFTP (asyncssh >= 2.19 with an OpenSSH >= 9.0 server) needs no shell access and keeps the data on the server, which also unblocks copy/move on SFTP-only servers that deny exec. The branch is gated on supports_remote_copy: without it asyncssh would silently copy through the client (download + re-upload) on servers lacking the extension, and asyncssh <= 2.18 has no server-side copy at all. remote_only=True keeps that guarantee even if the gate is ever bypassed. Everything else falls back to the shell cp as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A channel advertising supports_remote_copy must be used with remote_only=True and no shell; a channel without it (or an asyncssh without the attribute) must use the shell fallback. test_copy keeps covering the fallback end to end against mockssh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @crawld — the direction was right, and I've finished it up (rebased onto
Details in the updated description. |
|
Heads-up: GitHub doesn't fire workflow runs for maintainer pushes to a fork's default branch (this PR is from |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
sshfs/spec.py:274
- _cp_file now always acquires an SFTP channel just to check supports_remote_copy, even when the server doesn’t support remote copy and the function immediately falls back to shell
cp. This can unnecessarily consume/potentially create SFTP channels and add overhead on the common fallback path. Consider caching the capability after the first check so subsequent fallbacks can go straight to the shell path without touching the SFTP pool.
async with self._pool.get() as channel:
if getattr(channel, "supports_remote_copy", False):
return await channel.copy(lpath, rpath, remote_only=True)
cmd = f"cp {shlex.quote(lpath)} {shlex.quote(rpath)}"
tests/test_sshfs.py:272
- The test comment says it covers the asyncssh < 2.19 case where
supports_remote_copydoesn’t exist, but the fake Channel definessupports_remote_copy = False. This leaves the “missing attribute” behavior untested. Add a second fake channel without the attribute (or parametrize) so the getattr/default path is actually exercised.
def test_cp_file_shell_fallback(fs, monkeypatch):
# Without copy-data support (or on asyncssh < 2.19, where the
# attribute does not exist), the shell cp path is used.
calls = {}
class Channel:
supports_remote_copy = False
|
Closing in favor of #74 |
Problem
cp_file(and themovefallback) shells out tocpover an SSH exec channel. SFTP-only servers that deny exec (chrootedinternal-sftp, transfer appliances) can't copy at all, and the remote must have a POSIX shell withcp.Change
When the channel advertises the
copy-dataextension (supports_remote_copy, asyncssh >= 2.19 talking to e.g. OpenSSH >= 9.0), copy server-side over SFTP withchannel.copy(..., remote_only=True). Everything else keeps the shellcpfallback, byte-for-byte as today.Two guards matter:
supports_remote_copygate — plainchannel.copy()never raisesSFTPOpUnsupportedunlessremote_only=Trueis passed; without the gate, servers lacking the extension would be silently "copied" through the client (download + re-upload). On asyncssh <= 2.18copy()is only the client-side round-trip, so the gate (viagetattr) also keeps old asyncssh on the shell path. No version pin change needed.remote_only=True— belt and braces so asyncssh can never fall back to the client-side copy.Tests
test_cp_file_remote_copy/test_cp_file_shell_fallbackpin the gating with fake channels; the existingtest_copykeeps covering the shell fallback end to end (mockssh has no copy-data).