Skip to content

Commit 2a4806b

Browse files
authored
Merge pull request galaxyproject#21372 from mvdbeek/debug_ascp
Various fixes around ascp file source
2 parents e68da17 + db7220c commit 2a4806b

6 files changed

Lines changed: 71 additions & 33 deletions

File tree

client/src/utils/url.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const URI_PREFIXES = [
3030
"dataverse://",
3131
"elabftw://",
3232
"zip://",
33+
"ascp://",
3334
];
3435

3536
export function isUrl(content: string): boolean {

doc/source/admin/data.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ Example configuration for EBI SRA downloads:
455455
-----BEGIN RSA PRIVATE KEY-----
456456
<YOUR ACTUAL SSH PRIVATE KEY CONTENT>
457457
-----END RSA PRIVATE KEY-----
458+
# SSH key passphrase. https://embl.service-now.com/kb?id=kb_article_view&sys_kb_id=4cc60cf8c398a610bf313dfc0501314c#mcetoc_1idpn4k0to
459+
ssh_key_passphrase: sample_passphrase
458460
```
459461
460462
The plugin is **download-only** and supports automatic retry with exponential backoff for transient

lib/galaxy/config/sample/file_sources_conf.yml.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
-----BEGIN RSA PRIVATE KEY-----
9292
<YOUR ACTUAL SSH PRIVATE KEY CONTENT>
9393
-----END RSA PRIVATE KEY-----
94+
# SSH key passphrase. https://embl.service-now.com/kb?id=kb_article_view&sys_kb_id=4cc60cf8c398a610bf313dfc0501314c#mcetoc_1idpn4k0to
95+
ssh_key_passphrase: sample_passphrase
9496
# Note: This plugin is download-only (writable: false, browsable: false)
9597

9698
# Example: Custom Aspera endpoint with encryption enabled

lib/galaxy/files/sources/ascp.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
"""
2424

2525
import logging
26-
from typing import Union
26+
from typing import (
27+
Optional,
28+
Union,
29+
)
2730

2831
from galaxy.files.models import (
2932
FilesSourceRuntimeContext,
@@ -40,7 +43,6 @@
4043
log = logging.getLogger(__name__)
4144

4245
PLUGIN_TYPE = "ascp"
43-
REQUIRED_PACKAGE = "ascp" # Note: This is the binary, not a Python package
4446

4547

4648
class AscpFilesSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfiguration):
@@ -57,6 +59,7 @@ class AscpFilesSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfigura
5759

5860
ascp_path: Union[str, TemplateExpansion] = "ascp"
5961
ssh_key_content: Union[str, TemplateExpansion] # SSH key content as string (required)
62+
ssh_key_passphrase: Union[str, TemplateExpansion, None] = None # Passphrase for the SSH key (required)
6063
user: Union[str, TemplateExpansion] # Required field
6164
host: Union[str, TemplateExpansion] # Required field
6265
rate_limit: Union[str, TemplateExpansion] = "300m"
@@ -81,6 +84,7 @@ class AscpFilesSourceConfiguration(FsspecBaseFileSourceConfiguration):
8184

8285
ascp_path: str = "ascp"
8386
ssh_key_content: str # SSH key content as string (required)
87+
ssh_key_passphrase: Optional[str] = None # Passphrase for the SSH key (optional)
8488
user: str # Required field
8589
host: str # Required field
8690
rate_limit: str = "300m"
@@ -129,7 +133,7 @@ class AscpFilesSource(FsspecFilesSource[AscpFilesSourceTemplateConfiguration, As
129133

130134
plugin_type = PLUGIN_TYPE
131135
required_module = AscpFileSystem
132-
required_package = REQUIRED_PACKAGE
136+
required_package = "fsspec" # Dummy requirement, need no external package
133137

134138
template_config_class = AscpFilesSourceTemplateConfiguration
135139
resolved_config_class = AscpFilesSourceConfiguration
@@ -158,6 +162,7 @@ def _open_fs(
158162
return AscpFileSystem(
159163
ascp_path=config.ascp_path,
160164
ssh_key=config.ssh_key_content,
165+
ssh_key_passphrase=config.ssh_key_passphrase,
161166
user=config.user,
162167
host=config.host,
163168
rate_limit=config.rate_limit,

lib/galaxy/files/sources/ascp_fsspec.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class AscpFileSystem(AbstractFileSystem):
5656
Args:
5757
ascp_path: Path to the ascp binary (default: "ascp")
5858
ssh_key: SSH private key content as a string (required)
59+
ssh_key_passphrase: SSH private key passphrase as a string
5960
user: Username for ascp connection (e.g., "era-fasp")
6061
host: Hostname (e.g., "fasp.sra.ebi.ac.uk")
6162
rate_limit: Transfer rate limit (default: "300m")
@@ -72,8 +73,9 @@ class AscpFileSystem(AbstractFileSystem):
7273

7374
def __init__(
7475
self,
76+
ssh_key: str,
77+
ssh_key_passphrase: Optional[str] = None,
7578
ascp_path: str = "ascp",
76-
ssh_key: Optional[str] = None,
7779
user: Optional[str] = None,
7880
host: Optional[str] = None,
7981
rate_limit: str = "300m",
@@ -99,6 +101,7 @@ def __init__(
99101
super().__init__(**kwargs)
100102
self.ascp_path = ascp_path
101103
self.ssh_key = ssh_key
104+
self.ssh_key_passphrase = ssh_key_passphrase
102105
self.user = user
103106
self.host = host
104107
self.rate_limit = rate_limit
@@ -118,7 +121,15 @@ def __init__(
118121
"Please ensure ascp is installed and accessible in your PATH."
119122
)
120123

121-
def _get_file(self, rpath: str, lpath: str, **kwargs: Any) -> None:
124+
def isdir(self, path):
125+
"""Is this entry directory-like?"""
126+
return False
127+
128+
def isfile(self, path):
129+
"""Is this entry file-like?"""
130+
return True
131+
132+
def get_file(self, rpath: str, lpath: str, **kwargs: Any) -> None:
122133
"""Download a file from remote path to local path using ascp with retry logic.
123134
124135
This method handles:
@@ -237,12 +248,18 @@ def _execute_ascp_transfer(self, rpath: str, lpath: str, attempt: int) -> None:
237248

238249
log.debug(f"Executing ascp command (key path hidden): {self._sanitize_cmd_for_log(cmd)}")
239250

251+
if self.ssh_key_passphrase:
252+
env = os.environ.copy()
253+
env["ASPERA_SCP_PASS"] = self.ssh_key_passphrase
254+
else:
255+
env = None
240256
# Execute ascp
241257
result = subprocess.run(
242258
cmd,
243259
capture_output=True,
244260
text=True,
245261
check=False, # We'll handle errors manually
262+
env=env,
246263
)
247264

248265
if result.returncode != 0:

0 commit comments

Comments
 (0)