Files
alknet/docs/research/references/ssh/sftp-rs/09-quick-reference.md

159 lines
6.7 KiB
Markdown

# sftp-rs: Quick Reference
## Crate Info
| Field | Value |
|-------|-------|
| Name | `sftp` |
| Version | 0.3.0 |
| License | Apache-2.0 |
| Edition | 2021 |
| Repository | https://github.com/jelmer/sftp-rs |
| Author | Jelmer Vernooij |
## Feature Flags
| Feature | Default | Requires | Provides |
|---------|---------|----------|----------|
| `default` | ✅ | `bin` | CLI binary |
| `bin` | ✅ (via default) | `rustyline`, `shell-words` | `sftp` binary |
| `ssh2` | ❌ | `ssh2` crate | `TryFrom<ssh2::Channel>` |
| `async` | ❌ | `tokio` | `AsyncSftpClient` |
| `russh` | ❌ | `russh`, `async`, `tokio` | russh transport glue |
## Module Map
| Module | Feature Gate | Contents |
|--------|-------------|----------|
| `protocol` | always | Wire codec: types, builders, parsers |
| `sync` | always | `SftpClient<C>` |
| `r#async` | `async` | `AsyncSftpClient<W>` |
| `russh` | `russh` | `from_channel()`, `from_subsystem_channel()` |
## Re-exports (`lib.rs`)
```rust
// Always available
pub use protocol::{Attributes, Directory, Error, File, Kind, OpenOptions, Result, TextHint,
/* all SSH_FILEXFER_ATTR_* constants */};
pub use sync::SftpClient;
// With "async" feature
pub use r#async::AsyncSftpClient;
```
## Client Construction
```rust
// Sync: from any Read+Write
let client = SftpClient::new(channel)?; // channel: impl Read + Write
let client = SftpClient::from_fd(fd)?; // Unix: from raw fd
let client = SftpClient::from_handle(handle)?; // Windows: from raw handle
let client = SftpClient::try_from(ssh2_channel)?; // ssh2 feature
// Async: from split halves
let client = AsyncSftpClient::new(reader, writer).await?; // impl AsyncRead + AsyncWrite
// russh: from channel
let client = sftp::russh::from_channel(channel).await?; // requests sftp subsystem
let client = sftp::russh::from_subsystem_channel(channel).await?; // subsystem already requested
```
## Operations Cheat Sheet
### File Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Open | `open(path, opts, attrs)` | `open(path, opts, attrs).await` | OPEN | HANDLE → File |
| Read | `pread(&file, offset, len)` | `pread(&file, offset, len).await` | READ | DATA → Vec\<u8\> |
| Write | `pwrite(&file, offset, data)` | `pwrite(&file, offset, data).await` | WRITE | STATUS |
| Close | `fclose(&file)` | `fclose(&file).await` | CLOSE | STATUS |
| Line seek | `flineseek(&file, lineno)` | `flineseek(&file, lineno).await` | EXTENDED "text-seek" | STATUS/REPLY |
### Directory Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Open dir | `opendir(path)` | `opendir(path).await` | OPENDIR | HANDLE → Directory |
| Read dir | `readdir(&dir)` | `readdir(&dir).await` | READDIR | NAME → Vec\<(name, long, attrs)\> |
| Close dir | `closedir(&dir)` | `closedir(&dir).await` | CLOSE | STATUS |
| Make dir | `mkdir(path, attrs)` | `mkdir(path, attrs).await` | MKDIR | STATUS |
| Remove dir | `rmdir(path)` | `rmdir(path).await` | RMDIR | STATUS |
### Attribute Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Stat (follow) | `stat(path, flags)` | `stat(path, flags).await` | STAT | ATTRS |
| Lstat (no follow) | `lstat(path, flags)` | `lstat(path, flags).await` | LSTAT | ATTRS |
| Fstat (by handle) | `fstat(&file, flags)` | `fstat(&file, flags).await` | FSTAT | ATTRS |
| Set stat (path) | `setstat(path, attrs)` | `setstat(path, attrs).await` | SETSTAT | STATUS |
| Set stat (handle) | `fsetstat(&file, attrs)` | `fsetstat(&file, attrs).await` | FSETSTAT | STATUS |
### Path Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Canonicalize | `realpath(path, ctrl, compose)` | `realpath(path, ctrl, compose).await` | REALPATH | NAME → String |
| Read symlink | `readlink(path)` | `readlink(path).await` | READLINK | NAME → String |
| Remove file | `remove(path)` | `remove(path).await` | REMOVE | STATUS |
| Rename | `rename(old, new, flags)` | `rename(old, new, flags).await` | RENAME | STATUS |
| Symlink | `symlink(path, target)` | `symlink(path, target).await` | SYMLINK | STATUS |
| Hard link | `hardlink(path, target)` | `hardlink(path, target).await` | LINK | STATUS |
| Link (generic) | `link(path, target, sym)` | `link(path, target, sym).await` | LINK | STATUS |
### Lock Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Block | `block(&file, off, len, mask)` | `block(&file, off, len, mask).await` | BLOCK | STATUS |
| Unblock | `unblock(&file, off, len)` | `unblock(&file, off, len).await` | UNBLOCK | STATUS |
### Extension Operations
| Operation | Sync | Async | Request | Response |
|-----------|------|-------|---------|----------|
| Extended | `extended(req, data)` | `extended(req, data).await` | EXTENDED | REPLY → Option\<Vec\<u8\>\> |
## OpenOptions Builder
```rust
OpenOptions::new()
.read(true) // SFTP_FLAG_READ = 0x01
.write(true) // SFTP_FLAG_WRITE = 0x02
.append(true) // SFTP_FLAG_APPEND = 0x04
.create(true) // SFTP_FLAG_CREAT = 0x08
.truncate(true) // SFTP_FLAG_TRUNC = 0x10
.excl(true) // SFTP_FLAG_EXCL = 0x20
```
## Error Variants (Most Common)
| Error | When |
|-------|------|
| `Eof(msg, lang)` | End of file read, or end of directory listing |
| `NoSuchFile(msg, lang)` | File/path does not exist |
| `PermissionDenied(msg, lang)` | Insufficient permissions |
| `Failure(msg, lang)` | Generic failure |
| `DirNotEmpty(msg, lang)` | rmdir on non-empty directory |
| `NotADirectory(msg, lang)` | Expected directory, got file |
| `FileAlreadyExists(msg, lang)` | Exclusive create on existing file |
| `Io(err)` | Local I/O error or unexpected protocol message |
| `Other(code, msg, lang)` | Unrecognized SFTP status code |
## Testing Approach
Both sync and async clients have extensive test suites using **stub servers**:
- **Sync**: `spawn_stub()` on TCP sockets — a background thread that handles INIT/VERSION then routes requests through a handler closure
- **Async**: `with_stub()` using `tokio::io::duplex()` — a spawned async task that runs a router against a handler closure
Both approaches allow per-test programmable server behavior: the handler receives `(cmd, body_without_req_id)` and returns `(response_cmd, response_body_without_req_id)`.
The protocol module has unit tests for:
- `Attributes` round-trip serialization (empty, all fields, individual field groups)
- Builder byte layout verification (exact byte-level assertions)
- Request-ID wrap/unwrap
- Status parsing (OK, EOF, typed errors)
- Expect functions (correct type, status-as-error, unexpected-type rejection)