216 lines
6.6 KiB
Markdown
216 lines
6.6 KiB
Markdown
# 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 | |