Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sshfs/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,15 @@ async def _get_file(

@wrap_exceptions
async def _cp_file(self, lpath, rpath, **kwargs):
# Server-side copy through the copy-data extension (asyncssh >=
# 2.19 with an OpenSSH >= 9.0 server) needs no shell access and
# keeps the data on the server. remote_only guards against
# asyncssh silently copying through the client instead. Without
# the extension, fall back to a shell cp.
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)}"
await self._execute(cmd)

Expand Down
60 changes: 60 additions & 0 deletions tests/test_sshfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,66 @@ def test_copy(fs, remote_dir):
assert strip_keys(initial_info) == strip_keys(secondary_info)


class _FakeChannelPool:
def __init__(self, channel):
self.channel = channel

def get(self):
pool = self

class _Ctx:
async def __aenter__(self):
return pool.channel

async def __aexit__(self, *exc):
return False

return _Ctx()


def test_cp_file_remote_copy(fs, monkeypatch):
# A channel advertising copy-data must be used with remote_only=True
# (otherwise asyncssh silently copies through the client) and the
# shell fallback must not run.
calls = {}

class Channel:
supports_remote_copy = True

async def copy(self, lpath, rpath, remote_only=False):
calls["copy"] = (lpath, rpath, remote_only)

async def no_shell(*args, **kwargs):
raise AssertionError("shell fallback must not run")

monkeypatch.setattr(fs, "_pool", _FakeChannelPool(Channel()))
monkeypatch.setattr(fs, "_execute", no_shell)

fs.cp_file("/src", "/dst")
assert calls["copy"] == ("/src", "/dst", True)


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

async def copy(self, *args, **kwargs):
raise AssertionError("copy-data must not be attempted")

async def record_shell(cmd, **kwargs):
calls["cmd"] = cmd

monkeypatch.setattr(fs, "_pool", _FakeChannelPool(Channel()))
monkeypatch.setattr(fs, "_execute", record_shell)

fs.cp_file("/src", "/dst")
assert calls["cmd"] == "cp /src /dst"


def test_rm(fs, remote_dir):
fs.touch(remote_dir + "/a.txt")
fs.rm(remote_dir + "/a.txt")
Expand Down
Loading