13 KiB
13 KiB
sftp-rs: Data Flow and Examples
File Download Flow (Sync Client)
The most common SFTP operation — reading a file from the server:
Client Server
│ │
│ SSH_FXP_OPEN [req_id=1] [path] [flags] [attrs] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_HANDLE [req_id=1] [handle_bytes] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_READ [req_id=2] [handle] [offset=0] [len=32K]
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_DATA [req_id=2] [data_bytes] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_READ [req_id=3] [handle] [offset=N] [len=32K]
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [req_id=3] [SSH_FX_EOF] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_CLOSE [req_id=4] [handle] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [req_id=4] [SSH_FX_OK] │
│←───────────────────────────────────────────────│
Sync Code Path
let file = client.open("/remote/file", OpenOptions::new().read(true), &Attributes::new())?;
let mut offset: u64 = 0;
const CHUNK: u32 = 32 * 1024;
loop {
match client.pread(&file, offset, CHUNK) {
Ok(data) if data.is_empty() => break,
Ok(data) => { /* write data locally */ offset += data.len() as u64; }
Err(Error::Eof(_, _)) => break,
Err(e) => return Err(e),
}
}
client.fclose(&file)?;
Async Code Path
let file = client.open("/remote/file", OpenOptions::new().read(true), &Attributes::new()).await?;
let mut offset: u64 = 0;
const CHUNK: u32 = 32 * 1024;
loop {
match client.pread(&file, offset, CHUNK).await {
Ok(data) if data.is_empty() => break,
Ok(data) => { /* write data locally */ offset += data.len() as u64; }
Err(Error::Eof(_, _)) => break,
Err(e) => return Err(e),
}
}
client.fclose(&file).await?;
File Upload Flow
Client Server
│ │
│ SSH_FXP_OPEN [path] [WRITE|CREAT|TRUNC] [attrs]│
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_HANDLE [handle] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_WRITE [handle] [offset=0] [data] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [SSH_FX_OK] │
│←───────────────────────────────────────────────│
│ │
│ ... (repeat for each chunk) ... │
│ │
│ SSH_FXP_CLOSE [handle] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [SSH_FX_OK] │
│←───────────────────────────────────────────────│
Directory Listing Flow
Client Server
│ │
│ SSH_FXP_OPENDIR [path] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_HANDLE [dir_handle] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_READDIR [dir_handle] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_NAME [count] [name+longname+attrs] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_READDIR [dir_handle] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [SSH_FX_EOF] │
│←───────────────────────────────────────────────│
│ │
│ SSH_FXP_CLOSE [dir_handle] │
│───────────────────────────────────────────────→│
│ │
│ SSH_FXP_STATUS [SSH_FX_OK] │
│←───────────────────────────────────────────────│
Async Pipelining Flow
The async client can send multiple requests before any responses arrive:
Task 1 Task 2 Task 3 Reader Task
│ │ │ │
│ process() │ process() │ process() │
│ req_id=1 │ req_id=2 │ req_id=3 │
│ insert tx1 │ insert tx2 │ insert tx3 │
│ write pkt │ write pkt │ write pkt │
│ await rx1 │ await rx2 │ await rx3 │
│ │ │ │ read pkt
│ │ │ │ req_id=2 → tx2
│ │ │ │
│ │ rx2 resolved │ │
│ │ returns ───────│ │
│ │ │ │ read pkt
│ │ │ │ req_id=1 → tx1
│ rx1 resolved │ │ │
│ returns ─────│ │ │
│ │ │ │ read pkt
│ │ │ │ req_id=3 → tx3
│ │ │ rx3 resolved │
│ │ │ returns ───────│
russh Integration Flow
┌──────────────────────────────────────────────────────────┐
│ Application Code │
│ │
│ // 1. Establish SSH session │
│ let session = /* russh client session setup */; │
│ │
│ // 2. Open a channel │
│ let channel = session.channel_open_session().await?; │
│ │
│ // 3. Request SFTP subsystem + SFTP handshake │
│ let sftp = sftp::russh::from_channel(channel).await?; │
│ │
│ // 4. Use SFTP │
│ let file = sftp.open("/path", opts, &attrs).await?; │
│ let data = sftp.pread(&file, 0, 4096).await?; │
│ sftp.fclose(&file).await?; │
└──────────────────────────────────────────────────────────┘
Expanded step 3:
from_channel(channel)
│
├─ channel.request_subsystem(true, "sftp")
│ └─ SSH_MSG_CHANNEL_REQUEST subsystem=sftp
│ └─ SSH_MSG_CHANNEL_SUCCESS
│
├─ channel.into_stream() → ChannelStream<Msg>
│
├─ tokio::io::split(stream) → (read_half, write_half)
│
└─ AsyncSftpClient::new(read_half, write_half)
├─ write SSH_FXP_INIT version=3
├─ read SSH_FXP_VERSION
├─ parse version + extensions
└─ spawn reader task
Wire Format: build_open Example
Opening a file /hello.txt for read:
let opts = OpenOptions::new().read(true);
let body = build_open("/hello.txt", opts.get(), &Attributes::new())?;
// body layout:
// [0x00 0x00 0x00 0x0A] path length = 10
// [0x2F 0x68 0x65 0x6C 0x6C 0x6F 0x2E 0x74 0x78 0x74] "/hello.txt"
// [0x00 0x00 0x00 0x01] flags = SFTP_FLAG_READ
// [0x00 0x00 0x00 0x00] attrs (empty: valid_attribute_flags = 0)
Full packet on wire (with request-id = 42):
[0x00 0x00 0x00 0x19] length = 25 (1 type + 4 req_id + 10 path + 4 flags + 4 flags + 4 attrs_len)
[0x03] type = SSH_FXP_OPEN
[0x00 0x00 0x00 0x2A] request_id = 42
[0x00 0x00 0x00 0x0A] path length = 10
"/hello.txt" path
[0x00 0x00 0x00 0x01] open flags = READ
[0x00 0x00 0x00 0x00] attrs valid_flags = 0 (no attributes)
Wire Format: build_pread Example
Reading 1024 bytes from offset 4096:
let body = build_pread(&handle, 4096, 1024);
// body layout:
// [0x00 0x00 0x00 0x04] handle length = 4
// [0x48 0x41 0x4E 0x44] handle = "HAND"
// [0x00 0x00 0x00 0x00 0x00 0x00 0x10 0x00] offset = 4096
// [0x00 0x00 0x04 0x00] length = 1024
Wire Format: SSH_FXP_STATUS Response
Server returns SSH_FX_NO_SUCH_FILE:
[0x00 0x00 0x00 0x2A] request_id = 42 (matching the request)
[0x00 0x00 0x00 0x02] status = SSH_FX_NO_SUCH_FILE
[0x00 0x00 0x00 0x0F] error message length = 15
"No such file" error message
[0x00 0x00 0x00 0x02] language tag length = 2
"en" language tag
The parse_status() function maps this to Error::NoSuchFile("No such file".into(), "en".into()).
Error Handling Pattern
Both sync and async clients follow the same pattern for response handling:
// For operations that expect a handle (open, opendir):
let (cmd, data) = self.process(SSH_FXP_OPEN, &body).await?;
Ok(File(expect_handle(cmd, &data)?))
// If server sends SSH_FXP_STATUS instead of SSH_FXP_HANDLE,
// expect_handle() converts the status error to the appropriate Error variant.
// For operations that expect only status (mkdir, remove, close):
let (cmd, data) = self.process(SSH_FXP_MKDIR, &body).await?;
expect_status(cmd, &data)
// If server sends anything other than SSH_FXP_STATUS, returns unexpected response error.
// For operations that expect data (read):
let (cmd, data) = self.process(SSH_FXP_READ, &body).await?;
expect_data(cmd, &data)
// SSH_FXP_DATA → returns the bytes
// SSH_FXP_STATUS with SSH_FX_EOF → Error::Eof (used to signal end-of-file)
// SSH_FXP_STATUS with other codes → appropriate Error variant