diff --git a/lib/galaxy/config/sample/file_sources_conf.yml.sample b/lib/galaxy/config/sample/file_sources_conf.yml.sample index cb17905922a9..d1f98378d0f6 100644 --- a/lib/galaxy/config/sample/file_sources_conf.yml.sample +++ b/lib/galaxy/config/sample/file_sources_conf.yml.sample @@ -127,6 +127,20 @@ config_path: "" port: 2222 +- type: smb + id: smb-example + label: "SMB Share Example" + doc: "Connect to a Windows/Samba SMB share" + host: "smb.example.com" + domain: "DOMAIN" + user: "username" + passwd: "password" + port: 445 + encrypt: false + share_access: "" + path: "share/folder" + writable: false + - type: s3fs label: My MinIO storage endpoint_url: "https://minio.usegalaxy.eu" diff --git a/lib/galaxy/dependencies/__init__.py b/lib/galaxy/dependencies/__init__.py index 021b84ad4c7d..ecba523715de 100644 --- a/lib/galaxy/dependencies/__init__.py +++ b/lib/galaxy/dependencies/__init__.py @@ -348,6 +348,9 @@ def check_huggingface_hub(self): def check_omero_py(self): return "omero" in self.file_sources + def check_smbprotocol(self): + return "smb" in self.file_sources + def strip_comment(line): # lifted from https://github.com/tox-dev/tox/commit/3c6b4f204e89852c4b7536b246a66d20be6d39ec diff --git a/lib/galaxy/dependencies/conditional-requirements.txt b/lib/galaxy/dependencies/conditional-requirements.txt index afc82730c22c..1feb4f579ed2 100644 --- a/lib/galaxy/dependencies/conditional-requirements.txt +++ b/lib/galaxy/dependencies/conditional-requirements.txt @@ -35,6 +35,7 @@ rspace-client>=2.6.1,<3 # type: rspace adlfs huggingface_hub omero-py #type: omero +smbprotocol #type: smb # Vault backend hvac diff --git a/lib/galaxy/files/sources/smb.py b/lib/galaxy/files/sources/smb.py new file mode 100644 index 000000000000..0fcd60f7c95b --- /dev/null +++ b/lib/galaxy/files/sources/smb.py @@ -0,0 +1,88 @@ +from typing import ( + Optional, + Union, +) + +# file source requires conditional dependency smbclient +try: + from fsspec.implementations.smb import SMBFileSystem +except ModuleNotFoundError: + SMBFileSystem = None + +from galaxy.files.models import FilesSourceRuntimeContext +from galaxy.files.sources._fsspec import ( + CacheOptionsDictType, + FsspecBaseFileSourceConfiguration, + FsspecBaseFileSourceTemplateConfiguration, + FsspecFilesSource, +) +from galaxy.util.config_templates import TemplateExpansion + + +class SmbFileSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfiguration): + domain: Optional[Union[str, TemplateExpansion]] = None + host: Union[str, TemplateExpansion] + user: Optional[Union[str, TemplateExpansion]] = None + passwd: Optional[Union[str, TemplateExpansion]] = None + port: Union[int, TemplateExpansion] = 445 + encrypt: Union[bool, TemplateExpansion] = False + share_access: Optional[Union[str, TemplateExpansion]] = None + + path: Union[str, TemplateExpansion] + + +class SmbFileSourceConfiguration(FsspecBaseFileSourceConfiguration): + domain: Optional[str] = None + host: str + user: Optional[str] = None + passwd: Optional[str] = None + port: int = 445 + encrypt: bool = False + share_access: Optional[str] = None + path: str + + +class SmbFilesSource(FsspecFilesSource[SmbFileSourceTemplateConfiguration, SmbFileSourceConfiguration]): + plugin_type = "smb" + required_module = SMBFileSystem + required_package = "fsspec" + + template_config_class = SmbFileSourceTemplateConfiguration + resolved_config_class = SmbFileSourceConfiguration + + def _open_fs( + self, + context: FilesSourceRuntimeContext[SmbFileSourceConfiguration], + cache_options: CacheOptionsDictType, + ): + cfg = context.config + # Build username with optional domain + username = f"{cfg.domain}\\{cfg.user}" if cfg.domain and cfg.user else cfg.user + # Determine share access: explicit config overrides, otherwise infer from writable flag + share_access = cfg.share_access + if share_access is None: + # Allow read access for other handles if we only read, otherwise exclusive + share_access = "r" if getattr(cfg, "writable", False) else "" + return SMBFileSystem( + host=cfg.host, + port=cfg.port, + username=username, + password=cfg.passwd, + encrypt=cfg.encrypt, + share_access=share_access, + ) + + def _to_filesystem_path(self, path: str) -> str: + base = self.template_config.path.rstrip("/") + rel = path.lstrip("/") + return f"{base}/{rel}" if rel else base or "/" + + def _adapt_entry_path(self, filesystem_path: str) -> str: + base = self.template_config.path.rstrip("/") + if base and filesystem_path.startswith(base): + vp = filesystem_path[len(base) :] + return vp if vp.startswith("/") else f"/{vp}" + return filesystem_path + + +__all__ = ("SmbFilesSource",) diff --git a/lib/galaxy/files/templates/examples/smb.yml b/lib/galaxy/files/templates/examples/smb.yml new file mode 100644 index 000000000000..4162a6a27a43 --- /dev/null +++ b/lib/galaxy/files/templates/examples/smb.yml @@ -0,0 +1,58 @@ +- id: smb + version: 0 + name: SMB Share + description: | + Connect to Windows/Samba SMB shares using the fsspec SMBFileSystem. + configuration: + type: smb + host: "{{ variables.host }}" + domain: "{{ variables.domain }}" + user: "{{ variables.user }}" + passwd: "{{ secrets.password }}" + port: "{{ variables.port }}" + encrypt: "{{ variables.encrypt }}" + + path: "{{ variables.path }}" + writable: "{{ variables.writable }}" + variables: + host: + label: SMB Host + type: string + help: Hostname or IP address of the SMB server. + user: + label: Username + type: string + optional: true + help: Username for authentication. + domain: + label: Domain + type: string + optional: true + help: Domain for the SMB server (e.g., INTRANET). + path: + label: Share Path + type: string + help: The share and directory path on the server, e.g., "share/folder". + port: + label: Port + type: integer + optional: true + default: 445 + help: SMB port (usually 445). + encrypt: + label: Encrypt + type: boolean + optional: true + default: false + help: Whether to force encryption. + writable: + label: Writable? + type: boolean + optional: true + default: false + help: Allow Galaxy to write to this SMB share. + secrets: + password: + label: Password + optional: true + help: Password for authentication. diff --git a/lib/galaxy/files/templates/models.py b/lib/galaxy/files/templates/models.py index ed9adc73f2d9..70f15ae16266 100644 --- a/lib/galaxy/files/templates/models.py +++ b/lib/galaxy/files/templates/models.py @@ -48,6 +48,7 @@ "dataverse", "huggingface", "omero", + "smb", ] @@ -155,6 +156,33 @@ class FtpFileSourceConfiguration(StrictModel): writable: bool = False +class SMBFileSourceTemplateConfiguration(StrictModel): + type: Literal["smb"] + host: Union[str, TemplateExpansion] + domain: Optional[Union[str, TemplateExpansion]] = None + user: Optional[Union[str, TemplateExpansion]] = None + passwd: Optional[Union[str, TemplateExpansion]] = None + port: Union[int, TemplateExpansion] = 445 + encrypt: Union[bool, TemplateExpansion] = False + path: Union[str, TemplateExpansion] + writable: Union[bool, TemplateExpansion] = False + template_start: Optional[str] = None + template_end: Optional[str] = None + + +class SMBFileSourceConfiguration(StrictModel): + type: Literal["smb"] + host: str + domain: Optional[str] = None + user: Optional[str] = None + passwd: Optional[str] = None + port: int = 445 + encrypt: bool = False + share_access: Optional[str] = None + path: str + writable: bool = False + + class AzureFileSourceTemplateConfiguration(StrictModel): type: Literal["azure"] account_name: Union[str, TemplateExpansion] @@ -369,6 +397,7 @@ class OmeroFileSourceConfiguration(StrictModel): DataverseFileSourceTemplateConfiguration, HuggingFaceFileSourceTemplateConfiguration, OmeroFileSourceTemplateConfiguration, + SMBFileSourceTemplateConfiguration, ], Field(discriminator="type"), ] @@ -391,6 +420,7 @@ class OmeroFileSourceConfiguration(StrictModel): DataverseFileSourceConfiguration, HuggingFaceFileSourceConfiguration, OmeroFileSourceConfiguration, + SMBFileSourceConfiguration, ], Field(discriminator="type"), ] @@ -471,6 +501,7 @@ def template_to_configuration( "dataverse": DataverseFileSourceConfiguration, "huggingface": HuggingFaceFileSourceConfiguration, "omero": OmeroFileSourceConfiguration, + "smb": SMBFileSourceConfiguration, }