Skip to content

Commit

Permalink
Register SSHFileSystem with fsspec. (#34)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
NotSpecial authored Jul 14, 2023
1 parent c988754 commit dca7d9b
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 5 deletions.
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,20 @@ the SFTP protocol using [asyncssh](https://github.com/ronf/asyncssh).

## Tutorial

Install the `sshfs` from PyPI or the conda-forge, and import it;
Install the `sshfs` from PyPI or the conda-forge. This will install `fsspec`
and register `sshfs` for `ssh://` urls, so you can open files using:

```py
from fsspec import open

with open('ssh://[user@]host[:port]/path/to/file', "w") as file:
file.write("Hello World!")

with open('ssh://[user@]host[:port]/path/to/file', "r") as file:
print(file.read())
```

For more operations, you can use the `SSHFileSystem` class directly:

```py
from sshfs import SSHFileSystem
Expand Down Expand Up @@ -43,6 +56,8 @@ fs = SSHFileSystem(
)
```

Note: you can also pass `client_keys` as an argument to `fsspec.open`.

All operations and their descriptions are specified [here](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem).
Here are a few example calls you can make, starting with `info()` which allows you to retrieve the metadata about given path;

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

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

```py
>>> with fs.open('/tmp/message.dat') as stream:
... print(stream.read())
...
...
b'super secret messsage!'
```

Expand All @@ -80,10 +95,10 @@ There are also a lot of other basic filesystem operations, such as `mkdir`, `tou
>>> fs.mkdir('/tmp/dir/eggs')
>>> fs.touch('/tmp/dir/spam')
>>> fs.touch('/tmp/dir/eggs/quux')
>>>
>>>
>>> for file in fs.find('/tmp/dir'):
... print(file)
...
...
/tmp/dir/eggs/quux
/tmp/dir/spam
```
Expand Down
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ libnacl = asyncssh[libnacl]
pkcs11 = asyncssh[python-pkcs11]
pyopenssl = asyncssh[pyOpenSSL]
pywin32 = asyncssh[pywin32]

[options.entry_points]
fsspec.specs =
ssh = sshfs.spec:SSHFileSystem
14 changes: 14 additions & 0 deletions sshfs/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import asyncssh
from asyncssh.sftp import SFTPOpUnsupported
from fsspec.asyn import AsyncFileSystem, async_methods, sync, sync_wrapper
from fsspec.utils import infer_storage_options

from sshfs.file import SSHFile
from sshfs.pools import SFTPSoftChannelPool
Expand Down Expand Up @@ -73,6 +74,19 @@ def __init__(
self, sync, self.loop, self._finalize, self._pool, self._stack
)

@classmethod
def _strip_protocol(cls, path):
# Remove components such as host and username from path.
inferred_path = infer_storage_options(path)["path"]
return super()._strip_protocol(inferred_path)

@staticmethod
def _get_kwargs_from_urls(urlpath):
out = infer_storage_options(urlpath)
out.pop("path", None)
out.pop("protocol", None)
return out

@wrap_exceptions
async def _connect(
self, host, pool_type, max_sftp_channels, **client_args
Expand Down
26 changes: 26 additions & 0 deletions tests/test_sshfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import datetime, timedelta
from pathlib import Path

import fsspec
import pytest
from asyncssh.sftp import SFTPFailure

Expand Down Expand Up @@ -71,6 +72,31 @@ def strip_keys(info):
info.pop(key, None)


def test_fsspec_registration(ssh_server):
fs = fsspec.filesystem(
"ssh",
host=ssh_server.host,
port=ssh_server.port,
username="user",
client_keys=[USERS["user"]],
)
assert isinstance(fs, SSHFileSystem)


def test_fsspec_url_parsing(ssh_server, remote_dir, user="user"):
url = f"ssh://{user}@{ssh_server.host}:{ssh_server.port}/{remote_dir}/file"
with fsspec.open(url, "w", client_keys=[USERS[user]]) as file:
# Check the underlying file system.
file_fs = file.buffer.fs
assert isinstance(file_fs, SSHFileSystem)
assert file_fs.storage_options == {
"host": ssh_server.host,
"port": ssh_server.port,
"username": user,
"client_keys": [USERS[user]],
}


def test_info(fs, remote_dir):
fs.touch(remote_dir + "/a.txt")
details = fs.info(remote_dir + "/a.txt")
Expand Down

0 comments on commit dca7d9b

Please sign in to comment.