diff --git a/README.md b/README.md index 4d288f9..7515b0b 100644 --- a/README.md +++ b/README.md @@ -253,15 +253,20 @@ await crypto.decryptMessage(...) import { downloadUnsealed } from "@yeez-tech/meta-encryptor"; await downloadUnsealed({ - url: "https://example.com/encrypted file", - privateKeyHex: "YOUR_PRIVATE_KEY_HEX", + url: "https://example.com/encrypted-file", + privateKey: "YOUR_PRIVATE_KEY_HEX", filename: "decrypted-file.txt", - progressHandler: (totalItems, processedItems, readBytes, writeBytes) => { - console.log(`Progress: ${processedItems}/${totalItems}`); + onProgress: (total, processed, readBytes, writeBytes) => { + console.log(`Progress: ${processed}/${total}`); }, + // 可选:覆盖明文大小上限(字节);默认桌面 1 GiB、移动端 200 MiB + // desktopLimit: 2 * 1024 * 1024 * 1024, + // mobileLimit: 500 * 1024 * 1024, }); ``` +更完整的参数说明见 [`src/browser/README.md`](./src/browser/README.md)。 + ##### sealedFileVersion 返回封装文件的版本号。 diff --git a/jest.config.browser.mjs b/jest.config.browser.mjs index 8a05d22..87e41c8 100644 --- a/jest.config.browser.mjs +++ b/jest.config.browser.mjs @@ -7,6 +7,7 @@ export default { "/test/downloadFunctions.spec.mjs", "/test/downloadCallbacks.spec.mjs", "/test/downloadUnsealedCallbacks.spec.mjs", + "/test/downloadUnsealedSizeLimit.spec.mjs", "/test/progress.spec.mjs", ], diff --git a/package.json b/package.json index af974fe..c04e4d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yeez-tech/meta-encryptor", - "version": "5.0.7", + "version": "5.1.0", "description": "Data Seal/Unseal for Fidelius", "main": "./build/commonjs/index.node.cjs", "module": "./build/es/index.browser.js", diff --git a/src/browser/README.md b/src/browser/README.md index f30d979..08b98ac 100644 --- a/src/browser/README.md +++ b/src/browser/README.md @@ -50,6 +50,10 @@ npm install @yeez-tech/meta-encryptor | `onDownloadReady` | function | ❌ | 下载就绪回调:HTTP 流首个数据块进入管道时触发(HEAD+tail 完成后),可用于关闭准备蒙层 | | `onSuccess` | function | ❌ | 成功回调 `(data: { filename }) => void` | | `onError` | function | ❌ | 错误回调 `(error: Error) => void` | +| `desktopLimit` | number | ❌ | 桌面端明文大小上限(字节),默认 `1 GiB`(`1024 * 1024 * 1024`) | +| `mobileLimit` | number | ❌ | 移动端明文大小上限(字节),默认 `200 MiB`(`200 * 1024 * 1024`) | + +超过对应上限时抛出 `MetaEncryptorError`(`code: ERR_FILE_TOO_LARGE`)。不传或非法值时回退到平台默认值。 #### 返回值 @@ -88,6 +92,19 @@ try { ## 使用示例 +### 自定义大小上限 + +```javascript +await downloadUnsealed({ + url, + privateKey, + filename, + // 可选:覆盖默认上限(字节) + desktopLimit: 2 * 1024 * 1024 * 1024, // 桌面 2 GiB + mobileLimit: 500 * 1024 * 1024, // 移动端 500 MiB +}); +``` + ### 示例 1:基本使用 ```javascript diff --git a/src/browser/downloadUnsealed.js b/src/browser/downloadUnsealed.js index 9352a89..b395c16 100644 --- a/src/browser/downloadUnsealed.js +++ b/src/browser/downloadUnsealed.js @@ -88,6 +88,8 @@ async function inspectSealed(url, log) { * @param {Function} [options.onDownloadReady] - HTTP 流首个数据块进入管道时触发(可关蒙层) * @param {Function} [options.onSuccess] * @param {Function} [options.onError] + * @param {number} [options.desktopLimit] - desktop plaintext size limit in bytes (default 1 GiB) + * @param {number} [options.mobileLimit] - mobile plaintext size limit in bytes (default 200 MiB) * @returns {Promise} */ export async function downloadUnsealed({ @@ -98,7 +100,11 @@ export async function downloadUnsealed({ onProgress, onDownloadReady, onSuccess, - onError + onError, + /** Override desktop plaintext size limit (bytes). Default 1 GiB. */ + desktopLimit, + /** Override mobile plaintext size limit (bytes). Default 200 MiB. */ + mobileLimit, }) { const log = makeLogger(onLog); const key = privateKey.trim() @@ -116,7 +122,10 @@ export async function downloadUnsealed({ log(`Plaintext size=${meta.plaintextSize} bytes, sealed=${meta.sealedContentSize} bytes, totalSize=${meta.totalSize}`); const mobile = isMobile() - const limit = mobile ? MOBILE_LIMIT : DESKTOP_LIMIT + const configured = mobile ? mobileLimit : desktopLimit + const platformDefault = mobile ? MOBILE_LIMIT : DESKTOP_LIMIT + const limit = + typeof configured === 'number' && configured > 0 ? configured : platformDefault const mode = mobile ? 'mobile' : 'desktop' const ua = typeof navigator !== 'undefined' ? navigator.userAgent : 'n/a'; log(`Detected ${mode} (ua=${ua}), limit ${(limit / 1024 / 1024).toFixed(0)} MB`); diff --git a/src/browser/stream_download.js b/src/browser/stream_download.js index 94eb67c..a5d7949 100644 --- a/src/browser/stream_download.js +++ b/src/browser/stream_download.js @@ -3,6 +3,8 @@ import { HttpSealedFileStream } from './HttpSealedFileStream.js' import { MetaEncryptorError } from '../common/errors.js'; import { createProgressTransformer, createDownloadReadyTransformer } from '../common/progress.js'; +const STREAMSAVER_LOAD_TIMEOUT_MS = 8000; + async function ensureStreamSaver(log) { if (typeof window === 'undefined') return null; if (window.streamSaver?.createWriteStream) return window.streamSaver; @@ -12,8 +14,9 @@ async function ensureStreamSaver(log) { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js'; - s.onload = resolve; - s.onerror = reject; + const timer = setTimeout(() => reject(new Error('StreamSaver load timeout')), STREAMSAVER_LOAD_TIMEOUT_MS); + s.onload = () => { clearTimeout(timer); resolve(); }; + s.onerror = (err) => { clearTimeout(timer); reject(err); }; document.head.appendChild(s); }); if (window.streamSaver?.createWriteStream) { diff --git a/src/common/locale.js b/src/common/locale.js index 0e6f960..5a1772b 100644 --- a/src/common/locale.js +++ b/src/common/locale.js @@ -1,5 +1,5 @@ -import en from '../locales/en.json'; -import zhCN from '../locales/zh-CN.json'; +import en from '../locales/en.json' with { type: 'json' }; +import zhCN from '../locales/zh-CN.json' with { type: 'json' }; const _bundled = { en, 'zh-CN': zhCN }; let _messages = null; diff --git a/test/downloadFunctions.spec.mjs b/test/downloadFunctions.spec.mjs index b935d21..dc797e6 100644 --- a/test/downloadFunctions.spec.mjs +++ b/test/downloadFunctions.spec.mjs @@ -62,7 +62,7 @@ describe('blobDownloadAndDecrypt', () => { ); expect(result).toEqual({ ok: true }); - expect(logs.some(m => m.includes('下载完成'))).toBe(true); + expect(logs.some(m => m.includes('Download complete'))).toBe(true); expect(blobContent).not.toBeNull(); } finally { URL.createObjectURL = origCOU; @@ -80,7 +80,7 @@ describe('blobDownloadAndDecrypt', () => { await expect( blobDownloadAndDecrypt('http://mock/file', 'a'.repeat(64), 'out.bin', { log: (m) => logs.push(m) }) ).rejects.toThrow('fetch fail'); - expect(logs.some(m => m.includes('失败'))).toBe(true); + expect(logs.some(m => /failed/i.test(m))).toBe(true); }); }); @@ -98,16 +98,30 @@ describe('streamDownloadAndDecrypt', () => { ); expect(result).toEqual({ ok: true }); - expect(logs.some(m => m.includes('下载完成'))).toBe(true); + expect(logs.some(m => m.includes('Download complete'))).toBe(true); expect(chunks.length).toBe(1); expect(new TextDecoder().decode(chunks[0])).toBe('hello from mock'); }); test('throws when no writable and no StreamSaver', async () => { + // jsdom does not load external