docs(research): add russh and sftp-rs deep-dive references
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
# 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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
// 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
|
||||
```
|
||||
Reference in New Issue
Block a user