SFTP backend¶
Connect to any SSH/SFTP server with paramiko.
"""SFTP backend — Connect to any SSH/SFTP server with paramiko.
Demonstrates:
- Configuring an SFTP backend via RegistryConfig
- File operations: write, read, list, copy, move, delete, delete_folder
- Streaming line iteration
- Escape hatch: unwrap() to access paramiko.SFTPClient
Prerequisites:
- pip install "remote-store[sftp]"
- An accessible SFTP server
Environment variables:
RS_SFTP_HOST SFTP hostname (required)
RS_SFTP_USER SSH username (required)
RS_SFTP_PASS SSH password
RS_SFTP_PORT SSH port (default: 22)
RS_SFTP_BASE Base path on the server (default: /)
---
see_also:
- label: SFTP Backend
url: ../../guides/backends/sftp.md
note: backend guide
"""
from __future__ import annotations
import os
import sys
from remote_store import BackendConfig, Registry, RegistryConfig, StoreProfile
from remote_store.backends import SFTPUtils
HostKeyPolicy = SFTPUtils.HostKeyPolicy
HOST = os.environ.get("RS_SFTP_HOST", "")
USER = os.environ.get("RS_SFTP_USER", "")
if not HOST or not USER:
print(
"Set RS_SFTP_HOST and RS_SFTP_USER to run this example.\n"
"Optional: RS_SFTP_PASS, RS_SFTP_PORT, RS_SFTP_BASE\n\n"
"Example with a local Docker SFTP server:\n"
" RS_SFTP_HOST=localhost RS_SFTP_USER=benchuser RS_SFTP_PASS=benchpass "
"RS_SFTP_PORT=2222 RS_SFTP_BASE=/upload python examples/sftp_backend.py"
)
sys.exit(1)
if __name__ == "__main__":
# --- Build options from environment ---
options: dict[str, object] = {
"host": HOST,
"username": USER,
}
if val := os.environ.get("RS_SFTP_PASS"):
options["password"] = val
if val := os.environ.get("RS_SFTP_PORT"):
options["port"] = int(val)
if val := os.environ.get("RS_SFTP_BASE"):
options["base_path"] = val
# host_key_policy is an enum — it works in Python config-as-code (below)
# but cannot be serialized to JSON/TOML. When loading from a file, use
# direct backend construction instead (see commented section at the end).
# The default is STRICT; for dev/testing with unknown hosts use AUTO_ADD.
options["host_key_policy"] = HostKeyPolicy.AUTO_ADD
config = RegistryConfig(
backends={"sftp": BackendConfig(type="sftp", options=options)},
stores={"files": StoreProfile(backend="sftp", root_path="example")},
)
with Registry(config) as registry:
store = registry.get_store("files")
# --- Write ---
store.write("readme.txt", b"Hello from SFTP!\n")
store.write("data/report.csv", b"col1,col2\n10,20\n30,40\n")
print("Wrote 2 files.")
# --- Read ---
content = store.read_bytes("readme.txt")
print(f"\nreadme.txt: {content.decode().strip()}")
# --- Metadata ---
info = store.get_file_info("readme.txt")
print(f"readme.txt size={info.size} modified={info.modified_at}")
# --- List files (recursive) ---
print("\nAll files (recursive):")
for f in store.list_files("", recursive=True):
print(f" {f.path} ({f.size} bytes)")
# --- List folders ---
print("\nFolders:")
for folder in store.list_folders(""):
print(f" {folder.name}/")
# --- Folder info ---
folder_info = store.get_folder_info("data")
print(f"\ndata/ totals: {folder_info.file_count} files, {folder_info.total_size} bytes")
# --- Copy (SFTP copy = read + write, no server-side copy) ---
store.copy("readme.txt", "readme_backup.txt")
print(f"\nCopied readme.txt -> readme_backup.txt (exists: {store.exists('readme_backup.txt')})")
# --- Move (uses posix_rename with fallback) ---
store.move("readme_backup.txt", "archive/readme_old.txt")
print(f"Moved to archive/readme_old.txt (original exists: {store.exists('readme_backup.txt')})")
# --- Streaming line iteration ---
reader = store.read("data/report.csv")
print("\nStreaming read (line by line):")
newline = b"\n"
for line in reader:
text = line.rstrip(newline).decode()
if text:
print(f" {text}")
# --- Delete files and folders ---
for f in store.list_files("", recursive=True):
store.delete(str(f.path))
store.delete_folder("data")
store.delete_folder("archive")
print("\nCleaned up all example files and folders.")
# --- Escape hatch: unwrap() via direct construction ---
# Construct the backend directly to access paramiko.SFTPClient and to
# set host_key_policy for dev/testing environments.
from remote_store.backends import SFTPBackend, SFTPUtils
HostKeyPolicy = SFTPUtils.HostKeyPolicy
backend = SFTPBackend(
host=HOST,
port=int(os.environ.get("RS_SFTP_PORT", "22")),
username=USER,
password=os.environ.get("RS_SFTP_PASS"),
base_path=os.environ.get("RS_SFTP_BASE", "/"),
host_key_policy=HostKeyPolicy.AUTO_ADD, # Dev/testing only!
)
try:
import paramiko
sftp_client = backend.unwrap(paramiko.SFTPClient)
print(f"\nparamiko SFTPClient: {type(sftp_client).__name__}")
finally:
backend.close()
# --- Host key policies (commented out) ---
#
# HostKeyPolicy = SFTPUtils.HostKeyPolicy
#
# # STRICT (default) — reject unknown hosts; key must be in known_hosts
# backend = SFTPBackend(host="...", username="...", password="...",
# host_key_policy=HostKeyPolicy.STRICT)
#
# # TRUST_ON_FIRST_USE — accept and save on first connect, verify after
# backend = SFTPBackend(host="...", username="...", password="...",
# host_key_policy=HostKeyPolicy.TRUST_ON_FIRST_USE)
#
# # AUTO_ADD — accept any key silently (dev/testing only)
# backend = SFTPBackend(host="...", username="...", password="...",
# host_key_policy=HostKeyPolicy.AUTO_ADD)
# --- Key-based authentication (commented out) ---
#
# load_private_key = SFTPUtils.load_private_key
#
# # From a file
# pkey = load_private_key("/path/to/id_rsa", from_file=True)
# backend = SFTPBackend(host="...", username="...", pkey=pkey)
#
# # From a PEM string (e.g. from a secrets manager)
# pkey = load_private_key(pem_string)
# backend = SFTPBackend(host="...", username="...", pkey=pkey)
print("\nDone!")
See also¶
- SFTP Backend — backend guide
- Source:
examples/backends/sftp_backend.py