docs(research): add russh-sftp deep-dive reference
This commit is contained in:
216
docs/research/references/ssh/russh-sftp/07-quick-reference.md
Normal file
216
docs/research/references/ssh/russh-sftp/07-quick-reference.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# russh-sftp: Quick Reference
|
||||
|
||||
## Crate Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Version | 2.3.0 |
|
||||
| License | Apache-2.0 |
|
||||
| Protocol | SFTP v3 (draft-ietf-secsh-filexfer-02) |
|
||||
| Min Rust | 2021 edition |
|
||||
| WASM | Client works on wasm32; server not supported |
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Feature | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `async-trait` | ❌ | Enables `#[async_trait]` on Handler traits |
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
`tokio` (io-util, rt, sync, time, macros), `tokio-util`, `serde`, `serde_bytes`, `bitflags`, `bytes`, `dashmap`, `chrono`, `thiserror`, `log`
|
||||
|
||||
## Public Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `client` | Client-side: `RawSftpSession`, `SftpSession`, `Handler`, `Config`, `fs::File`, `fs::ReadDir`, `fs::DirEntry` |
|
||||
| `server` | Server-side: `Handler`, `StatusReply`, `Config`, `run()`, `run_with_config()` |
|
||||
| `protocol` | All SFTP packet types, `Packet` enum, `StatusCode`, `OpenFlags`, `FileAttributes`, `File`, etc. |
|
||||
| `extensions` | OpenSSH extensions: `LimitsExtension`, `HardlinkExtension`, `FsyncExtension`, `StatvfsExtension`, `Statvfs` |
|
||||
| `de` | `from_bytes()` — public deserialization function for extension data |
|
||||
| `ser` | `to_bytes()` — public serialization function |
|
||||
|
||||
## Client Quick Start
|
||||
|
||||
```rust
|
||||
use russh_sftp::client::SftpSession;
|
||||
use russh_sftp::protocol::OpenFlags;
|
||||
|
||||
// Connect (using any AsyncRead+AsyncWrite stream)
|
||||
let sftp = SftpSession::new(stream).await?;
|
||||
|
||||
// Or with config:
|
||||
let sftp = SftpSession::new_with_config(stream, Config {
|
||||
max_packet_len: 262144,
|
||||
max_concurrent_writes: 8,
|
||||
request_timeout_secs: 30,
|
||||
}).await?;
|
||||
|
||||
// File operations
|
||||
let mut file = sftp.open("remote.txt").await?; // read-only
|
||||
let mut file = sftp.create("new.txt").await?; // create+truncate+write
|
||||
let mut file = sftp.open_with_flags("f", OpenFlags::READ | OpenFlags::WRITE).await?;
|
||||
|
||||
file.write_all(b"hello").await?;
|
||||
file.flush().await?; // drains write pipeline + optional fsync
|
||||
file.rewind().await?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).await?;
|
||||
file.shutdown().await?; // properly closes handle
|
||||
|
||||
// Directory operations
|
||||
for entry in sftp.read_dir(".").await? {
|
||||
println!("{}: {:?}", entry.file_name(), entry.file_type());
|
||||
}
|
||||
|
||||
// Other operations
|
||||
sftp.canonicalize(".").await?;
|
||||
sftp.metadata("file").await?;
|
||||
sftp.symlink_metadata("link").await?;
|
||||
sftp.create_dir("dir").await?;
|
||||
sftp.remove_dir("dir").await?;
|
||||
sftp.remove_file("file").await?;
|
||||
sftp.rename("old", "new").await?;
|
||||
sftp.symlink("target", "link").await?;
|
||||
sftp.read_link("link").await?;
|
||||
sftp.hardlink("src", "dst").await?; // returns false if unsupported
|
||||
sftp.fs_info("/").await?; // returns Ok(None) if unsupported
|
||||
sftp.close().await?;
|
||||
```
|
||||
|
||||
## Server Quick Start
|
||||
|
||||
```rust
|
||||
use russh_sftp::protocol::{File, Handle, Name, Status, StatusCode, Version};
|
||||
use russh_sftp::server::{Handler, StatusReply};
|
||||
|
||||
struct MyHandler;
|
||||
|
||||
impl Handler for MyHandler {
|
||||
type Error = StatusCode;
|
||||
|
||||
fn unimplemented(&self) -> Self::Error {
|
||||
StatusCode::OpUnsupported
|
||||
}
|
||||
|
||||
async fn init(&mut self, version: u32, _ext: HashMap<String, String>)
|
||||
-> Result<Version, Self::Error>
|
||||
{
|
||||
Ok(Version::new())
|
||||
}
|
||||
|
||||
// ... implement methods as needed
|
||||
}
|
||||
|
||||
// In your SSH server handler:
|
||||
async fn subsystem_request(&mut self, channel_id: ChannelId, name: &str, session: &mut Session)
|
||||
-> Result<(), Error>
|
||||
{
|
||||
if name == "sftp" {
|
||||
let channel = self.get_channel(channel_id).await;
|
||||
session.channel_success(channel_id)?;
|
||||
russh_sftp::server::run(channel.into_stream(), MyHandler).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## RawSftpSession Quick Reference
|
||||
|
||||
```rust
|
||||
use russh_sftp::client::RawSftpSession;
|
||||
|
||||
let session = RawSftpSession::new(stream);
|
||||
// or: RawSftpSession::new_with_config(stream, config);
|
||||
|
||||
// Must call init first
|
||||
let version = session.init().await?;
|
||||
|
||||
// Request-response methods
|
||||
let handle = session.open("file", OpenFlags::READ, FileAttributes::empty()).await?;
|
||||
let data = session.read(handle.handle, 0, 32768).await?;
|
||||
session.write(handle.handle, 0, vec![1,2,3]).await?;
|
||||
session.close(handle.handle).await?;
|
||||
let attrs = session.stat("/path").await?;
|
||||
let name = session.realpath(".").await?;
|
||||
let dir_handle = session.opendir("/").await?;
|
||||
let entries = session.readdir(dir_handle.handle).await?;
|
||||
session.close(dir_handle.handle).await?;
|
||||
|
||||
// Extensions
|
||||
let limits = session.limits().await?;
|
||||
session.hardlink("/old", "/new").await?;
|
||||
session.fsync(handle).await?;
|
||||
let fs_info = session.statvfs("/").await?;
|
||||
```
|
||||
|
||||
## StatusCode Reference
|
||||
|
||||
| Code | Constant | Meaning |
|
||||
|------|----------|---------|
|
||||
| 0 | `Ok` | Successful completion |
|
||||
| 1 | `Eof` | End of file / no more directory entries |
|
||||
| 2 | `NoSuchFile` | File does not exist |
|
||||
| 3 | `PermissionDenied` | Insufficient permissions |
|
||||
| 4 | `Failure` | Generic failure |
|
||||
| 5 | `BadMessage` | Badly formatted packet |
|
||||
| 6 | `NoConnection` | Client-side: no connection (never from server) |
|
||||
| 7 | `ConnectionLost` | Client-side: connection lost (never from server) |
|
||||
| 8 | `OpUnsupported` | Operation not supported |
|
||||
|
||||
## OpenFlags Reference
|
||||
|
||||
| Flag | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `READ` | 0x01 | Open for reading |
|
||||
| `WRITE` | 0x02 | Open for writing |
|
||||
| `APPEND` | 0x04 | Append to existing data |
|
||||
| `CREATE` | 0x08 | Create if doesn't exist |
|
||||
| `TRUNCATE` | 0x10 | Truncate to zero length |
|
||||
| `EXCLUDE` | 0x20 | Fail if file exists (must be with CREATE) |
|
||||
|
||||
## FileAttr (Attribute Flags) Reference
|
||||
|
||||
| Flag | Value | Fields Present |
|
||||
|------|-------|---------------|
|
||||
| `SIZE` | 0x00000001 | `size: u64` |
|
||||
| `UIDGID` | 0x00000002 | `uid: u32`, `gid: u32` |
|
||||
| `PERMISSIONS` | 0x00000004 | `permissions: u32` |
|
||||
| `ACMODTIME` | 0x00000008 | `atime: u32`, `mtime: u32` |
|
||||
| `EXTENDED` | 0x80000000 | (not yet implemented) |
|
||||
|
||||
## FileMode (File Type) Reference
|
||||
|
||||
| Constant | Value | Type |
|
||||
|----------|-------|------|
|
||||
| `FIFO` | 0x1000 | Named pipe |
|
||||
| `CHR` | 0x2000 | Character device |
|
||||
| `DIR` | 0x4000 | Directory |
|
||||
| `NAM` | 0x5000 | Named file |
|
||||
| `BLK` | 0x6000 | Block device |
|
||||
| `REG` | 0x8000 | Regular file |
|
||||
| `LNK` | 0xA000 | Symbolic link |
|
||||
| `SOCK` | 0xC000 | Socket |
|
||||
|
||||
## Packet Wire Format
|
||||
|
||||
All SFTP packets:
|
||||
|
||||
```
|
||||
[u32 length] [u8 type] [payload...]
|
||||
```
|
||||
|
||||
- `length` = size of type byte + payload (does not include the length field itself)
|
||||
- Strings: `[u32 len] [utf8 bytes]`
|
||||
- Byte arrays: `[u32 len] [raw bytes]`
|
||||
- `FileAttributes`: `[u32 flags] [conditional fields based on flags]`
|
||||
|
||||
## Extension Names
|
||||
|
||||
| Extension | Name | Version |
|
||||
|-----------|------|---------|
|
||||
| Limits | `limits@openssh.com` | 1 |
|
||||
| Hardlink | `hardlink@openssh.com` | 1 |
|
||||
| Fsync | `fsync@openssh.com` | 1 |
|
||||
| Statvfs | `statvfs@openssh.com` | 2 |
|
||||
Reference in New Issue
Block a user