Skip to content

Filesystem paths

client.path(...) returns a pathapi Path object for a location on the target:

p = client.path("/mnt/tank/data/report.txt")
if p.exists():
    data = p.read_bytes()
p.with_name("report.bak").write_bytes(data)

Local vs remote backends

  • For a local target, client.path(...) is an ordinary local Path.
  • For a remote target it prefers SFTP (the ssh extra: asyncssh and sftppath) and falls back to the middleware filesystem.* websocket API when SFTP is not configured or cannot connect. SFTP runs over the same SSH connection as run(): one connection and one login per host.
  • A relative path, and client.path() with no segments, is rooted at / on a remote target, whichever leg serves it.
  • client.path(p, backend="tnasws") (or "sftp", "local") pins the path to that leg for every operation on it.
pip install pytruenas[ssh]     # enables the SFTP backend

Choose a provider for one call with client.path(..., backend="sftp"), or set the whole preference order at construction with path=["sftp", "tnasws"] — see Running commands. Valid path providers are local, sftp, and tnasws.

Examples

Paths behave like pathlib.Path, so most of what you know transfers:

p = client.path("/mnt/tank/config/app.conf")

p.read_text()
p.write_text("key = value\n")
p.exists(), p.is_file(), p.stat().st_size

# Parents, joining, and globbing all work
(p.parent / "backup.conf").write_bytes(p.read_bytes())
for conf in client.path("/mnt/tank/config").glob("*.conf"):
    print(conf.name, conf.stat().st_mtime)

Walking a tree and copying between hosts:

src = client.path("/mnt/tank/data")
for root, dirs, files in src.walk():
    for name in files:
        print(root / name)

# `pathapi` copies across backends, so this crosses machines
local_copy = pathlib.Path("/tmp/app.conf")
client.path("/mnt/tank/config/app.conf").copy(local_copy)

Creating directories and cleaning up:

d = client.path("/mnt/tank/scratch/run-42")
d.mkdir(parents=True, exist_ok=True)
(d / "log.txt").write_text("started\n")

d.rm(recursive=True, missing_ok=True)   # pytruenas convenience

What needs SFTP

The middleware filesystem.* API has no symlink or rename operation, so these need the ssh extra and a reachable SSH leg. Without one they raise NotImplementedError rather than failing silently:

operation sftp tnasws
read/write/stat/mkdir/chmod ✅ ✅
unlink/rmdir ✅ ✅ (shells out)
rename ✅ ❌
symlink_to / readlink ✅ ❌
resolve ✅ ❌ — returns the path unchanged

resolve() replaces every symlink in the path over SFTP. The middleware has no realpath, so on a host with no SFTP leg it returns the path unchanged.

On the tnasws leg the operations keep pathlib's contract: chmod() and chown() return once the middleware job behind them has finished and raise OSError when it failed; unlink() and rmdir() raise FileNotFoundError, IsADirectoryError, NotADirectoryError or OSError(ENOTEMPTY) as a local path would; stat() follows a symlink and is_symlink() sees the link itself.

symlink_to(target, force=True) (pathapi's) replaces a conflicting file or symlink first; a directory in the way is never removed, and raises FileExistsError:

link = client.path("/mnt/tank/current")
link.symlink_to("/mnt/tank/releases/v2", force=True)

On a host with no SFTP leg, no leg can create a symlink, so it raises NotImplementedError with the existing entry still in place — rather than deleting it and then failing.

Note

SFTP concurrency and transport details come from hostctl and sftppath; pytruenas only orders the providers. The SFTP leg verifies the server's host key (see Host keys); when it cannot connect, client.path() operations are served by the websocket leg instead. p / "name" joins a literal file name, so p / "cache?v=2" names the file cache?v=2 — the same path p.iterdir() lists for it.

See the Filesystem API.