diff --git a/README.md b/README.md index 6fe0318..56165b2 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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; @@ -61,7 +76,7 @@ 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; @@ -69,7 +84,7 @@ And either download it through `get_file` or simply read it on the fly with open ```py >>> with fs.open('/tmp/message.dat') as stream: ... print(stream.read()) -... +... b'super secret messsage!' ``` @@ -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 ``` diff --git a/setup.cfg b/setup.cfg index 6999eff..8428235 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/sshfs/spec.py b/sshfs/spec.py index fc35e4c..954c7bd 100644 --- a/sshfs/spec.py +++ b/sshfs/spec.py @@ -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 @@ -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 diff --git a/tests/test_sshfs.py b/tests/test_sshfs.py index 06d009b..051a619 100644 --- a/tests/test_sshfs.py +++ b/tests/test_sshfs.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from pathlib import Path +import fsspec import pytest from asyncssh.sftp import SFTPFailure @@ -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")