Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/galaxy/config/sample/file_sources_conf.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/dependencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/dependencies/conditional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions lib/galaxy/files/sources/smb.py
Original file line number Diff line number Diff line change
@@ -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",)
58 changes: 58 additions & 0 deletions lib/galaxy/files/templates/examples/smb.yml
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions lib/galaxy/files/templates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"dataverse",
"huggingface",
"omero",
"smb",
]


Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -369,6 +397,7 @@ class OmeroFileSourceConfiguration(StrictModel):
DataverseFileSourceTemplateConfiguration,
HuggingFaceFileSourceTemplateConfiguration,
OmeroFileSourceTemplateConfiguration,
SMBFileSourceTemplateConfiguration,
],
Field(discriminator="type"),
]
Expand All @@ -391,6 +420,7 @@ class OmeroFileSourceConfiguration(StrictModel):
DataverseFileSourceConfiguration,
HuggingFaceFileSourceConfiguration,
OmeroFileSourceConfiguration,
SMBFileSourceConfiguration,
],
Field(discriminator="type"),
]
Expand Down Expand Up @@ -471,6 +501,7 @@ def template_to_configuration(
"dataverse": DataverseFileSourceConfiguration,
"huggingface": HuggingFaceFileSourceConfiguration,
"omero": OmeroFileSourceConfiguration,
"smb": SMBFileSourceConfiguration,
}


Expand Down
Loading