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 localPath. - For a remote target it prefers SFTP (the
sshextra: asyncssh andsftppath) and falls back to the middlewarefilesystem.*websocket API when SFTP is not configured or cannot connect. SFTP runs over the same SSH connection asrun(): 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.