|
1 | | -try: |
2 | | - from fs.sshfs.sshfs import SSHFS |
3 | | -except ImportError: |
4 | | - SSHFS = None |
5 | | - |
| 1 | +from io import StringIO |
6 | 2 | from typing import ( |
7 | 3 | Optional, |
| 4 | + TYPE_CHECKING, |
8 | 5 | Union, |
9 | 6 | ) |
10 | 7 |
|
11 | | -from galaxy.files.models import ( |
12 | | - BaseFileSourceConfiguration, |
13 | | - BaseFileSourceTemplateConfiguration, |
14 | | - FilesSourceRuntimeContext, |
| 8 | +try: |
| 9 | + from fsspec.implementations.sftp import SFTPFileSystem |
| 10 | + from paramiko.ecdsakey import ECDSAKey |
| 11 | + from paramiko.ed25519key import Ed25519Key |
| 12 | + from paramiko.rsakey import RSAKey |
| 13 | +except ImportError: |
| 14 | + SFTPFileSystem = None |
| 15 | + if TYPE_CHECKING: |
| 16 | + from paramiko.ecdsakey import ECDSAKey |
| 17 | + from paramiko.ed25519key import Ed25519Key |
| 18 | + from paramiko.rsakey import RSAKey |
| 19 | + |
| 20 | +from galaxy.exceptions import AuthenticationFailed |
| 21 | +from galaxy.files.models import FilesSourceRuntimeContext |
| 22 | +from galaxy.files.sources._fsspec import ( |
| 23 | + CacheOptionsDictType, |
| 24 | + FsspecBaseFileSourceConfiguration, |
| 25 | + FsspecBaseFileSourceTemplateConfiguration, |
| 26 | + FsspecFilesSource, |
15 | 27 | ) |
16 | 28 | from galaxy.util.config_templates import TemplateExpansion |
17 | | -from ._pyfilesystem2 import PyFilesystem2FilesSource |
18 | 29 |
|
19 | 30 |
|
20 | | -class SshFileSourceTemplateConfiguration(BaseFileSourceTemplateConfiguration): |
| 31 | +def _parse_private_key(private_key: str, password: Optional[str]): |
| 32 | + # Paramiko cannot autodetect the key type, so try the supported key classes. |
| 33 | + for pkey_class in (RSAKey, ECDSAKey, Ed25519Key): |
| 34 | + try: |
| 35 | + with StringIO(private_key) as pkey_file: |
| 36 | + return pkey_class.from_private_key(pkey_file, password=password) |
| 37 | + except Exception: |
| 38 | + continue |
| 39 | + |
| 40 | + return None |
| 41 | + |
| 42 | + |
| 43 | +class SshFileSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfiguration): |
21 | 44 | host: Union[str, TemplateExpansion] |
22 | 45 | user: Optional[Union[str, TemplateExpansion]] = None |
23 | 46 | passwd: Optional[Union[str, TemplateExpansion]] = None |
24 | 47 | pkey: Optional[Union[str, TemplateExpansion]] = None |
25 | 48 | timeout: Union[int, TemplateExpansion] = 10 |
26 | 49 | port: Union[int, TemplateExpansion] = 22 |
27 | 50 | compress: Union[bool, TemplateExpansion] = False |
28 | | - config_path: Union[str, TemplateExpansion] = "~/.ssh/config" |
29 | 51 | path: Union[str, TemplateExpansion] |
30 | 52 |
|
31 | 53 |
|
32 | | -class SshFileSourceConfiguration(BaseFileSourceConfiguration): |
| 54 | +class SshFileSourceConfiguration(FsspecBaseFileSourceConfiguration): |
33 | 55 | host: str |
34 | 56 | user: Optional[str] = None |
35 | 57 | passwd: Optional[str] = None |
36 | 58 | pkey: Optional[str] = None |
37 | 59 | timeout: int = 10 |
38 | 60 | port: int = 22 |
39 | 61 | compress: bool = False |
40 | | - config_path: str = "~/.ssh/config" |
41 | 62 | path: str |
42 | 63 |
|
43 | 64 |
|
44 | | -class SshFilesSource(PyFilesystem2FilesSource[SshFileSourceTemplateConfiguration, SshFileSourceConfiguration]): |
| 65 | +class SshFilesSource(FsspecFilesSource[SshFileSourceTemplateConfiguration, SshFileSourceConfiguration]): |
45 | 66 | plugin_type = "ssh" |
46 | | - required_module = SSHFS |
47 | | - required_package = "fs.sshfs" |
| 67 | + required_module = SFTPFileSystem |
| 68 | + required_package = "fsspec" |
48 | 69 |
|
49 | 70 | template_config_class = SshFileSourceTemplateConfiguration |
50 | 71 | resolved_config_class = SshFileSourceConfiguration |
51 | 72 |
|
52 | | - def _open_fs(self, context: FilesSourceRuntimeContext[SshFileSourceConfiguration]): |
53 | | - if SSHFS is None: |
| 73 | + def _open_fs( |
| 74 | + self, |
| 75 | + context: FilesSourceRuntimeContext[SshFileSourceConfiguration], |
| 76 | + cache_options: CacheOptionsDictType, # Ignored because fsspec's SFTPFileSystem does not support caching options. |
| 77 | + ): |
| 78 | + if SFTPFileSystem is None: |
54 | 79 | raise self.required_package_exception |
55 | 80 |
|
56 | 81 | config = context.config |
57 | | - handle = SSHFS( |
| 82 | + pkey = None |
| 83 | + password = config.passwd |
| 84 | + if config.pkey: |
| 85 | + pkey = _parse_private_key(config.pkey, config.passwd) |
| 86 | + if pkey is None: |
| 87 | + raise AuthenticationFailed("Invalid or unsupported SSH private key provided.") |
| 88 | + password = None |
| 89 | + |
| 90 | + fs = SFTPFileSystem( |
58 | 91 | host=config.host, |
59 | | - user=config.user, |
60 | | - passwd=config.passwd, |
61 | | - pkey=config.pkey, |
| 92 | + username=config.user, |
| 93 | + password=password, |
| 94 | + pkey=pkey, |
62 | 95 | port=config.port, |
63 | 96 | timeout=config.timeout, |
64 | 97 | compress=config.compress, |
65 | | - config_path=config.config_path, |
66 | 98 | ) |
67 | | - return handle |
| 99 | + return fs |
68 | 100 |
|
69 | 101 | def _to_filesystem_path(self, path: str, config: SshFileSourceConfiguration) -> str: |
70 | 102 | base = config.path.rstrip("/") |
|
0 commit comments