@filebox/smb-client
v1.0.0
Published
Cross-platform SMB client for Node.js via NAPI-RS (WNet on Windows, pure-Rust smb2 elsewhere)
Maintainers
Readme
@filebox/smb-client
跨平台 SMB 客户端 Node.js 包(@filebox/smb-client),基于 Rust + NAPI-RS。
采用双后端、按平台取最优:
| 平台 | 会话/连接 | 文件操作 |
| --- | --- | --- |
| Windows | WNetAddConnection2W / WNetCancelConnection2W(OS 原生 MPR) | std::fs 访问 UNC 路径 \\server\share\... |
| Linux / macOS | 纯 Rust smb2 crate(SMB2/3,无 C 依赖) | 同一 smb2 crate 的读写 API |
所有操作放进 NAPI-RS 的异步任务池执行:Windows 端用 tokio::task::spawn_blocking
包裹阻塞的 WNet/std::fs 调用,Linux/macOS 端 smb2 本身是 async,直接在 napi 的
tokio 运行时上 await。均不阻塞 Node 事件循环。目标 Node.js >= 18。
构建
pnpm install
pnpm build # 在当前平台构建,生成 index.js / index.d.ts / smb-native.<platform>.nodeWindows 上只会编译 WNet 后端;Linux/macOS 上只会编译
smb2后端(#[cfg]切换,无 C 依赖,cargo build即可)。
用法
支持 CommonJS 和 ESM 双格式(由 package.json 的 exports 自动选择):
// CommonJS
const { SmbSession } = require('@filebox/smb-client');// ESM / TypeScript
import { SmbSession } from '@filebox/smb-client';const s = new SmbSession();
await s.connect({
server: '192.168.1.100', // Windows 也可写 '\\server';Linux/macOS 可带端口 'host:445'
share: 'shared',
username: 'user',
password: 'pass',
domain: 'WORKGROUP', // 可选
// 以下可选,仅 Linux/macOS 端生效(Windows 的 WNet 不支持调参)
port: 445,
timeoutMs: 5000,
compression: true,
autoReconnect: false,
});
// 列目录
const entries = await s.listDir('/'); // 或 listDir('/some/folder')
// -> [{ name, isDirectory, isFile, size }, ...]
// 递归遍历目录树(返回相对路径)
const tree = await s.walkDir('/folder', true);
// -> [{ path, name, isDirectory, isFile, size }, ...]
// 元信息
const st = await s.stat('/folder/file.txt');
// -> { isFile, isDirectory, size, readonly, created, modified, accessed } (时间为 epoch 毫秒)
// 读文件
const buf = await s.readFile('/folder/file.txt'); // Buffer
// 局部读(不载入整文件,适合大文件)
const head = await s.readFileRange('/folder/file.txt', 0, 1024); // 从 offset 0 读 1024 字节
// 读写
await s.writeFile('/folder/out.txt', Buffer.from('hi'));
await s.appendFile('/folder/out.txt', Buffer.from('!'));
// 目录
await s.mkdir('/newdir');
await s.mkdirP('/a/b/c'); // 递归创建
await s.rmdir('/newdir');
await s.rmdirAll('/old/tree'); // 递归删除
// 文件
await s.deleteFile('/folder/file.txt');
await s.rename('/old.txt', '/new.txt');
await s.copyFile('/a.txt', '/a.copy.txt');
await s.truncate('/a.txt', 100); // 截断/扩展到 100 字节
await s.setReadOnly('/a.txt', true);
await s.exists('/folder/file.txt'); // -> boolean
// 磁盘与共享
await s.fsInfo(); // -> { totalBytes, freeBytes, totalFreeBytes }
await s.listShares(); // -> [{ name, comment }, ...]
// 与本地磁盘互传(流式,不占大内存)
await s.download('/folder/big.iso', './big.iso');
await s.upload('./big.iso', '/folder/big.iso');
// 带进度回调(error-first:第一个参数为 null 表示成功)
await s.download('/folder/big.iso', './big.iso', (err, p) => {
if (err) return;
console.log(`${p.percent.toFixed(1)}% ${p.bytesTransferred}/${p.totalBytes}`);
});
// 流式读写(拉/推,带背压;JS 可用 Readable.from 包装)
const rs = await s.createReadStream('/folder/big.iso', { start: 0, end: 1 << 20, chunkSize: 1 << 20 });
const total = rs.size;
const chunks = [];
while (true) { const chunk = await rs.read(); if (chunk === null) break; chunks.push(chunk); }
await rs.close();
const ws = await s.createWriteStream('/folder/out.bin');
await ws.write(Buffer.from('hello'));
await ws.write(Buffer.from(' world'));
await ws.end();
console.log('written', ws.bytesWritten);
// 设置文件时间(epoch 毫秒;仅 Windows 支持)
await s.setTimes('/folder/a.txt', { modifiedMs: Date.now(), createdMs: 0, accessedMs: Date.now() });
await s.disconnect();API
| 方法 | 说明 |
| --- | --- |
| new SmbSession() | 创建会话对象 |
| s.isConnected | 是否已连接(getter) |
| s.connect(opts) | 建立连接 |
| s.disconnect() | 断开连接 |
| s.listDir(path?) | 列出目录条目 |
| s.walkDir(path?, recursive) | 递归遍历目录树(条目含相对 path) |
| s.listShares() | 列出服务器上的共享 |
| s.stat(path) | 获取文件/目录元信息 |
| s.exists(path) | 是否存在 |
| s.readFile(path) | 读取整个文件,返回 Buffer |
| s.readFileRange(path, offset, length) | 按区间读(大文件局部读取) |
| s.createReadStream(path, opts?) | 创建流式读流,返回 SmbReadStream |
| s.writeFile(path, buf) | 写入文件(覆盖) |
| s.appendFile(path, buf) | 追加写入 |
| s.createWriteStream(path, opts?) | 创建流式写流,返回 SmbWriteStream |
| s.copyFile(src, dst) | 在 share 内复制文件 |
| s.rename(from, to) | 重命名/移动(同一 share 内) |
| s.deleteFile(path) | 删除文件 |
| s.mkdir(path) / s.mkdirP(path) | 创建目录 / 递归创建 |
| s.rmdir(path) / s.rmdirAll(path) | 删除目录 / 递归删除 |
| s.truncate(path, len) | 截断/扩展文件到指定长度(字节) |
| s.setReadOnly(path, readonly) | 设置只读位 |
| s.setTimes(path, { createdMs?, modifiedMs?, accessedMs? }) | 设置文件时间(epoch 毫秒) |
| s.download(remotePath, localPath, onProgress?) | 从 SMB 下载到本地磁盘(流式 + 进度) |
| s.upload(localPath, remotePath, onProgress?) | 从本地上传到 SMB(流式 + 进度) |
| s.fsInfo() | 磁盘容量 { totalBytes, freeBytes, totalFreeBytes } |
所有方法均为 async,返回 Promise。两个后端 API 完全一致。
流类
SmbReadStream:size(getter)、read(): Promise<Buffer | null>(null表 EOF)、close(): Promise<void>SmbWriteStream:bytesWritten(getter)、write(chunk: Buffer): Promise<void>、end(): Promise<void>createReadStream选项{ start?, end?, chunkSize? };createWriteStream选项{ append? }
进度回调 (err, p: TransferProgress) => void(error-first,err 为 null 表示成功),p = { bytesTransferred, totalBytes, percent },从 Rust 异步任务池回调,不阻塞 Node。
connect(opts):{ server, share, username?, password?, domain?, port?, timeoutMs?, compression?, autoReconnect? }。
其中 port/timeoutMs/compression/autoReconnect 仅 Linux/macOS 端生效(Windows WNet 不支持调参,使用系统默认)。
平台差异
- Windows:用 OS 原生 WNet +
std::fs+GetDiskFreeSpaceExW+SetFileTime,性能最优;domain通过 NTLM 凭据正常传递;setTimes支持。 - Linux/macOS:
smb2纯 Rust 实现,走 TCP 445 + NTLM;server支持host或host:port;domain原生支持;port/timeoutMs/compression/autoReconnect映射到ClientConfig。 以下方法在该平台有限制:setReadOnly/setTimes:smb2 未暴露设置属性/时间 API → 返回not supported错误。truncate:通过读改写实现(读取整文件 → 截断/补零 → 写回),超大文件会占用内存。createWriteStream的append选项不支持 → 返回错误(用appendFile代替)。FileStat.readonly固定为false(smb2的FileInfo不返回此字段)。
性能说明
- Windows 端阻塞 I/O 全部走
tokio::spawn_blocking,不阻塞 Node 事件循环。 - Linux/macOS 端
smb2本身 async,在 napi 的 tokio 运行时上 await;流用'static的FileReader/FileWriter,流独立于会话(流式期间不持会话锁),多个流可并发跑在同一 SMB 会话上。 - 批量读写用 pipelined 变体(填充信用窗口,吞吐更高);下载/上传 1MB 分块 + 进度,内存恒定。
测试
需要一个可访问的 SMB 服务器,通过环境变量传入凭据:
set SMB_HOST=192.168.1.100
set SMB_SHARE=shared
set SMB_USER=user
set SMB_PASS=pass
set SMB_DOMAIN=WORKGROUP
pnpm test发布多平台预编译包
由于原生模块需在各目标平台编译,推荐用 CI 矩阵(GitHub Actions)在各平台跑
napi build --platform,再用 napi artifacts 收集,napi prepublish 生成
npm/<platform>/ 子包(optionalDependencies 自动按平台加载)。package.json 的
napi.targets 已配置 win32-x64-msvc / linux-x64-gnu / darwin-x64-gnu。
pnpm build && pnpm artifacts
pnpm prepublishOnly # 生成 npm/<platform>/ 子包