Skip to content

Getting Started

Install

uv add smbfence

Python 3.13 or later.

Configure

Credentials are read from the environment, or from a secrets.env file, prefixed SMB_.

Variable Meaning Default
SMB_HOST Server hostname —
SMB_SHARE Share name —
SMB_USER Username —
SMB_PASSWORD Password, held as a SecretStr —
SMB_PORT Port 445
SMB_ALLOWED_PATHS Roots this process may read ()
# secrets.env — never commit this
SMB_HOST=fileserver.example.internal
SMB_SHARE=accounting
SMB_USER=svc-import
SMB_PASSWORD=...

The allowlist is not optional

A share with no configured roots can reach nothing. That is deliberate — see Allowlisting paths.

Check before you connect

is_configured answers "should anything be attempted at all", which a caller often needs before it has a connection to ask.

from smbfence import ShareConfig

config = ShareConfig()
if not config.is_configured:
    ...  # nothing to retry until a human supplies credentials

Read

from smbfence import SmbClientBackend, share_connection

with share_connection(config, SmbClientBackend(), allowed=["clients/acme"]) as share:
    names = share.list_files("clients/acme/bank", suffix=".sta")
    for name in names:
        data = share.read_file(f"clients/acme/bank/{name}")

The session is registered on entry and dropped on exit — see Failure modes.

Write

with share_connection(config, SmbClientBackend(), allowed=["clients/acme"]) as share:
    share.write_file("clients/acme/out/invoice-2026-001.pdf", pdf_bytes)

Written beside itself and renamed into place. A file already there with identical bytes is success; one with different bytes is left alone. See Atomic writes.

Handle the four errors

from smbfence import (
    ConflictingContentError,
    PathNotAllowedError,
    ShareConfigError,
    ShareUnreachableError,
)

try:
    with share_connection(config, SmbClientBackend(), allowed=roots) as share:
        share.write_file(path, content)
except ShareConfigError:
    ...  # nothing was attempted
except PathNotAllowedError:
    ...  # a bug in the caller, not a transient fault
except ShareUnreachableError:
    ...  # retry later; this is a fault, not an empty folder
except ConflictingContentError:
    ...  # somebody else's file is under that name

Test without a share

paths and names are pure. The client takes its SMB backend as a Protocol, so an in-memory fake is enough for the rest — see Architecture.