SFTP paths
sftppath
pathlib-style paths for SFTP servers, on paramiko or asyncssh.
SFTPPath needs no URI and imports neither SSH library until a backend is
built: SFTPBackend (paramiko) and AsyncSSHBackend (asyncssh) are resolved
on first use (PEP 562), so import sftppath needs neither extra and never
imports uripath; naming one whose library is missing raises an ImportError
that names the extra. The sftp: scheme of uripath is sftppath.uri.
DEFAULT_PORT = 22
module-attribute
The port a connection uses when neither the caller nor the ssh_config of the host names one.
DEFAULT_SSH_CONFIG = _Sentinel(__name__, 'DEFAULT_SSH_CONFIG')
module-attribute
SFTPPath(*segments, backend=None, host=None, port=None, user=None, password=None, ssh_config=DEFAULT_SSH_CONFIG, timeout=UNSET, connect_opts=None, **options)
Bases: TreePath
A path on an SFTP server, over paramiko or asyncssh (see "backend
selection" in _select). A request needs the paramiko or the asyncssh
extra; naming, joining and comparing paths needs neither.
Also implements pathapi.protocols.checksum.NativeChecksum
(checksum(), delegating to self.backend.checksum()) -- native on both
backends via the filexfer draft's check-file-handle extension where the
server implements it (OpenSSH does not), NotImplementedError (falls back
to streaming) on a server without that extension.
SFTPPath("/srv/x", host="h", user="u") builds the path's own backend
from the endpoint keywords and the connect options (ssh_config=,
timeout=, connect_opts=, and the keywords of the backend class that
SFTPPATH_BACKEND selects); backend= instead shares one connection
between paths built separately, or plugs in a BaseSFTPBackend of your
own. Giving backend= together with any of those keywords is a
TypeError, and so is giving neither (unless a segment is an SFTPPath,
whose backend is kept). Nothing connects when a path is built. Every path
derived from one (/, joinpath, parent, with_name, iterdir,
relative_to) keeps its backend. Names are read from the server root:
srv/x and /srv/x are one file.
Source code in src/sftppath/_path.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
backend
property
The backend (SFTPBackend, AsyncSSHBackend or a custom
BaseSFTPBackend) holding the connections of this path.
as_uri()
Source code in src/sftppath/_path.py
168 169 170 171 172 173 174 175 176 177 178 | |
checksum(algorithm='md5')
protocols.checksum.NativeChecksum implementation: delegates to
self.backend.checksum() (the check-file-handle SFTP extension
on both backends -- see BaseSFTPBackend.checksum). Any failure
that isn't already
NotImplementedError (a server that doesn't advertise the
extension, an unsupported algorithm, a transport-level error) is
also translated to NotImplementedError: this method's whole
contract is "raise if a genuine digest can't be produced", and a
caller (e.g. PathSyncer) must be able to fall back to streaming
on ANY such failure, not just the backend's own explicit signal.
Source code in src/sftppath/_path.py
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | |
chmod(mode, *, follow_symlinks=True)
Source code in src/sftppath/_path.py
343 344 345 346 347 348 349 | |
copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)
Copy with concurrent fan-out on the asyncssh backend.
When using the asyncssh backend with recursive=True on a
directory, child copies are fanned out as concurrent native requests,
bounded by backend.max_concurrency (requests in flight, and files
open at once); the whole copy has no wall-clock timeout. progress
is honored on the generic single-file path below, but not called
during the concurrent native fan-out itself.
on_error(error, path) is Path.copy()'s predicate; the native
fan-out calls it from a worker thread, once per failing child.
Source code in src/sftppath/_path.py
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 | |
hardlink_to(target)
Source code in src/sftppath/_path.py
582 583 584 585 586 587 588 589 590 591 592 593 | |
is_local()
Whether the server is this machine, as far as its name says: a
loopback or unspecified address, or localhost. A host name is not
resolved, and a backend with no host counts as local. PathSyncer
skips its quick_check for two local sides.
Source code in src/sftppath/_path.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
iterdir()
Source code in src/sftppath/_path.py
223 224 225 | |
readlink()
Source code in src/sftppath/_path.py
574 575 576 | |
rename(target)
Source code in src/sftppath/_path.py
458 459 460 461 462 463 464 465 466 467 468 469 | |
replace(target)
Rename this entry to target, replacing what is there, and return
target as a path.
Onto a path of this server the request is posix-rename@openssh.com
where the server has it: one request, atomic, and if it fails both
entries are as they were. A server that lacks the extension, or that
refuses it without saying why, gets what Path.replace() does: the
file at target is removed and then this entry is renamed. Onto
anything else this is Path.replace(), which move(overwrite=True)
uses too.
Source code in src/sftppath/_path.py
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 | |
rm(recursive=False, missing_ok=False, ignore_error=False, *, follow_symlinks=False, follow_binds=False)
Source code in src/sftppath/_path.py
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 | |
rmdir()
Source code in src/sftppath/_path.py
449 450 451 452 453 454 455 456 | |
stat(*, follow_symlinks=True)
Source code in src/sftppath/_path.py
304 305 306 307 308 | |
supported_checksums()
protocols.checksum.NativeChecksum implementation: delegates to
self.backend.supported_checksums(self) -- a real per-connection
probe on both backends (see
_checkfile.CheckFileSFTPBackend.supported_checksums), so this
is empty when the connected server doesn't implement
check-file-handle (OpenSSH never does).
Source code in src/sftppath/_path.py
403 404 405 406 407 408 409 410 411 | |
unlink(missing_ok=False)
Source code in src/sftppath/_path.py
434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
SFTPEndpoint
Bases: NamedTuple
The server and account a backend connects to: what two backends must
share for their paths to name the same files. The password is not part of
it, and the host name is in lower case. port is None when the caller
named none (the ssh_config Port of the host, else 22, applies).
host
instance-attribute
port
instance-attribute
user
instance-attribute
BaseSFTPBackend
Bases: object
Contract for obtaining a ready-to-use, paramiko-shaped SFTPClient for
one server. Subclass this to plug in custom connection handling (tests
mock it directly, no real server); SFTPBackend (paramiko) and
AsyncSSHBackend (asyncssh) are the real implementations. Connection
caching is each backend's own responsibility: client() is expected to
return an already-cached-or-freshly-opened client, and SFTPPath does no
per-backend branching anywhere.
endpoint, when a subclass sets it, is a hashable value naming the server
and account: two backends with equal endpoints serve the same files.
A context manager: leaving the block calls close().
endpoint = None
class-attribute
instance-attribute
supports_hardlink = False
class-attribute
instance-attribute
supports_lchmod = False
class-attribute
instance-attribute
supports_tree = False
class-attribute
instance-attribute
checksum(path, algorithm)
Backend-native digest for path's content, e.g. via the
filexfer draft's check-file-handle SFTP extension (implemented by
some servers, e.g. ProFTPD's mod_sftp; OpenSSH has no such
extension). Raises
NotImplementedError (the base/default here) when the backend has
no such capability at all, and MUST also raise it -- not return a
value -- when the server doesn't advertise algorithm specifically
(see pathapi.protocols.checksum.NativeChecksum.checksum for
why this is a hard contract, not a style choice). Both real
backends implement it through
_checkfile.CheckFileSFTPBackend, each supplying only the extended
request itself.
Source code in src/sftppath/_backend.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
client()
A ready-to-use SFTP client for this backend's server, cached or newly opened.
Source code in src/sftppath/_backend.py
101 102 103 104 | |
close()
Closes every connection this backend has cached, whichever thread
opened it, and returns when they are closed. A no-op here; both real
backends override it. Harmless when called again; the backend stays
usable, and the next client() call reconnects.
Source code in src/sftppath/_backend.py
106 107 108 109 110 | |
supported_checksums(path)
Advisory set of algorithm names checksum() can currently
produce against path's server connection (see
pathapi.protocols.checksum.NativeChecksum.supported_checksums).
Empty here (the default): no native-hashing support at all. Both real
backends override this with a per-connection probe against path
(neither library exposes the server's extension list -- see
_checkfile.CheckFileSFTPBackend.supported_checksums). Takes a
path argument (unlike a bare capability flag) because the only
reliable way to know is to actually try the extension against a
real file.
Source code in src/sftppath/_backend.py
87 88 89 90 91 92 93 94 95 96 97 98 99 | |
tree_copy(path, target, *, overwrite, follow_symlinks, preserve_metadata, ignore_error, on_error=None)
Copy the directory path into the existing target directory,
on the server both are on. Only called when supports_tree.
on_error(error, path), when given, decides whether a failure of one
child is skipped; ignore_error is then None.
Source code in src/sftppath/_backend.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
tree_rm(path, *, missing_ok, on_error)
Remove path and everything below it, never following a link.
on_error(error, path) decides whether a failure is skipped. Only
called when supports_tree.
Source code in src/sftppath/_backend.py
81 82 83 84 85 | |
CheckFileSFTPBackend
Bases: BaseSFTPBackend
A BaseSFTPBackend whose checksum()/supported_checksums() speak
check-file-handle. A subclass supplies _check_file_request(); the
handle is opened (and always closed) through its client's paramiko-
shaped open().
checksum(path, algorithm)
Server-side digest of path through check-file-handle. A server
that answers "operation unsupported" for algorithm is not asked for
it again on this connection (an OpenSSH server costs one request per
algorithm, not one per file); other algorithms are still asked for.
Other failures propagate as raised, and refuse nothing;
SFTPPath.checksum() translates them to NotImplementedError.
Source code in src/sftppath/_checkfile.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
supported_checksums(path)
Per-connection probe: try checksum() against path once and
remember the answer for this connected client. path must already
exist and be readable, or the probe's own open() fails for an
unrelated reason (a missing file), which is reported as empty here
but not remembered -- this method is advisory, never raises.
Only the FIRST algorithm is actually attempted: a server either
implements the extension (and then the draft's algorithm set) or it
doesn't. Once any digest has been produced the answer is the draft's
set less the algorithms the server refused; before that it is empty,
and checksum() still asks for any algorithm not refused.
Source code in src/sftppath/_checkfile.py
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
sftppath.SFTPBackend(host, port=None, *, user=None, password=None, connect_opts=None, hostkeypolicy=None, ssh_config=DEFAULT_SSH_CONFIG, known_hosts=_DEFAULT_KNOWN_HOSTS, timeout=DEFAULT_TIMEOUT)
Bases: CheckFileSFTPBackend
The connections to one SFTP server and account, on paramiko.SSHClient:
connect_opts merged with host, port, user and password.
Nothing connects when the backend is built. client() caches per
(self, calling thread) -- see _CACHED_CLIENTS above -- and replaces
(and closes) a cached client whose connection has dropped; close()
closes every connection the backend opened, on any thread, and is
harmless when called again.
host may be a name from the user's ssh_config: HostName, Port,
User and IdentityFile of the matching block apply where the caller
named no port, user or key. Host keys are verified by default.
known_hosts (default: the user's ~/.ssh/known_hosts plus any
ssh_config UserKnownHostsFile for the host; None loads no file; a path
or iterable of paths loads exactly those) is loaded read-only, and
hostkeypolicy (default paramiko.RejectPolicy()) decides what happens
to a key found in none of them. A key that differs from a known one
always raises SFTPHostKeyError, as one the policy refuses does.
Opt-out, in code only:
SFTPBackend(host, hostkeypolicy=paramiko.AutoAddPolicy(), known_hosts=None)
accepts any server key -- a network man-in-the-middle then receives the
password.
timeout (default 30 s) is the default for paramiko's timeout,
banner_timeout, auth_timeout and channel_timeout connect options;
a value in connect_opts wins, and timeout=None leaves them unset.
Individual SFTP requests on an established connection are not bounded.
ssh_config support is paramiko's own (HostName, Port, User,
IdentityFile, ProxyCommand) plus Include. ProxyJump is not
supported: a host whose config sets it raises NotImplementedError
unless connect_opts supplies a sock -- use the asyncssh backend, or
an equivalent ProxyCommand.
Source code in src/sftppath/_paramiko.py
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | |
DEFAULT_TIMEOUT = 30.0
class-attribute
instance-attribute
__slots__ = ('host', 'port', 'user', 'password', 'connect_opts', 'hostkeypolicy', 'ssh_config', 'known_hosts', 'timeout')
class-attribute
instance-attribute
connect_opts = {} if connect_opts is None else connect_opts
instance-attribute
endpoint
property
host = str(host)
instance-attribute
hostkeypolicy = _paramiko.RejectPolicy() if hostkeypolicy is None else hostkeypolicy
instance-attribute
known_hosts = known_hosts
instance-attribute
password = password
instance-attribute
port = None if port is None else int(port)
instance-attribute
ssh_config = ssh_config
instance-attribute
timeout = timeout
instance-attribute
user = user
instance-attribute
__reduce__()
Source code in src/sftppath/_paramiko.py
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | |
__repr__()
Source code in src/sftppath/_paramiko.py
441 442 443 444 445 446 447 | |
client()
Source code in src/sftppath/_paramiko.py
560 561 562 563 564 565 566 567 | |
close()
Close every cached connection this backend opened, on any thread.
Source code in src/sftppath/_paramiko.py
569 570 571 572 573 | |
opts()
The keyword arguments SSHClient.connect() is called with: the
connect_opts merged with the ssh_config of the host (a
ProxyCommand starts its process here).
Source code in src/sftppath/_paramiko.py
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 | |
transport()
Connects and returns the paramiko.Transport.
Source code in src/sftppath/_paramiko.py
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | |
sftppath.AsyncSSHBackend(host, port=None, *, user=None, password=None, connect_opts=None, max_concurrency=None, sftp_version=4, ssh_config=DEFAULT_SSH_CONFIG, timeout=_bridge._DEFAULT_TIMEOUT)
Bases: CheckFileSFTPBackend
The connections to one SFTP server and account, on asyncssh instead
of paramiko. Selected automatically when asyncssh is importable (see
backend selection in SFTPPath), or explicitly via
backend=AsyncSSHBackend(host) / SFTPPATH_BACKEND=asyncssh. Nothing
connects when the backend is built. The connection is cached per backend
(see _ConnectionCache) and served through a single shared background
asyncio loop (see _run) -- not fork-safe (a fork()ed child inherits a
dead loop thread; detected via stored PID, loop+cache lazily recreated
when os.getpid() changes). close() closes every connection the backend
opened, returns when they are closed, and is harmless when called again.
host may be a name from the user's ssh_config: asyncssh applies the
matching block where the caller named no port, user or key. Host keys
are verified by default: asyncssh checks the server key against
~/.ssh/known_hosts and the ssh_config's UserKnownHostsFile, and an
unknown or changed key raises SFTPHostKeyError. Opt-out, in code
only: AsyncSSHBackend(host, connect_opts={"known_hosts": None}) accepts
any server key -- a network man-in-the-middle then receives the password.
Any other asyncssh known_hosts value (a file, a list of keys) is passed
through as given.
timeout (default 60 s) bounds a single request -- connect, stat, open,
mkdir, rename, ... -- and a timed-out request is cancelled and raises
TimeoutError. Recursive copy()/rm(), streaming file reads/writes
(read_bytes(), write_bytes(), chunked copies) and a native checksum()
(the server hashes the whole file) have no wall-clock bound; asyncssh's own
connect_timeout, login_timeout and keepalive_interval connect options
detect a dead peer there. timeout=None disables the per-request bound
too.
Source code in src/sftppath/_asyncssh/_backend.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
DEFAULT_MAX_CONCURRENCY = 16
class-attribute
instance-attribute
__slots__ = ('host', 'port', 'user', 'password', 'connect_opts', 'max_concurrency', 'sftp_version', 'timeout')
class-attribute
instance-attribute
connect_opts = {} if connect_opts is None else dict(connect_opts)
instance-attribute
endpoint
property
host = str(host)
instance-attribute
max_concurrency = max_concurrency
instance-attribute
password = password
instance-attribute
port = None if port is None else int(port)
instance-attribute
sftp_version = sftp_version
instance-attribute
supports_hardlink = True
class-attribute
instance-attribute
supports_lchmod = True
class-attribute
instance-attribute
supports_tree = True
class-attribute
instance-attribute
timeout = timeout
instance-attribute
user = user
instance-attribute
__reduce__()
Source code in src/sftppath/_asyncssh/_backend.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
__repr__()
Source code in src/sftppath/_asyncssh/_backend.py
115 116 117 118 119 120 121 | |
client()
Source code in src/sftppath/_asyncssh/_backend.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
close()
Close every cached connection this backend opened.
Source code in src/sftppath/_asyncssh/_backend.py
163 164 165 | |
tree_copy(path, target, *, overwrite, follow_symlinks, preserve_metadata, ignore_error, on_error=None)
Source code in src/sftppath/_asyncssh/_backend.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
tree_rm(path, *, missing_ok, on_error)
Source code in src/sftppath/_asyncssh/_backend.py
176 177 178 179 180 181 182 183 184 185 186 | |