Skip to content

Commit cdb203e

Browse files
committed
Phase 8: polish public API
Clean re-exports from lib.rs: users write `use smb2::{SmbClient, Tree}` instead of `use smb2::client::{SmbClient, Tree}`. SmbClient convenience methods: 11 methods (list_directory, read_file, write_file, delete_file, stat, rename, create_directory, etc.) that delegate to Tree — users call `client.list_directory(&share, "path")` instead of `tree.list_directory(client.connection_mut(), "path")`. README updated with actual API examples that match the code. Quick start, pipeline API, and API overview all use real types and methods. Four runnable examples: list_shares, list_directory, read_file, write_file. All compile and use the high-level SmbClient API. Module-level docs on internal modules (msg, pack, crypto, auth, rpc, transport, types) point users to SmbClient instead. Crate-level docs with quick start example and module overview. 519 unit tests + 12 integration tests, zero clippy warnings.
1 parent b0cacdd commit cdb203e

15 files changed

Lines changed: 409 additions & 54 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ tests/
9090
9191
examples/
9292
list_shares.rs # Connect and enumerate shares
93+
list_directory.rs # List files in a directory
9394
read_file.rs # Read a file from a share
9495
write_file.rs # Write a file to a share
95-
watch_directory.rs # Monitor directory changes
9696
```
9797

9898
## Architecture

README.md

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,16 @@ I built this because I needed fast SMB access for [Cmdr](https://github.com/vdav
2727
- Credit-window-based flow control (the server tells us how fast to go)
2828
- SMB 3.x signing, encryption, and LZ4 compression
2929
- Share enumeration (list shares on a server)
30-
- Directory change notifications (watch for file changes)
31-
- Auto-reconnect with durable handles (survives Wi-Fi drops)
32-
- Server-side copy
3330

3431
## What it doesn't do (yet)
3532

3633
If you need any of these, check the [`smb`](https://crates.io/crates/smb) crate which supports them:
3734

38-
- **Kerberos authentication** NTLM only for now. Kerberos is needed for Active Directory environments that disable NTLM. Most home NAS setups (Synology, QNAP, Pi) use NTLM.
39-
- **DFS path resolution** returns `Error::DfsReferralRequired` with the path so you can handle it yourself. Full automatic DFS follow-through is planned for post-1.0.
40-
- **Multi-channel** multiple TCP connections to the same server for higher throughput. Planned for post-1.0.
41-
- **QUIC transport** SMB over QUIC for Azure Files and Windows Server 2022+ over the internet
42-
- **RDMA transport** datacenter-only, ultra-low-latency storage
35+
- **Kerberos authentication** -- NTLM only for now. Kerberos is needed for Active Directory environments that disable NTLM. Most home NAS setups (Synology, QNAP, Pi) use NTLM.
36+
- **DFS path resolution** -- returns `Error::DfsReferralRequired` with the path so you can handle it yourself. Full automatic DFS follow-through is planned for post-1.0.
37+
- **Multi-channel** -- multiple TCP connections to the same server for higher throughput. Planned for post-1.0.
38+
- **QUIC transport** -- SMB over QUIC for Azure Files and Windows Server 2022+ over the internet
39+
- **RDMA transport** -- datacenter-only, ultra-low-latency storage
4340

4441
These aren't planned:
4542

@@ -49,55 +46,82 @@ These aren't planned:
4946
## Quick start
5047

5148
```rust
52-
use smb2::SmbClient;
49+
use smb2::{SmbClient, ClientConfig};
5350

5451
#[tokio::main]
5552
async fn main() -> Result<(), smb2::Error> {
53+
let mut client = smb2::connect("192.168.1.100:445", "user", "pass").await?;
54+
55+
// List shares
56+
let shares = client.list_shares().await?;
57+
for share in &shares {
58+
println!("{} - {}", share.name, share.comment);
59+
}
60+
5661
// Connect to a share
57-
let client = SmbClient::connect("192.168.1.100", "user", "password").await?;
5862
let share = client.connect_share("Documents").await?;
5963

6064
// List files
61-
for entry in share.list("projects/").await? {
62-
println!("{} {:>10} bytes", entry.name, entry.size);
65+
let entries = client.list_directory(&share, "projects/").await?;
66+
for entry in &entries {
67+
println!("{} ({} bytes)", entry.name, entry.size);
6368
}
6469

6570
// Read a file
66-
let data = share.read_file("projects/report.pdf").await?;
71+
let data = client.read_file(&share, "report.pdf").await?;
6772
std::fs::write("report.pdf", data)?;
6873

74+
// Write a file
75+
let content = std::fs::read("local_file.txt")?;
76+
client.write_file(&share, "remote_file.txt", &content).await?;
77+
78+
// Clean up
79+
client.disconnect_share(&share).await?;
80+
6981
Ok(())
7082
}
7183
```
7284

7385
## Pipeline API
7486

75-
The pipeline is the core feature. It lets you push requests from anywhere, at any time, and results stream back as they complete. You don't need to know the total count upfront.
87+
The pipeline is the core feature. It lets you batch multiple operations and execute them together:
7688

7789
```rust
78-
let (tx, mut rx) = share.open_pipeline();
90+
use smb2::{Pipeline, Op, OpResult};
91+
92+
# async fn example(client: &mut smb2::SmbClient, share: &smb2::Tree) -> Result<(), smb2::Error> {
93+
let mut pipeline = Pipeline::new(client.connection_mut(), &share);
7994

80-
// Push requests — from any task, any time
81-
tx.request(Op::ReadFile("a.txt")).await;
82-
tx.request(Op::WriteFile("b.txt", data)).await;
83-
tx.request(Op::Delete("c.txt")).await;
84-
tx.request(Op::List("projects/")).await;
95+
let results = pipeline.execute(vec![
96+
Op::ReadFile("a.txt".into()),
97+
Op::ReadFile("b.txt".into()),
98+
Op::ListDirectory("docs/".into()),
99+
Op::Delete("temp.txt".into()),
100+
]).await;
85101

86-
// Results stream back as they complete
87-
while let Some(result) = rx.next().await {
102+
for result in results {
88103
match result {
89-
OpResult::FileData(path, bytes) => { /* ... */ }
90-
OpResult::Written(path, n) => { /* ... */ }
91-
OpResult::Deleted(path) => { /* ... */ }
92-
OpResult::DirEntries(path, entries) => { /* ... */ }
93-
OpResult::Error(path, err) => { /* ... */ }
104+
OpResult::FileData { path, data } => println!("{}: {} bytes", path, data.len()),
105+
OpResult::DirEntries { path, entries } => println!("{}: {} entries", path, entries.len()),
106+
OpResult::Deleted { path } => println!("deleted {}", path),
107+
OpResult::Error { path, error } => eprintln!("{}: {}", path, error),
108+
other => println!("{:?}", other),
94109
}
95110
}
96-
97-
// Drop tx to signal "no more requests" — pipeline drains gracefully
111+
# Ok(())
112+
# }
98113
```
99114

100-
The simple API (`share.read_file()`, etc.) wraps this internally. One request in, one result out.
115+
For large file I/O, use the pipelined variants which fill the credit window:
116+
117+
```rust
118+
# async fn example(client: &mut smb2::SmbClient, share: &smb2::Tree) -> Result<(), smb2::Error> {
119+
// ~10-25x faster than sequential for large files
120+
let data = client.read_file_pipelined(&share, "big_file.iso").await?;
121+
client.write_file_pipelined(&share, "copy.iso", &data).await?;
122+
# Ok(())
123+
# }
124+
```
101125

102126
## Installation
103127

@@ -117,26 +141,39 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
117141

118142
## API overview
119143

120-
### Simple API
144+
### High-level API
121145

122146
For when you want to do one thing and get the result:
123147

124-
- `SmbClient::connect()` — connect and authenticate
125-
- `share.list()` — list a directory
126-
- `share.read_file()` — download a file
127-
- `share.write_file()` — upload a file
128-
- `share.delete()` — delete a file
129-
- `share.stat()` — get file metadata
148+
- `smb2::connect()` -- connect and authenticate (shorthand)
149+
- `SmbClient::connect()` -- connect with full config
150+
- `client.list_shares()` -- list available shares
151+
- `client.connect_share()` -- connect to a share
152+
- `client.list_directory(&share, path)` -- list a directory
153+
- `client.read_file(&share, path)` -- download a file
154+
- `client.write_file(&share, path, data)` -- upload a file
155+
- `client.delete_file(&share, path)` -- delete a file
156+
- `client.stat(&share, path)` -- get file metadata
157+
- `client.rename(&share, from, to)` -- rename a file
158+
- `client.create_directory(&share, path)` -- create a directory
159+
- `client.delete_directory(&share, path)` -- remove a directory
160+
- `client.disconnect_share(&share)` -- disconnect from a share
130161

131162
### Pipeline API
132163

133164
For when you have many operations and want them fast:
134165

135-
- `share.open_pipeline()` — returns `(tx, rx)` channel pair
136-
- `tx.request(Op)` — push an operation (from any task)
137-
- `rx.next()` — receive the next completed result
166+
- `Pipeline::new(conn, &share)` -- create a pipeline
167+
- `pipeline.execute(ops)` -- run a batch of operations
168+
169+
### Low-level API
170+
171+
For advanced use cases, the underlying types are available:
138172

139-
The pipeline handles credit management, chunking, compounding, and reordering internally. Multiple operations fly over the wire concurrently, bounded by the server's credit grants.
173+
- `Connection` -- message exchange, credit tracking
174+
- `Session` -- NTLM authentication, key derivation
175+
- `Tree` -- share-level file operations (take `&mut Connection`)
176+
- `NegotiatedParams` -- protocol parameters from negotiate
140177

141178
## Performance
142179

@@ -169,10 +206,10 @@ The [`smb`](https://crates.io/crates/smb) crate is the most complete Rust SMB2 o
169206

170207
But for the common case (connect to a NAS, move files around), `smb2` is a better fit:
171208

172-
- **Pipelined I/O** `smb` sends one request at a time, making downloads ~10x slower
173-
- **Auto-reconnect with durable handles** survives Wi-Fi drops without restarting transfers
174-
- **Comprehensive test suite** `smb` has almost no tests
175-
- **MIT OR Apache-2.0** `smb` is MIT-only
209+
- **Pipelined I/O** -- `smb` sends one request at a time, making downloads ~10x slower
210+
- **Auto-reconnect with durable handles** -- survives Wi-Fi drops without restarting transfers
211+
- **Comprehensive test suite** -- `smb` has almost no tests
212+
- **MIT OR Apache-2.0** -- `smb` is MIT-only
176213

177214
I initially considered forking `smb`, but the architecture didn't support pipelining well, and adding it would have been a near-complete rewrite anyway.
178215

examples/list_directory.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// List files in a directory on an SMB share.
2+
//
3+
// Usage:
4+
// cargo run --example list_directory
5+
//
6+
// Set RUST_LOG=smb2=debug for protocol-level logging.
7+
8+
#[tokio::main]
9+
async fn main() -> Result<(), smb2::Error> {
10+
env_logger::init();
11+
12+
// Change these to match your server.
13+
let addr = "192.168.1.100:445";
14+
let username = "user";
15+
let password = "password";
16+
let share_name = "Documents";
17+
let directory = ""; // empty = root of the share
18+
19+
let mut client = smb2::connect(addr, username, password).await?;
20+
let share = client.connect_share(share_name).await?;
21+
22+
let entries = client.list_directory(&share, directory).await?;
23+
24+
println!("{} entries in \\\\{}\\{}\\{}:", entries.len(), addr, share_name, directory);
25+
for entry in &entries {
26+
let kind = if entry.is_directory { "DIR " } else { " " };
27+
println!(" {} {:>12} bytes {}", kind, entry.size, entry.name);
28+
}
29+
30+
client.disconnect_share(&share).await?;
31+
32+
Ok(())
33+
}

examples/list_shares.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// List available shares on an SMB server.
2+
//
3+
// Usage:
4+
// cargo run --example list_shares
5+
//
6+
// Set RUST_LOG=smb2=debug for protocol-level logging.
7+
8+
#[tokio::main]
9+
async fn main() -> Result<(), smb2::Error> {
10+
env_logger::init();
11+
12+
// Change these to match your server.
13+
let addr = "192.168.1.100:445";
14+
let username = "user";
15+
let password = "password";
16+
17+
let mut client = smb2::connect(addr, username, password).await?;
18+
19+
println!("Connected to {}", addr);
20+
if let Some(params) = client.params() {
21+
println!("Dialect: {}", params.dialect);
22+
}
23+
24+
let shares = client.list_shares().await?;
25+
26+
println!("\nShares ({} total):", shares.len());
27+
for share in &shares {
28+
if share.comment.is_empty() {
29+
println!(" {}", share.name);
30+
} else {
31+
println!(" {} - {}", share.name, share.comment);
32+
}
33+
}
34+
35+
Ok(())
36+
}

examples/read_file.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Read a file from an SMB share and save it to disk.
2+
//
3+
// Usage:
4+
// cargo run --example read_file
5+
//
6+
// Set RUST_LOG=smb2=debug for protocol-level logging.
7+
8+
use std::time::Instant;
9+
10+
#[tokio::main]
11+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
12+
env_logger::init();
13+
14+
// Change these to match your server.
15+
let addr = "192.168.1.100:445";
16+
let username = "user";
17+
let password = "password";
18+
let share_name = "Documents";
19+
let remote_path = "report.pdf";
20+
let local_path = "report.pdf";
21+
22+
let mut client = smb2::connect(addr, username, password).await?;
23+
let share = client.connect_share(share_name).await?;
24+
25+
// Use pipelined read for large files (much faster).
26+
let start = Instant::now();
27+
let data = client.read_file_pipelined(&share, remote_path).await?;
28+
let elapsed = start.elapsed();
29+
30+
std::fs::write(local_path, &data)?;
31+
32+
println!(
33+
"Downloaded {} ({} bytes) in {:.2?}",
34+
remote_path,
35+
data.len(),
36+
elapsed,
37+
);
38+
if elapsed.as_secs_f64() > 0.0 {
39+
println!(
40+
" {:.1} MB/s",
41+
data.len() as f64 / (1024.0 * 1024.0) / elapsed.as_secs_f64()
42+
);
43+
}
44+
45+
client.disconnect_share(&share).await?;
46+
47+
Ok(())
48+
}

examples/write_file.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Write a local file to an SMB share.
2+
//
3+
// Usage:
4+
// cargo run --example write_file
5+
//
6+
// Set RUST_LOG=smb2=debug for protocol-level logging.
7+
8+
use std::time::Instant;
9+
10+
#[tokio::main]
11+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
12+
env_logger::init();
13+
14+
// Change these to match your server.
15+
let addr = "192.168.1.100:445";
16+
let username = "user";
17+
let password = "password";
18+
let share_name = "Documents";
19+
let local_path = "local_file.txt";
20+
let remote_path = "uploaded_file.txt";
21+
22+
let data = std::fs::read(local_path)?;
23+
println!("Read {} bytes from {}", data.len(), local_path);
24+
25+
let mut client = smb2::connect(addr, username, password).await?;
26+
let share = client.connect_share(share_name).await?;
27+
28+
// Use pipelined write for large files (much faster).
29+
let start = Instant::now();
30+
let written = client.write_file_pipelined(&share, remote_path, &data).await?;
31+
let elapsed = start.elapsed();
32+
33+
println!(
34+
"Uploaded {} bytes to {} in {:.2?}",
35+
written, remote_path, elapsed,
36+
);
37+
if elapsed.as_secs_f64() > 0.0 {
38+
println!(
39+
" {:.1} MB/s",
40+
written as f64 / (1024.0 * 1024.0) / elapsed.as_secs_f64()
41+
);
42+
}
43+
44+
client.disconnect_share(&share).await?;
45+
46+
Ok(())
47+
}

src/auth/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Authentication mechanisms for SMB2.
22
//!
33
//! Currently supports NTLM authentication (MS-NLMP).
4+
//!
5+
//! Most users don't need this module directly -- [`SmbClient`](crate::SmbClient)
6+
//! handles authentication during [`connect`](crate::connect).
47
58
pub mod ntlm;
69

0 commit comments

Comments
 (0)