# smart-file-transfer **Repository Path**: wuyao-fee/smart-file-transfer ## Basic Information - **Project Name**: smart-file-transfer - **Description**: 一个专注核心调度、解决大文件传输痛点的开源 npm 包。不绑定 React/Vue,不绑定 axios/fetch,把传输策略与业务接口彻底解耦,只做最硬核的「调度 + 重试 + 进度」三件事。 - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-03 - **Last Updated**: 2026-09-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # smart-file-transfer > 零框架依赖的大文件传输利器:**切片上传 / 断点续传 / 秒传 / 分片下载 / 并发调度 / 精准进度** 一个专注核心调度、解决大文件传输痛点的开源 npm 包。不绑定 React/Vue,不绑定 axios/fetch,把传输策略与业务接口彻底解耦,只做最硬核的「调度 + 重试 + 进度」三件事。 [![npm version](https://img.shields.io/npm/v/smart-file-transfer.svg)](https://www.npmjs.com/package/smart-file-transfer) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) --- ## 为什么用它 - **零运行时依赖**:核心代码不依赖 React/Vue/axios,Hash 用浏览器原生 Web Crypto API(`crypto.subtle`),无需安装任何额外包。 - **职责清晰**:`FileChunker` / `ChunkUploader` / `FileDownloader` / `SmartTransfer` 四件套,从底层调度到极简封装一应俱全。 - **能力完整**:秒传、断点续传、并发控制、失败重试(指数退避)、精准进度、AbortController 中止。 - **接口可移植**:上传/下载的具体请求由业务侧提供,包只负责调度,可对接任意后端协议。 - **路径全可定制**:`SmartTransfer` 默认约定 `/check /chunk /merge`,路径不同时通过 `resolveEndpoints` 覆盖,分片路径支持按 index 动态生成(RESTful)。 - **TS 友好**:完整类型定义,泛型化的钩子签名。 - **体积小**:tree-shake 友好,按需引入。 --- ## 安装 ```bash npm i smart-file-transfer ``` > 零运行时依赖,开箱即用。Hash 基于 Web Crypto API(`crypto.subtle.digest`),在所有现代浏览器与支持 `webcrypto` 的 Node(≥16)中均可直接使用。 --- ## 四件套总览 | 模块 | 职责 | 关键能力 | | ---------------- | ---------------------------------------------- | --------------------------------------------------- | | `SmartTransfer` | 极简封装,3 行代码完成上传/下载 | 路径全可定制、约定协议、RESTful 动态路径 | | `FileChunker` | 纯前端切片 + Web Worker 算 Hash | 流式切片、Worker 异步 Hash(SHA-256)、主线程回退 | | `ChunkUploader` | 并发控制 + 失败重试 + 精准进度 | 秒传、断点续传、并发度、指数退避、AbortController | | `FileDownloader` | Range 分片拉取 + 并发调度 + Blob 合并 | HEAD 探测、Range 切片、按序合并、不支持 Range 时回退 | > **推荐从 `SmartTransfer` 开始**:90% 场景下你只需要它的两个静态方法。需要深度定制时再下沉到 `FileChunker` / `ChunkUploader` / `FileDownloader`。 --- ## 快速上手 ### 0. 极简封装(推荐)—— 3 行代码完成上传/下载 ```ts import { SmartTransfer } from 'smart-file-transfer'; // 上传(自动切片 + Hash + 秒传/续传检查 + 并发 + 重试 + 合并) await SmartTransfer.upload(file, '/api/upload', { onProgress: (p) => console.log(`upload ${Math.floor(p * 100)}%`), }); // 下载(HEAD 探测 + Range 分片 + 并发 + Blob 合并) const blob = await SmartTransfer.download('/api/files/abc', { onProgress: (p) => console.log(`download ${Math.floor(p * 100)}%`), }); ``` **后端约定协议**(`SmartTransfer` 默认): | 路径 | 方法 | 请求体 | 响应 | | --- | --- | --- | --- | | `{endpoint}/check` | POST | JSON `{ fileHash, totalChunks, fileSize, fileName }` | JSON `{ instantMatch, uploadedChunks }` | | `{endpoint}/chunk` | POST | FormData `{ file, index, fileHash, totalChunks }` | 任意 2xx | | `{endpoint}/merge` | POST | JSON `{ fileHash, fileName, totalChunks, fileSize }` | 任意 2xx | | `{url}` (下载) | HEAD | — | `Accept-Ranges: bytes` + `Content-Length` | | `{url}` (下载) | GET | `Range: bytes=start-end` | `206 Partial Content` | **路径定制**(路径与默认约定不同时): ```ts // 场景 1:改名 await SmartTransfer.upload(file, '/api', { resolveEndpoints: (b) => ({ check: `${b}/verify`, chunk: `${b}/upload`, merge: `${b}/combine`, }), }); // 场景 2:加版本号 await SmartTransfer.upload(file, '/api', { resolveEndpoints: (b) => ({ check: `${b}/v2/check`, chunk: `${b}/v2/chunk`, merge: `${b}/v2/merge`, }), }); // 场景 3:RESTful 动态分片路径(chunk 是函数,按 index 生成) await SmartTransfer.upload(file, '/api', { resolveEndpoints: (b) => ({ check: `${b}/files/check`, chunk: (ctx) => `${b}/files/${ctx.fileHash}/chunks/${ctx.index}`, merge: `${b}/files/merge`, }), // index 已在 URL,FormData 里不必再塞 buildChunkBody: (chunk, meta) => { const fd = new FormData(); fd.append('file', chunk.blob); return { body: fd }; }, }); ``` ### 1. 底层三件套上传(含秒传 + 断点续传) ```ts import { FileChunker, ChunkUploader } from 'smart-file-transfer'; async function uploadFile(file: File) { // ① 切片 + 算 Hash(Worker 中执行,不卡 UI) const chunker = new FileChunker(file, { chunkSize: 5 * 1024 * 1024, onHashProgress: (p) => console.log('hash:', (p * 100).toFixed(1) + '%'), }); const { fileHash, chunks, totalSize, totalChunks } = await chunker.chunkAndHash(); // ② 上传(业务侧提供 check / uploadChunk / merge 三个接口) const uploader = new ChunkUploader({ concurrency: 4, maxRetries: 3, // 秒传/续传检查:问服务端这个 hash 已存在哪些分片 async check(hash, total, size) { const resp = await fetch(`/api/upload/check?hash=${hash}&total=${total}&size=${size}`); const data = await resp.json(); return { instantMatch: data.completed, // 整文件秒传命中 uploadedChunks: data.uploadedChunks ?? [], // 已存在的分片序号 }; }, // 上传单片 async uploadChunk(chunk, hash, total, onProgress, signal) { const form = new FormData(); form.append('hash', hash); form.append('index', String(chunk.index)); form.append('total', String(total)); form.append('chunk', chunk.blob); // 用 XMLHttpRequest 才能拿到上传进度 await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload/chunk'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) onProgress(e.loaded / e.total); }; xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(xhr.statusText))); xhr.onerror = () => reject(new Error('network error')); signal.addEventListener('abort', () => xhr.abort()); xhr.send(form); }); }, // 全部分片就位后通知服务端合并 async merge(hash, name, total, size) { await fetch('/api/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash, name, total, size }), }); }, onProgress: (p, sent, total) => console.log(`upload: ${(p * 100).toFixed(1)}%`), onSuccess: () => console.log('done!'), }); await uploader.upload({ fileHash, chunks, totalSize, totalChunks }, file.name); } ``` ### 2. 下载(Range 分片 + 并发) ```ts import { FileDownloader } from 'smart-file-transfer'; async function downloadFile(url: string) { const downloader = new FileDownloader({ chunkSize: 5 * 1024 * 1024, concurrency: 4, onProgress: (p, got, total) => console.log(`download: ${(p * 100).toFixed(1)}%`), }); const blob = await downloader.download(url); // 触发浏览器保存 const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'file.bin'; a.click(); } ``` ### 3. 中止操作 ```ts const controller = new AbortController(); const uploader = new ChunkUploader({ signal: controller.signal, uploadChunk: /* ... */, }); // 用户点取消时 controller.abort(); ``` --- ## API 速查 ### `SmartTransfer`(推荐入口) ```ts // 上传 SmartTransfer.upload(file: File | Blob, endpoint: string, options?: { chunkSize?: number; // 默认 5MB concurrency?: number; // 默认 3 maxRetries?: number; // 默认 3 timeoutMs?: number; headers?: Record; withCredentials?: boolean; signal?: AbortSignal; hash?: boolean; // 默认 true(开启秒传/续传) useWorker?: boolean; // 默认 true resolveEndpoints?: (endpoint: string) => { check: string; chunk: string | ((ctx: { index, fileHash, totalChunks, base }) => string); merge: string; }; buildChunkBody?: (chunk, meta) => { body: BodyInit; headers?: Record }; parseCheckResponse?: (resp: Response) => Promise<{ instantMatch, uploadedChunks, extra? }>; onProgress?: (p, uploaded, total) => void; onHashProgress?: (p, current, total) => void; onChunkSuccess?: (index) => void; onChunkError?: (index, error, retryLeft) => void; onSuccess?: () => void; onError?: (error) => void; }): Promise // 下载 SmartTransfer.download(url: string, options?: { chunkSize?: number; // 默认 5MB concurrency?: number; // 默认 3 maxRetries?: number; timeoutMs?: number; headers?: Record; withCredentials?: boolean; signal?: AbortSignal; fetch?: typeof fetch; onProgress?: (p, downloaded, total) => void; onChunkSuccess?: (index, size) => void; onChunkError?: (index, error, retryLeft) => void; onSuccess?: (blob) => void; onError?: (error) => void; }): Promise ``` | 方法 | 说明 | | --- | --- | | `SmartTransfer.upload(file, endpoint, options?)` | 极简上传:自动切片 + Hash + 秒传/续传 + 并发 + 重试 + 合并 | | `SmartTransfer.download(url, options?)` | 极简下载:HEAD + Range 分片 + 并发 + Blob 合并 | ### `FileChunker` ```ts new FileChunker(file: File | Blob, options?: { chunkSize?: number; // 默认 5MB useWorker?: boolean; // 默认 true hashAlgorithm?: 'sha256' | 'none'; // 默认 'sha256' onHashProgress?: (p, current, total) => void; }) ``` | 方法 | 返回 | 说明 | | ------------------- | --------------------------- | ------------------------------------- | | `.chunk()` | `ChunkInfo[]` | 仅切片,不计算 Hash | | `.chunkAndHash()` | `Promise` | 切片 + Hash(Worker,回退主线程) | | `.computeHash()` | `Promise` | 单独算 Hash | > **Hash 算法**:`SHA-256 of (SHA-256 of each chunk)` —— 先对每个分片算 SHA-256 得到 32 字节摘要,再把所有摘要按序拼接后算一次 SHA-256 作为文件 Hash。增量计算、内存友好,仅依赖 `crypto.subtle`。服务端可按相同规则复算以校验秒传/断点续传。 ### `ChunkUploader` ```ts new ChunkUploader({ concurrency?: number; // 默认 3 maxRetries?: number; // 默认 3 retryBackoffMs?: number; // 默认 1000 retryBackoffFactor?: number; // 默认 2 timeoutMs?: number; // 单片超时 signal?: AbortSignal; check?: (hash, total, size) => Promise<{ instantMatch; uploadedChunks; extra? }>; uploadChunk: (chunk, hash, total, onProgress, signal) => Promise; merge?: (hash, name, total, size) => Promise; onProgress?: (p, uploaded, total) => void; onChunkSuccess?: (chunk) => void; onChunkError?: (chunk, error, retryLeft) => void; onSuccess?: () => void; onError?: (error) => void; }) ``` | 方法 | 说明 | | ------------------------------------- | ----------------------------- | | `.upload(chunkResult, fileName?)` | 主流程:秒传检查 → 并发上传 → merge | | `.uploadChunks(chunks, hash, size, name?)` | 便捷重载 | ### `FileDownloader` ```ts new FileDownloader({ chunkSize?: number; // 默认 5MB concurrency?: number; // 默认 3 maxRetries?: number; // 默认 3 retryBackoffMs?: number; retryBackoffFactor?: number; timeoutMs?: number; headers?: Record; withCredentials?: boolean; signal?: AbortSignal; fetchChunk?: (url, start, end, signal) => Promise; onProgress?: (p, got, total) => void; onChunkSuccess?: (index, size) => void; onChunkError?: (index, error, retryLeft) => void; onSuccess?: (blob) => void; onError?: (error) => void; }) ``` | 方法 | 返回 | 说明 | | ------------------- | ----------------- | ----------------------------------------------- | | `.head(url)` | `DownloadHeadInfo`| 探测是否支持 Range / 文件大小 / 文件名 | | `.download(url)` | `Promise` | 下载并合并;不支持 Range 时自动回退为整文件下载 | --- ## 错误处理 所有错误都是 `TransferError` 实例,带 `code` 字段: ```ts import { TransferError, ErrorCode } from 'smart-file-transfer'; try { await uploader.upload(...); } catch (e) { if (e instanceof TransferError) { switch (e.code) { case ErrorCode.Aborted: // 用户中止 case ErrorCode.MaxRetryExceeded: // 重试耗尽 case ErrorCode.NetworkError: // 网络错误 case ErrorCode.HttpError: // HTTP 非 2xx case ErrorCode.VerifyFailed: // 秒传/校验失败 case ErrorCode.HashFailed: // Hash 计算失败 case ErrorCode.InvalidParams: // 参数非法 } } } ``` --- ## 设计要点 ### 精准进度算法 `ChunkUploader` 的进度不是「已完成分片数 / 总分片数」这种粗粒度,而是: ``` 已上传字节 = 已完成分片字节数 + Σ(进行中分片的实时字节) 进度 = 已上传字节 / 总字节 ``` 通过 `chunkLiveBytes` Map 实时追踪每个进行中分片的进度,再聚合到全局进度。即使单片很大也能给出平滑的进度曲线。 ### 重试退避 ``` delay = retryBackoffMs × retryBackoffFactor ^ attempt ``` 默认 1000ms × 2^n:1s → 2s → 4s,避免在服务端过载时继续打雷。 ### Range 下载合并 分片按原始顺序下载到 `buffers[index]`,最后用 `new Blob(buffers)` 合并。Blob 构造函数接受 `ArrayBuffer[]` 并按数组顺序拼接,保证字节序正确。 ### Worker Hash Worker 源码以**字符串内联**方式打包进产物,运行时通过 `new Blob([code]).createObjectURL()` 创建 Worker。这种方式的优势: - **零外部文件**:不依赖 `new URL('./hash.worker.ts', import.meta.url)`,tsup/webpack/Vite 打包后不会出现 Worker 路径错乱。 - **开箱即用**:`dist/index.cjs` / `dist/index.js` 单文件即可运行,无需额外配置 `worker` loader。 - **自动回退**:Worker 创建失败(如 Node 环境、旧浏览器)时自动回退到主线程同步计算,功能不受影响。 > Hash 算法为 `SHA-256 of (SHA-256 of each chunk)`:先对每个分片算 SHA-256,再把所有摘要拼接后算一次 SHA-256 作为文件 Hash。仅依赖 `crypto.subtle`,服务端可按相同规则复算以校验秒传/断点续传。 --- ## 开发 ```bash # 安装依赖 npm install # 类型检查 npm run typecheck # 构建(同时产出 ESM / CJS / d.ts) npm run build # 跑测试 npm test # 端到端验证 dist 产物(起本地 mock server,真实跑 SmartTransfer 完整链路) node verify.cjs # watch 模式 npm run dev ``` --- ## 兼容性 - 现代浏览器(Chrome/Edge/Firefox/Safari 最近 2 年版本) - Node 16+(用于 SSR 场景的类型导入) - 需要支持 `Worker` `AbortController` `FileReader` `Blob.slice` 等 Web API --- ## License MIT © smart-file-transfer contributors