Skip to content

Commit dca7d9b

Browse files
authored
Register SSHFileSystem with fsspec. (#34)
* Make it possible to register SSHFS. * Fix _strip_protocol. * Add test for registration. * Update README. * Sort imports to satisfy pre-commit. * Assert file system type. * Formatting. * Update tests. * Simplify test.
1 parent c988754 commit dca7d9b

File tree

4 files changed

+64
-5
lines changed

4 files changed

+64
-5
lines changed

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,20 @@ the SFTP protocol using [asyncssh](https://github.com/ronf/asyncssh).
1313

1414
## Tutorial
1515

16-
Install the `sshfs` from PyPI or the conda-forge, and import it;
16+
Install the `sshfs` from PyPI or the conda-forge. This will install `fsspec`
17+
and register `sshfs` for `ssh://` urls, so you can open files using:
18+
19+
```py
20+
from fsspec import open
21+
22+
with open('ssh://[user@]host[:port]/path/to/file', "w") as file:
23+
file.write("Hello World!")
24+
25+
with open('ssh://[user@]host[:port]/path/to/file', "r") as file:
26+
print(file.read())
27+
```
28+
29+
For more operations, you can use the `SSHFileSystem` class directly:
1730

1831
```py
1932
from sshfs import SSHFileSystem
@@ -43,6 +56,8 @@ fs = SSHFileSystem(
4356
)
4457
```
4558

59+
Note: you can also pass `client_keys` as an argument to `fsspec.open`.
60+
4661
All operations and their descriptions are specified [here](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem).
4762
Here are a few example calls you can make, starting with `info()` which allows you to retrieve the metadata about given path;
4863

@@ -61,15 +76,15 @@ You can also create new files through either putting a local file with `put_file
6176
```py
6277
>>> with fs.open('/tmp/message.dat', 'wb') as stream:
6378
... stream.write(b'super secret messsage!')
64-
...
79+
...
6580
```
6681

6782
And either download it through `get_file` or simply read it on the fly with opening it;
6883

6984
```py
7085
>>> with fs.open('/tmp/message.dat') as stream:
7186
... print(stream.read())
72-
...
87+
...
7388
b'super secret messsage!'
7489
```
7590

@@ -80,10 +95,10 @@ There are also a lot of other basic filesystem operations, such as `mkdir`, `tou
8095
>>> fs.mkdir('/tmp/dir/eggs')
8196
>>> fs.touch('/tmp/dir/spam')
8297
>>> fs.touch('/tmp/dir/eggs/quux')
83-
>>>
98+
>>>
8499
>>> for file in fs.find('/tmp/dir'):
85100
... print(file)
86-
...
101+
...
87102
/tmp/dir/eggs/quux
88103
/tmp/dir/spam
89104
```

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ libnacl = asyncssh[libnacl]
2626
pkcs11 = asyncssh[python-pkcs11]
2727
pyopenssl = asyncssh[pyOpenSSL]
2828
pywin32 = asyncssh[pywin32]
29+
30+
[options.entry_points]
31+
fsspec.specs =
32+
ssh = sshfs.spec:SSHFileSystem

sshfs/spec.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import asyncssh
1010
from asyncssh.sftp import SFTPOpUnsupported
1111
from fsspec.asyn import AsyncFileSystem, async_methods, sync, sync_wrapper
12+
from fsspec.utils import infer_storage_options
1213

1314
from sshfs.file import SSHFile
1415
from sshfs.pools import SFTPSoftChannelPool
@@ -73,6 +74,19 @@ def __init__(
7374
self, sync, self.loop, self._finalize, self._pool, self._stack
7475
)
7576

77+
@classmethod
78+
def _strip_protocol(cls, path):
79+
# Remove components such as host and username from path.
80+
inferred_path = infer_storage_options(path)["path"]
81+
return super()._strip_protocol(inferred_path)
82+
83+
@staticmethod
84+
def _get_kwargs_from_urls(urlpath):
85+
out = infer_storage_options(urlpath)
86+
out.pop("path", None)
87+
out.pop("protocol", None)
88+
return out
89+
7690
@wrap_exceptions
7791
async def _connect(
7892
self, host, pool_type, max_sftp_channels, **client_args

tests/test_sshfs.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from datetime import datetime, timedelta
88
from pathlib import Path
99

10+
import fsspec
1011
import pytest
1112
from asyncssh.sftp import SFTPFailure
1213

@@ -71,6 +72,31 @@ def strip_keys(info):
7172
info.pop(key, None)
7273

7374

75+
def test_fsspec_registration(ssh_server):
76+
fs = fsspec.filesystem(
77+
"ssh",
78+
host=ssh_server.host,
79+
port=ssh_server.port,
80+
username="user",
81+
client_keys=[USERS["user"]],
82+
)
83+
assert isinstance(fs, SSHFileSystem)
84+
85+
86+
def test_fsspec_url_parsing(ssh_server, remote_dir, user="user"):
87+
url = f"ssh://{user}@{ssh_server.host}:{ssh_server.port}/{remote_dir}/file"
88+
with fsspec.open(url, "w", client_keys=[USERS[user]]) as file:
89+
# Check the underlying file system.
90+
file_fs = file.buffer.fs
91+
assert isinstance(file_fs, SSHFileSystem)
92+
assert file_fs.storage_options == {
93+
"host": ssh_server.host,
94+
"port": ssh_server.port,
95+
"username": user,
96+
"client_keys": [USERS[user]],
97+
}
98+
99+
74100
def test_info(fs, remote_dir):
75101
fs.touch(remote_dir + "/a.txt")
76102
details = fs.info(remote_dir + "/a.txt")

0 commit comments

Comments
 (0)