dart_smb2 0.1.0 copy "dart_smb2: ^0.1.0" to clipboard
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.

logo 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 pool
Smb2Pool spreads requests across N isolate workers and reconnects transparently when a connection drops.
File & directory ops
listDirectory, 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 handles
withFile(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 info
statvfs 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: true is 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).

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.part and 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, or downloadToFile whenever 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 Smb2PoolHandle is garbage-collected without closeHandle being called, a closeHandle command is sent to the worker from the finalizer as a best-effort cleanup. Do not rely on this — always close explicitly (or use withFile) 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: Smb2Pool handles reconnection automatically. You only need manual retry logic with Smb2Client.


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

5
likes
160
points
359
downloads
screenshot

Documentation

API reference

Publisher

verified publisherales-drnz.com

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#cross-platform #smb2 #smb3 #samba #libsmb2

License

BSD-3-Clause (license)

Dependencies

ffi, meta

More

Packages that depend on dart_smb2

Packages that implement dart_smb2