dart_smb2 0.1.0
dart_smb2: ^0.1.0 copied to clipboard
Dart client for SMB2/3 built on libsmb2. Supports listing, reads, writes, streaming, caching, worker pool with reconnect and more. Targets macOS, Windows, Linux, iOS and Android.
dart_smb2 #
SMB2/3 client for Dart & Flutter.
![]() |
dart_smb2 is a Dart client for SMB2/3 file shares built on libsmb2 v6.1.0. It provides a streaming API for reading, writing and managing files over the network across desktop and mobile. |
Installation #
Add dart_smb2 to your pubspec.yaml:
dependencies:
dart_smb2: ^0.1.0
Platforms #
| Platform | Minimum | Architecture | Device | Emulator |
|---|---|---|---|---|
| Android | 7.0 (SDK 24) | arm64-v8a, armeabi-v7a, x86_64 | ✅ | ✅ |
| iOS | 15.0 | arm64, x86_64 | ✅ | ✅ |
| macOS | 12.0 | arm64, x86_64 | ✅ | – |
| Windows | 10 | arm64, x86_64 | ✅ | – |
| Linux | Ubuntu 24.04 | aarch64, x86_64 | ✅ | – |
Contents #
Visuals #
The following images demonstrate the example app included in the example/ directory. This application serves as a reference client for testing the various features and capabilities of dart_smb2.
![]() |
Servers Manage saved connections |
![]() |
Browse Tree explorer |
![]() |
Read Reading performance test |
![]() |
Write Writing performance test |
Features #
| Pure Dart FFI synchronous bindings to libsmb2 via dart:ffi, with native binaries bundled for every supported platform. |
Cross-platform runs on macOS, Windows, Linux, iOS and Android — same API on every host. |
||
| SMB 2.02 → 3.1.1 full protocol coverage including encryption ( seal), signing and version pinning via Smb2Version. |
Worker poolSmb2Pool spreads requests across N isolate workers and reconnects transparently when a connection drops. |
||
File & directory opslistDirectory, stat, exists, mkdir, rmdir, rename, deleteFile, truncate and symlink resolution. |
Streaming reads & writes chunked I/O via sync Iterable or async Stream, with onProgress + isCanceled callbacks and a single persistent handle. |
||
Safe handleswithFile(path, body) opens a handle, runs your callback and guarantees closeHandle on any exit path. A Finalizer closes leaked handles as a safety net. |
Filesystem infostatvfs for free/total disk space, listShares for share enumeration, echo for keepalive ping. |
||
| Auto-reconnect dropped connections are detected and re-established transparently — the failed operation is reopened and retried without surfacing the disruption to your code. |
Semantic errors one Smb2Exception hierarchy with Smb2ErrorType enum (auth, fileNotFound, connection, alreadyExists, …), never raw NTSTATUS codes in your code. |
Quick Start #
import 'dart:io';
import 'dart:typed_data';
import 'package:dart_smb2/dart_smb2.dart';
void main() async {
// Connect the worker pool (auto-reconnect on connection errors)
final pool = await Smb2Pool.connect(
host: '192.168.1.100',
share: 'Files',
user: 'user',
password: 'pass',
);
// List a directory
final entries = await pool.listDirectory('Documents/Projects');
for (final entry in entries) {
print('${entry.name} — ${entry.size} bytes');
}
// Read the first 256 KB of a file
final header = await pool.readFileRange(
'Documents/report.pdf',
length: 256 * 1024,
);
// Download a file to disk with progress + cancel
await pool.downloadToFile(
'Music/song.flac',
File('/tmp/song.flac'),
onProgress: (received, total) =>
print('${(received / total * 100).toStringAsFixed(1)}%'),
);
// Read on-demand with a scoped handle (auto-closed)
final tags = await pool.withFile('Music/song.flac', (file) async {
final header = await file.read(length: 64 * 1024);
return parseTags(header); // your code
});
// Write a file
await pool.writeFile(
'Documents/notes.txt',
Uint8List.fromList('Hello SMB!'.codeUnits),
);
// Create a directory
await pool.mkdir('Documents/NewFolder');
// Get file info
final info = await pool.stat('Documents/report.pdf');
print('Size: ${info.size}, Modified: ${info.modified}');
await pool.disconnect();
}
Guide #
1. Connection & Lifecycle #
dart_smb2 offers two layers of abstraction. Pick the one that fits your use case — Smb2Pool is the recommended default.
| Layer | Class | Best for |
|---|---|---|
| Sync FFI | Smb2Client |
Scripts, background isolates, maximum control |
| Worker Pool | Smb2Pool |
Default. Async, multi-worker, auto-reconnect, scope-based file helpers |
1.1 Sync Client
The core layer. All operations are synchronous (they wait for the result before returning), so run it in a background isolate to keep your app responsive. The bundled native library loads automatically on every supported platform — no path configuration required.
final client = Smb2Client.open();
client.connect(
host: '192.168.1.100',
share: 'Files',
user: 'user',
password: 'pass',
domain: 'WORKGROUP', // optional, defaults to empty
timeoutSeconds: 30, // optional, defaults to 30
);
// ... use the client ...
client.disconnect();
1.2 Worker Pool
Spawns N isolate workers connected to the same share. Operations are dispatched round-robin. If a worker loses connection, it automatically reconnects and retries the operation.
For single-connection use cases (the old Smb2Isolate), set workers: 1 — you get the same single-connection semantics plus automatic reconnect.
final pool = await Smb2Pool.connect(
host: '192.168.1.100',
share: 'Files',
user: 'user',
password: 'pass',
workers: 4, // default: 4
timeoutSeconds: 30,
);
print('Active workers: ${pool.workerCount}');
final entries = await pool.listDirectory('');
await pool.disconnect();
1.3 Disconnecting
Always disconnect when done. This releases the native SMB2 context and kills any spawned isolates.
client.disconnect(); // Smb2Client (sync)
await pool.disconnect(); // Smb2Pool
1.4 Security Options
All connect methods accept optional security parameters:
final pool = await Smb2Pool.connect(
host: '192.168.1.100',
share: 'Files',
user: 'user',
password: 'pass',
seal: true, // encrypt all traffic (SMB 3.0+)
signing: true, // require message signing
version: Smb2Version.any3, // only accept SMB 3.x
);
| Parameter | Default | Description |
|---|---|---|
seal |
false |
Enable SMB3 encryption. All traffic is encrypted on the wire. Requires SMB 3.0 or later — the connection fails if the server only supports SMB 2.x. |
signing |
false |
Require message signing. Prevents tampering. Works with all SMB versions. The client will always sign if the server requires it, regardless of this setting. |
version |
Smb2Version.any |
Protocol version to negotiate. Default lets the server pick the highest mutually supported version. Use any3 to enforce SMB 3.x (required for encryption). |
Available versions:
Smb2Version |
Protocol | Encryption support |
|---|---|---|
any |
Best available (default) | Depends on negotiated version |
any2 |
Any SMB 2.x | No |
any3 |
Any SMB 3.x | Yes |
v202 |
SMB 2.0.2 | No |
v210 |
SMB 2.1 | No |
v300 |
SMB 3.0 | Yes |
v302 |
SMB 3.0.2 | Yes |
v311 |
SMB 3.1.1 (most secure) | Yes |
Note: When
seal: trueis set, the connection will fail if the server does not support SMB 3.0+. This is by design — silent fallback to unencrypted would be a security violation.
2. Path Format #
Paths are relative to the share root. Use an empty string '' for the root directory — not /.
pool.listDirectory(''); // share root
pool.listDirectory('Documents'); // subfolder
pool.listDirectory('Documents/Projects'); // nested subfolder
pool.stat('Documents/report.pdf'); // file in subfolder
pool.writeFile('Documents/notes.txt', data); // write to subfolder
pool.mkdir('Documents/NewFolder'); // create in subfolder
pool.rename('Documents/a.txt', 'Archive/a.txt'); // move between folders
3. Directory Listing #
Returns all entries in a directory with full metadata (name, type, size, timestamps) — no additional per-entry round-trips. The . and .. entries are automatically filtered out.
final entries = await pool.listDirectory('Documents/Projects');
for (final entry in entries) {
final icon = entry.isDirectory ? '📁' : '📄';
print('$icon ${entry.name} ${entry.size} bytes ${entry.stat.modified}');
}
Each entry is an Smb2DirEntry:
| Property | Type | Description |
|---|---|---|
name |
String |
Entry name (not full path) |
stat |
Smb2Stat |
Full metadata (type, size, modified, created) |
isDirectory |
bool |
Shorthand for stat.type == directory |
isFile |
bool |
Shorthand for stat.type == file |
size |
int |
Shorthand for stat.size |
4. File Metadata #
4.1 Stat
Returns file metadata in a single, efficient network round-trip.
final info = await pool.stat('Documents/report.pdf');
print('Type: ${info.type}'); // Smb2FileType.file
print('Size: ${info.size}'); // bytes
print('Modified: ${info.modified}'); // DateTime
print('Created: ${info.created}'); // DateTime
print('Is file: ${info.isFile}'); // bool
print('Is dir: ${info.isDirectory}'); // bool
4.2 File Size
Shortcut to get just the file size.
final size = await pool.fileSize('Documents/report.pdf');
print('$size bytes');
4.3 Exists
Check whether a file or directory exists without reading it. Returns false for non-existent paths; throws on connection or permission errors.
// Sync
if (client.exists('Documents/report.pdf')) {
print('File exists');
}
// Async
if (await pool.exists('Documents/report.pdf')) {
print('File exists');
}
4.4 Filesystem Info (statvfs)
Query total and free disk space on the share.
// Sync
final vfs = client.statvfs('');
print('Total: ${vfs.totalSize} bytes');
print('Free: ${vfs.freeSize} bytes');
print('Available: ${vfs.availableSize} bytes');
// Async
final vfs = await pool.statvfs('');
Returns an Smb2StatVfs with convenience getters totalSize, freeSize, and availableSize (in bytes).
4.5 Read Symlink
Read the target path of a symbolic link.
// Sync
final target = client.readlink('Documents/shortcut');
// Async
final target = await pool.readlink('Documents/shortcut');
Throws Smb2Exception if the path is not a symlink.
4.6 Connection Health (echo)
Send a keepalive ping to the server. Returns normally if the connection is alive; throws on failure.
// Sync
client.echo();
// Async
await pool.echo();
Useful for detecting disconnections early during idle periods.
5. Reading Files #
5.1 Read Entire File
Loads the entire file into memory.
final bytes = await pool.readFile('Documents/report.pdf');
5.2 Partial Read (Byte Range)
Reads N bytes starting at an offset. Ideal for reading partial content without downloading the entire file.
// Read first 256 KB of a file
final header = await pool.readFileRange(
'Documents/report.pdf',
offset: 0, // default: 0
length: 256 * 1024,
);
5.3 Scoped File Access (withFile)
Opens a file for reading, runs your callback with a scoped [Smb2File], and guarantees the handle is closed on any exit path — including exceptions, early returns, and cancellation.
This is the recommended way to read a file when you need more than a single one-shot read (e.g. parsing metadata that may need ranged fallback reads).
final tags = await pool.withFile('Music/song.flac', (file) async {
print('File is ${file.size} bytes');
// Initial header read — typically enough for most tag formats.
final header = await file.read(length: 64 * 1024);
// Some tag formats need bytes beyond the header (e.g. Vorbis comments
// stored at arbitrary offsets). Expose `file.read` as a fallback reader
// and only fetch more bytes when the parser actually needs them.
return parseVorbisComments(
header,
fileSize: file.size,
fallbackRead: (offset, length) => file.read(offset: offset, length: length),
);
});
If you already know the file size from a prior stat or directory listing, pass it via knownSize to skip the fstat round-trip:
final stat = await pool.stat('Music/song.flac');
await pool.withFile(
'Music/song.flac',
(file) async { /* … */ },
knownSize: stat.size,
);
5.4 Streaming (Chunked)
Reads a file in chunks via a single persistent handle (one Create + N Read + one Close on the wire), with optional progress and cancellation callbacks.
Sync (Smb2Client):
for (final chunk in client.readFileChunked('data/large_file.bin', chunkSize: 1024 * 1024)) {
sink.add(chunk);
}
Async (Smb2Pool):
bool canceled = false;
await for (final chunk in pool.streamFile(
'data/large_file.bin',
chunkSize: 1024 * 1024,
onProgress: (received, total) {
print('${(received / total * 100).toStringAsFixed(1)}% '
'($received / $total bytes)');
},
isCanceled: () => canceled,
)) {
sink.add(chunk);
}
| Parameter | Default | Description |
|---|---|---|
chunkSize |
1 MiB |
Dart-side buffer per iteration. Network-level chunking is automatic (libsmb2 splits into server-negotiated MaxReadSize packets, typically 1 MiB). |
onProgress |
— | Called after each chunk with (received, total) byte counts. |
isCanceled |
— | Polled after each chunk. Returning true aborts the stream with Smb2Exception. |
The handle is closed automatically when the stream completes, errors, or the listener cancels its subscription.
5.5 Download to File
One-call convenience that streams an SMB file to a local File via [streamFile]. Equivalent to streaming and piping to File.openWrite(), with onProgress and isCanceled wired through.
import 'dart:io';
await pool.downloadToFile(
'Music/song.flac',
File('/tmp/song.flac'),
onProgress: (received, total) {
print('${(received / total * 100).toStringAsFixed(1)}%');
},
isCanceled: () => userHitCancelButton,
);
Atomicity: if the download is canceled or errors, the destination file is left as-is (truncated or partially written). For safe replacement of an existing file, write to
dest.partand rename on success.
5.6 Low-Level File Handles
When you need finer control than withFile provides — e.g. a handle that outlives a single scope, or reusing a handle across multiple independent reads — the raw open/close primitives are still available.
Prefer
withFile,streamFile, ordownloadToFilewhenever you can. They are safer (guaranteed cleanup) and more efficient (persistent handle). Reach for the raw API only when those don't fit.
Sync (Smb2Client):
final handle = client.openFileHandle('data/file.bin');
final start = client.readHandle(handle, offset: 0, length: 4096);
final mid = client.readHandle(handle, offset: 100000, length: 4096);
client.closeHandle(handle);
With size (Smb2Client):
final (handle, size) = client.openFileWithSize('data/file.bin');
print('File is $size bytes');
// ... read from handle ...
client.closeHandle(handle);
Pool (auto-reconnect on failure):
final (handle, size) = await pool.openFileWithSize('data/file.bin');
try {
final data = await pool.readFromHandle(handle, offset: 0, length: size);
// ...
} finally {
await pool.closeHandle(handle);
}
If the connection drops during a read, the pool automatically reconnects the worker and reopens the file handle before retrying.
Safety net: if an
Smb2PoolHandleis garbage-collected withoutcloseHandlebeing called, acloseHandlecommand is sent to the worker from the finalizer as a best-effort cleanup. Do not rely on this — always close explicitly (or usewithFile) for deterministic, prompt cleanup.
6. Writing Files #
6.1 Write Entire File
Creates or overwrites a file with the given data. The file is truncated before writing.
Sync (Smb2Client):
client.writeFile('Documents/notes.txt', Uint8List.fromList('Hello!'.codeUnits));
Async (Smb2Pool):
await pool.writeFile('Documents/notes.txt', Uint8List.fromList('Hello!'.codeUnits));
6.2 Partial Write (Byte Range)
Writes data at a specific offset without truncating the file. Creates the file if it doesn't exist.
Sync (Smb2Client):
// Overwrite bytes 100–199 of an existing file
client.writeFileRange('data/file.bin', myBytes, offset: 100);
Async (Smb2Pool):
await pool.writeFileRange('data/file.bin', myBytes, offset: 100);
6.3 Streaming Write (Chunked)
Writes data from chunks without loading the entire file into RAM. Uses a sync Iterable on Smb2Client and an async Stream on Smb2Pool.
Sync (Smb2Client):
client.writeFileChunked('data/large_file.bin', generateChunks());
Async (Smb2Pool):
final fileStream = File('local_file.bin').openRead().cast<Uint8List>();
await pool.streamWrite('data/large_file.bin', fileStream);
6.4 File Handles (Write)
Open a file once for writing and write to it multiple times without reopening. This minimizes round-trips for repeated writes on the same file.
Sync (Smb2Client):
final handle = client.openFileHandleWrite('data/output.bin');
client.writeHandle(handle, chunk1, offset: 0);
client.writeHandle(handle, chunk2, offset: chunk1.length);
client.closeHandle(handle);
Pool (auto-reconnect on failure):
final handle = await pool.openFileWrite('data/output.bin');
await pool.writeToHandle(handle, chunk1, offset: 0);
await pool.writeToHandle(handle, chunk2, offset: chunk1.length);
await pool.closeHandle(handle);
Note: Write handles use the same
closeHandle()method as read handles.
6.5 Flush (fsync)
Flush all buffered writes on an open file handle to the server, ensuring data is persisted.
Sync (Smb2Client):
final handle = client.openFileHandleWrite('data/important.bin');
client.writeHandle(handle, data);
client.fsync(handle); // ensure data is on disk
client.closeHandle(handle);
Pool:
final handle = await pool.openFileWrite('data/important.bin');
await pool.writeToHandle(handle, data);
await pool.fsyncHandle(handle);
await pool.closeHandle(handle);
6.6 Truncate Handle (ftruncate)
Truncate an open file handle to a specific size. More efficient than path-based truncate() when the file is already open.
Sync (Smb2Client):
final handle = client.openFileHandleWrite('data/file.bin');
client.ftruncate(handle, 1024);
client.closeHandle(handle);
Pool:
final handle = await pool.openFileWrite('data/file.bin');
try {
await pool.ftruncateHandle(handle, 1024);
} finally {
await pool.closeHandle(handle);
}
7. File & Directory Management #
7.1 Create Directory
// Sync
client.mkdir('Documents/NewFolder');
// Async
await pool.mkdir('Documents/NewFolder');
7.2 Delete File
// Sync
client.deleteFile('Documents/old_report.pdf');
// Async
await pool.deleteFile('Documents/old_report.pdf');
7.3 Delete Directory
Removes an empty directory. Throws Smb2Exception if the directory is not empty.
// Sync
client.rmdir('Documents/EmptyFolder');
// Async
await pool.rmdir('Documents/EmptyFolder');
7.4 Rename / Move
Renames or moves a file or directory within the same share.
// Sync
client.rename('Documents/old_name.txt', 'Documents/new_name.txt');
// Async — also works for moving between directories
await pool.rename('Documents/report.pdf', 'Archive/report.pdf');
7.5 Truncate
Truncates a file to a specific size in bytes.
// Sync
client.truncate('Documents/logfile.txt', 0); // empty the file
// Async
await pool.truncate('Documents/logfile.txt', 1024); // keep first 1 KB
8. Share Enumeration #
List all shares available on a server. This does not require an active connection — it connects to IPC$ internally.
Sync:
final shares = client.listShares(
host: '192.168.1.100',
user: 'user',
password: 'pass',
);
for (final share in shares) {
print('${share.name} — disk: ${share.isDisk}, hidden: ${share.isHidden}');
}
Pool (static method, no active connection needed):
final shares = await Smb2Pool.listSharesOn(
host: '192.168.1.100',
user: 'user',
password: 'pass',
);
Each share is an Smb2ShareInfo with:
| Property | Type | Description |
|---|---|---|
name |
String |
Share name |
type |
int |
Raw share type |
isDisk |
bool |
True if disk/folder share |
isHidden |
bool |
True if hidden share |
9. Error Handling #
9.1 Smb2Exception
All errors throw Smb2Exception with a message, an optional POSIX errno, and a semantic error type.
try {
await pool.readFile('nonexistent/path.txt');
} on Smb2Exception catch (e) {
print(e.message); // Human-readable error from libsmb2
print(e.errorCode); // POSIX errno (nullable)
print(e.type); // Smb2ErrorType
print(e.isConnectionError); // true for retriable errors
}
9.2 Error Types
Smb2ErrorType maps native errno values to semantic categories:
| Type | Meaning | errno examples |
|---|---|---|
connection |
Network disconnected, pipe broken | ENETRESET, ECONNRESET, ECONNABORTED, EPIPE, ENOTCONN, ENETDOWN, ENETUNREACH, EHOSTDOWN, EHOSTUNREACH |
timeout |
Operation timed out | ETIMEDOUT |
auth |
Authentication failed | ECONNREFUSED |
fileNotFound |
File or path not found | ENOENT |
accessDenied |
Permission denied | EACCES |
notADirectory |
Path is not a directory | ENOTDIR |
alreadyExists |
Target already exists | EEXIST |
diskFull |
No space left on device | ENOSPC |
io |
I/O error | EIO |
invalidParam |
Invalid argument | EINVAL |
unknown |
Unmapped error code | — |
Use isConnectionError to decide whether to retry:
on Smb2Exception catch (e) {
if (e.isConnectionError) {
// Safe to retry — connection or timeout issue
}
}
Note:
Smb2Poolhandles reconnection automatically. You only need manual retry logic withSmb2Client.
Types Reference #
Smb2Stat #
| Property | Type | Description |
|---|---|---|
type |
Smb2FileType |
file, directory, or link |
size |
int |
Size in bytes |
modified |
DateTime |
Last modification time |
created |
DateTime |
Creation time |
isFile |
bool |
true if regular file |
isDirectory |
bool |
true if directory |
Smb2DirEntry #
| Property | Type | Description |
|---|---|---|
name |
String |
Entry name (not full path) |
stat |
Smb2Stat |
Full metadata |
isDirectory |
bool |
Shorthand |
isFile |
bool |
Shorthand |
size |
int |
Shorthand for stat.size |
Smb2StatVfs #
| Property | Type | Description |
|---|---|---|
blockSize |
int |
Fundamental block size |
fragmentSize |
int |
Fragment size |
totalBlocks |
int |
Total data blocks |
freeBlocks |
int |
Free blocks |
availableBlocks |
int |
Available blocks for non-root |
maxNameLength |
int |
Max filename length |
totalSize |
int |
Total size in bytes (computed) |
freeSize |
int |
Free space in bytes (computed) |
availableSize |
int |
Available space in bytes (computed) |
Smb2ShareInfo #
| Property | Type | Description |
|---|---|---|
name |
String |
Share name |
type |
int |
Raw share type |
isDisk |
bool |
True if disk/folder share |
isHidden |
bool |
True if hidden share |
Smb2Version #
| Value | Protocol |
|---|---|
any |
Best available (default) |
any2 |
Any SMB 2.x |
any3 |
Any SMB 3.x |
v202 |
SMB 2.0.2 |
v210 |
SMB 2.1 |
v300 |
SMB 3.0 |
v302 |
SMB 3.0.2 |
v311 |
SMB 3.1.1 |
Smb2FileType #
| Value | Description |
|---|---|
file |
Regular file |
directory |
Directory |
link |
Symbolic link |
Smb2ErrorType #
| Value | Description |
|---|---|
connection |
Network disconnected |
timeout |
Operation timed out |
auth |
Authentication failed |
fileNotFound |
File or path not found |
accessDenied |
Permission denied |
notADirectory |
Path is not a directory |
alreadyExists |
Target already exists |
diskFull |
No space left |
io |
I/O error |
invalidParam |
Invalid argument |
unknown |
Unmapped error |
Permissions #
Android #
<uses-permission android:name="android.permission.INTERNET" />
macOS #
Add to DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
Project background #
All the native bindings, FFI wrappers, and isolate logic were implemented through the use of Claude Code.
Developed by Alessandro Di Ronza





