Filesystem paths
client.path(...) returns a pathlib_next
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 is a
TruenasPath, which prefers SFTP (via thesshextra'sasyncsshbackend) and falls back to the middlewarefilesystem.*websocket API for operations SFTP doesn't cover cleanly (delete/rename/symlink).
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)
# `pathlib_next` 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() is the one exception to "SFTP first": neither leg can do it.
pathlib_next's SftpPath implements no resolve and the middleware has no
realpath, so resolve() returns the path unchanged on every host,
whether or not SFTP is configured.
symlink_to takes a pytruenas-specific force= that removes a conflicting
target first — a bool, a file-type string, or a set of
"file"/"link"/"directory":
link = client.path("/mnt/tank/current")
link.symlink_to("/mnt/tank/releases/v2", force="link") # replace only a symlink
The SFTP leg is resolved before force= removes anything, so a host that
cannot create the link raises NotImplementedError with the existing target
still in place — rather than deleting it and then failing.
Note
SFTP concurrency and transport details come from pathlib_next; pytruenas
just selects and wires the backend. The ssh extra requires
pathlib_next[sftp-async] >= 0.9.0, < 0.10, and the core
pathlib_next[uri] >= 0.9.1 floor applies on top of it — symlink_to(force=)
is a 0.9.1 addition.
See the Filesystem API.