docs(research): add russh-sftp deep-dive reference
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# russh-sftp: Overview and Architecture
|
||||
|
||||
**Version**: 2.3.0
|
||||
**Repository**: https://github.com/AspectUnk/russh-sftp
|
||||
**License**: Apache-2.0
|
||||
**Rust Edition**: 2021
|
||||
**Protocol**: SFTP v3 ([draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02))
|
||||
|
||||
## What It Is
|
||||
|
||||
`russh-sftp` is a Rust crate providing **both SFTP client and server** implementations. It operates on any transport that provides `AsyncRead + AsyncWrite` byte streams — not just russh. The crate targets SFTP protocol version 3, the most widely deployed version.
|
||||
|
||||
Core design decisions:
|
||||
- **Transport-agnostic** — works with any `AsyncRead + AsyncWrite` stream, not tied to a specific SSH library
|
||||
- **Both client and server** — provides handler traits and run functions for both sides
|
||||
- **Two client tiers** — a low-level `RawSftpSession` (request–response) and a high-level `SftpSession` (std::fs-like API with `AsyncRead`/`AsyncWrite` file I/O)
|
||||
- **Custom serde wire format** — implements its own `Serializer`/`Deserializer` over `bytes::BytesMut` for SFTP binary encoding, not using serde's typical self-describing formats
|
||||
- **Concurrent writes** — the high-level `File` type supports pipelined writes with configurable concurrency
|
||||
- **WASM support** — the server module is gated behind `not(target_arch = "wasm32")`; client runtime abstracts over tokio and wasm-bindgen-futures
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
russh-sftp/src/
|
||||
├── lib.rs # Crate root: module declarations, macro imports
|
||||
├── buf.rs # TryBuf trait: try_get_bytes/try_get_string on Buf
|
||||
├── de.rs # Custom serde Deserializer (binary SFTP wire format)
|
||||
├── ser.rs # Custom serde Serializer (binary SFTP wire format)
|
||||
├── error.rs # Top-level Error enum
|
||||
├── extensions.rs # OpenSSH extension types: limits, hardlink, fsync, statvfs
|
||||
├── utils.rs # unix() time helper, read_packet() async wire reader
|
||||
├── protocol/ # SFTP v3 message types and Packet enum
|
||||
│ ├── mod.rs # Packet enum, type constants, TryFrom<Bytes>/Into<Bytes>
|
||||
│ ├── init.rs # SSH_FXP_INIT
|
||||
│ ├── version.rs # SSH_FXP_VERSION
|
||||
│ ├── open.rs # SSH_FXP_OPEN + OpenFlags bitflags
|
||||
│ ├── close.rs # SSH_FXP_CLOSE
|
||||
│ ├── read.rs # SSH_FXP_READ
|
||||
│ ├── write.rs # SSH_FXP_WRITE
|
||||
│ ├── lstat.rs # SSH_FXP_LSTAT
|
||||
│ ├── stat.rs # SSH_FXP_STAT
|
||||
│ ├── fstat.rs # SSH_FXP_FSTAT
|
||||
│ ├── setstat.rs # SSH_FXP_SETSTAT
|
||||
│ ├── fsetstat.rs # SSH_FXP_FSETSTAT
|
||||
│ ├── opendir.rs # SSH_FXP_OPENDIR
|
||||
│ ├── readdir.rs # SSH_FXP_READDIR
|
||||
│ ├── remove.rs # SSH_FXP_REMOVE
|
||||
│ ├── mkdir.rs # SSH_FXP_MKDIR
|
||||
│ ├── rmdir.rs # SSH_FXP_RMDIR
|
||||
│ ├── realpath.rs # SSH_FXP_REALPATH
|
||||
│ ├── rename.rs # SSH_FXP_RENAME
|
||||
│ ├── readlink.rs # SSH_FXP_READLINK
|
||||
│ ├── symlink.rs # SSH_FXP_SYMLINK
|
||||
│ ├── status.rs # SSH_FXP_STATUS + StatusCode enum
|
||||
│ ├── handle.rs # SSH_FXP_HANDLE
|
||||
│ ├── data.rs # SSH_FXP_DATA
|
||||
│ ├── name.rs # SSH_FXP_NAME
|
||||
│ ├── attrs.rs # SSH_FXP_ATTRS
|
||||
│ ├── extended.rs # SSH_FXP_EXTENDED / SSH_FXP_EXTENDED_REPLY
|
||||
│ ├── file.rs # File struct (filename + longname + attrs)
|
||||
│ └── file_attrs.rs # FileAttributes, FileAttr flags, FileMode, FileType, FilePermissions
|
||||
├── client/ # Client-side implementation
|
||||
│ ├── mod.rs # Config, run(), execute_handler()
|
||||
│ ├── handler.rs # Client Handler trait
|
||||
│ ├── rawsession.rs # RawSftpSession: request-response SFTP client
|
||||
│ ├── session.rs # SftpSession: high-level std::fs-like client
|
||||
│ ├── error.rs # Client-specific Error enum
|
||||
│ ├── runtime.rs # Runtime abstraction (tokio native vs WASM)
|
||||
│ └── fs/
|
||||
│ ├── mod.rs # Re-exports: File, DirEntry, ReadDir, Metadata
|
||||
│ ├── file.rs # File: AsyncRead + AsyncWrite + AsyncSeek
|
||||
│ └── dir.rs # DirEntry, ReadDir iterator
|
||||
└── server/ # Server-side implementation
|
||||
├── mod.rs # Config, run(), run_with_config(), process_request()
|
||||
├── handler.rs # Server Handler trait
|
||||
└── reply.rs # StatusReply type for error responses
|
||||
```
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
| Dependency | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| `tokio` | 1 | Async runtime: io-util, rt, sync, time, macros |
|
||||
| `tokio-util` | 0.7.18 | Runtime utilities |
|
||||
| `serde` | 1.0 | Derive macros for protocol types |
|
||||
| `serde_bytes` | 0.11 | Efficient byte array serialization |
|
||||
| `bitflags` | 2.11 | Bitflag types: OpenFlags, FileAttr, FileMode, FilePermissionFlags |
|
||||
| `bytes` | 1.11 | BytesMut/Bytes for zero-copy wire I/O |
|
||||
| `dashmap` | 6.1 | Concurrent HashMap for request/response tracking |
|
||||
| `chrono` | 0.4 | DateTime for File::longname formatting |
|
||||
| `thiserror` | 2.0 | Error derive macros |
|
||||
| `log` | 0.4 | Logging facade |
|
||||
|
||||
Dev dependencies: `russh` 0.61.0 (for examples), `criterion` 0.8.2 (benchmarks).
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Feature | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `async-trait` | ❌ | Enables `#[async_trait]` attribute on Handler traits |
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ │
|
||||
│ ┌───────────────────┐ ┌──────────────────────┐ ┌──────────────┐ │
|
||||
│ │ SftpSession │ │ RawSftpSession │ │ server:: │ │
|
||||
│ │ (high-level) │ │ (low-level) │ │ Handler │ │
|
||||
│ │ │ │ │ │ (user impl) │ │
|
||||
│ │ • open/create │ │ • init/open/close │ │ │ │
|
||||
│ │ • read/write │ │ • read/write │ │ init, open, │ │
|
||||
│ │ • metadata │ │ • stat/lstat/fstat │ │ read, write │ │
|
||||
│ │ • read_dir │ │ • opendir/readdir │ │ close, ... │ │
|
||||
│ │ • canonicalize │ │ • mkdir/rmdir/remove │ └──────┬───────┘ │
|
||||
│ │ • hardlink/fsync │ │ • symlink/readlink │ │ │
|
||||
│ │ • fs_info (statvfs)│ │ • extended │ │ │
|
||||
│ └────────┬──────────┘ └──────────┬────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ File (AsyncIO) │ │ │
|
||||
│ │ ┌─────────────────┐ │ │ │
|
||||
│ │ │ AsyncRead/Write │ │ │ │
|
||||
│ │ │ AsyncSeek │ │ │ │
|
||||
│ │ │ • pipelined writes│ │ │ │
|
||||
│ │ │ • handle tracking │ │ │ │
|
||||
│ │ └────────┬─────────┘ │ │ │
|
||||
│ └───────────┼─────────────┘ │ │
|
||||
│ │ │ │
|
||||
├───────────────────────┼─────────────────────────────────────┼─────────┤
|
||||
│ │ Protocol Layer │ │
|
||||
│ ┌────────────────────┼─────────────────────────────────────┼───┐ │
|
||||
│ │ ▼ ▼ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Packet enum │ │ │
|
||||
│ │ │ Init, Version, Open, Close, Read, Write, │ │ │
|
||||
│ │ │ Lstat, Fstat, SetStat, FSetStat, OpenDir, │ │ │
|
||||
│ │ │ ReadDir, Remove, MkDir, RmDir, RealPath, │ │ │
|
||||
│ │ │ Stat, Rename, ReadLink, Symlink, Status, │ │ │
|
||||
│ │ │ Handle, Data, Name, Attrs, Extended, ExtendedReply │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌────────────┴────────────┐ │ │
|
||||
│ │ │ ser.rs / de.rs │ │ │
|
||||
│ │ │ Custom serde for binary │ │ │
|
||||
│ │ │ SFTP wire format │ │ │
|
||||
│ │ └─────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
├──────────────────────────────────┼──────────────────────────────────┤
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ utils::read_packet() │ │
|
||||
│ │ buf::TryBuf │ │
|
||||
│ │ Wire I/O (length-prefixed) │
|
||||
│ └────────────┬─────────────┘ │
|
||||
│ │ │
|
||||
├─────────────────────────────────┼───────────────────────────────────┤
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Transport (AsyncRead + AsyncWrite) │ │
|
||||
│ │ e.g., russh Channel::into_stream() │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## How russh Integration Works
|
||||
|
||||
The crate does **not** depend on `russh` at runtime — it only appears as a dev-dependency for examples. Integration is by the caller providing a stream:
|
||||
|
||||
```rust
|
||||
// From examples/client.rs — typical russh integration
|
||||
let channel = session.channel_open_session().await.unwrap();
|
||||
channel.request_subsystem(true, "sftp").await.unwrap();
|
||||
let sftp = SftpSession::new(channel.into_stream()).await.unwrap();
|
||||
```
|
||||
|
||||
```rust
|
||||
// From examples/server.rs — typical russh server integration
|
||||
async fn subsystem_request(&mut self, channel_id: ChannelId, name: &str, session: &mut Session)
|
||||
-> Result<(), Self::Error>
|
||||
{
|
||||
if name == "sftp" {
|
||||
let channel = self.get_channel(channel_id).await;
|
||||
let sftp = SftpSession::default();
|
||||
session.channel_success(channel_id)?;
|
||||
russh_sftp::server::run(channel.into_stream(), sftp).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The `into_stream()` method on russh's `Channel` produces a type implementing `AsyncRead + AsyncWrite + Unpin + Send + 'static`, which is exactly what `russh-sftp`'s `run()` and `SftpSession::new()` accept.
|
||||
|
||||
## Re-exports from `lib.rs`
|
||||
|
||||
```rust
|
||||
pub mod client;
|
||||
pub mod de;
|
||||
pub mod extensions;
|
||||
pub mod protocol;
|
||||
pub mod ser;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod server;
|
||||
|
||||
// Key re-exports:
|
||||
pub use client::Handler; // client::Handler
|
||||
pub use client::RawSftpSession; // low-level client
|
||||
pub use client::SftpSession; // high-level client
|
||||
pub use server::Handler; // server::Handler
|
||||
pub use server::StatusReply; // server error reply type
|
||||
```
|
||||
Reference in New Issue
Block a user