From d1fa2dc6b549d2dafaf1750e69604262f42d72ac Mon Sep 17 00:00:00 2001 From: abearxiong Date: Sun, 1 Feb 2026 15:09:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC=E8=87=B3?= =?UTF-8?q?=200.0.37=EF=BC=8C=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=A7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=88=86=E5=9D=97=E4=B8=8A=E4=BC=A0=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20SparkMD5=20=E8=AE=A1=E7=AE=97=20Blob=20?= =?UTF-8?q?=E5=93=88=E5=B8=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 ++- pnpm-lock.yaml | 16 ++++++++ query/query-resources/index.ts | 70 +++++++++++++++++++++++++++++++--- query/query-resources/utils.ts | 64 +++++++------------------------ 4 files changed, 99 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 3440881..4bb1906 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/api", - "version": "0.0.36", + "version": "0.0.37", "description": "", "main": "mod.ts", "scripts": { @@ -36,11 +36,13 @@ "dependencies": { "@kevisual/js-filter": "^0.0.5", "@kevisual/load": "^0.0.6", + "@types/spark-md5": "^3.0.5", "es-toolkit": "^1.44.0", "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "nanoid": "^5.1.6", - "path-browserify-esm": "^1.0.6" + "path-browserify-esm": "^1.0.6", + "spark-md5": "^3.0.2" }, "exports": { ".": "./mod.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bf99b9..4b18284 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@kevisual/load': specifier: ^0.0.6 version: 0.0.6 + '@types/spark-md5': + specifier: ^3.0.5 + version: 3.0.5 es-toolkit: specifier: ^1.44.0 version: 1.44.0 @@ -29,6 +32,9 @@ importers: path-browserify-esm: specifier: ^1.0.6 version: 1.0.6 + spark-md5: + specifier: ^3.0.2 + version: 3.0.2 devDependencies: '@kevisual/cache': specifier: ^0.0.5 @@ -350,6 +356,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/spark-md5@3.0.5': + resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -650,6 +659,9 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -915,6 +927,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/spark-md5@3.0.5': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -1201,6 +1215,8 @@ snapshots: '@types/node-forge': 1.3.11 node-forge: 1.3.1 + spark-md5@3.0.2: {} + supports-preserve-symlinks-flag@1.0.0: {} to-regex-range@5.0.1: diff --git a/query/query-resources/index.ts b/query/query-resources/index.ts index e9d9989..b9a1626 100644 --- a/query/query-resources/index.ts +++ b/query/query-resources/index.ts @@ -71,23 +71,83 @@ export class QueryResources { // Blob 类型时 hashContent 返回 Promise const hash = hashResult instanceof Promise ? await hashResult : hashResult; url.searchParams.set('hash', hash); + + // 判断是否需要分块上传(文件大于20MB) + const isBlob = content instanceof Blob; + const fileSize = isBlob ? content.size : new Blob([content]).size; + const CHUNK_THRESHOLD = 20 * 1024 * 1024; // 20MB + + if (fileSize > CHUNK_THRESHOLD && isBlob) { + // 使用分块上传 + return this.uploadChunkedFile(filepath, content, hash, opts); + } + const formData = new FormData(); - if (content instanceof Blob) { + if (isBlob) { formData.append('file', content); } else { formData.append('file', new Blob([content], { type })); } return adapter({ url: url.toString(), - headers: { ...this.header(opts?.headers, false) }, - params: { - hash: hash, - }, isPostFile: true, method: 'POST', body: formData, + timeout: 5 * 60 * 1000, // 5分钟超时 + ...opts, + headers: { ...opts?.headers, ...this.header(opts?.headers, false) }, + params: { + hash: hash, + ...opts?.params, + }, }); } + async uploadChunkedFile(filepath: string, file: Blob, hash: string, opts?: DataOpts): Promise> { + const pathname = `${this.prefix}${filepath}`; + const filename = path.basename(pathname); + const url = new URL(pathname, window.location.origin); + url.searchParams.set('hash', hash); + url.searchParams.set('chunked', '1'); + console.log(`url,`, url, hash); + // 预留 eventSource 支持(暂不处理) + // const createEventSource = opts?.createEventSource; + + const chunkSize = 5 * 1024 * 1024; // 5MB + const totalChunks = Math.ceil(file.size / chunkSize); + + for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) { + const start = currentChunk * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + const formData = new FormData(); + formData.append('file', chunk, filename); + formData.append('chunkIndex', currentChunk.toString()); + formData.append('totalChunks', totalChunks.toString()); + console.log(`Uploading chunk ${currentChunk + 1}/${totalChunks}`, url.toString()); + try { + const res = await adapter({ + url: url.toString(), + isPostFile: true, + method: 'POST', + body: formData, + timeout: 5 * 60 * 1000, // 5分钟超时 + ...opts, + headers: { ...opts?.headers, ...this.header(opts?.headers, false) }, + params: { + hash: hash, + ...opts?.params, + }, + }); + console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res); + } catch (error) { + console.error(`Error uploading chunk ${currentChunk + 1}/${totalChunks}`, error); + throw error; + } + } + + return { code: 200, message: '上传成功' }; + } async createFolder(folderpath: string, opts?: DataOpts): Promise> { const filepath = folderpath.endsWith('/') ? `${folderpath}keep.txt` : `${folderpath}/keep.txt`; return this.uploadFile(filepath, '文件夹占位,其他文件不存在,文件夹不存在,如果有其他文件夹,删除当前文件夹占位文件即可', opts); diff --git a/query/query-resources/utils.ts b/query/query-resources/utils.ts index 7cd1ce3..92cc7ca 100644 --- a/query/query-resources/utils.ts +++ b/query/query-resources/utils.ts @@ -1,4 +1,5 @@ import MD5 from 'crypto-js/md5'; +import SparkMD5 from 'spark-md5'; export const hashContent = (str: string | Blob | Buffer): Promise | string => { if (typeof str === 'string') { @@ -12,57 +13,20 @@ export const hashContent = (str: string | Blob | Buffer): Promise | stri return ''; }; +// 直接计算整个 Blob 的 MD5 export const hashBlob = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = async () => { - try { - const content = reader.result; - if (typeof content === 'string') { - resolve(MD5(content).toString()); - } else if (content) { - const contentString = new TextDecoder().decode(content); - resolve(MD5(contentString).toString()); - } else { - reject(new Error('Empty content')); - } - } catch (error) { - console.error('hashBlob error', error); - reject(error); - } - }; - reader.onerror = (error) => reject(error); - reader.readAsArrayBuffer(blob); - }); -}; -export const hashFile = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = async (event) => { - try { - const content = event.target?.result; - if (content instanceof ArrayBuffer) { - const contentString = new TextDecoder().decode(content); - const hashHex = MD5(contentString).toString(); - resolve(hashHex); - } else if (typeof content === 'string') { - const hashHex = MD5(content).toString(); - resolve(hashHex); - } else { - throw new Error('Invalid content type'); - } - } catch (error) { - console.error('hashFile error', error); - reject(error); - } - }; - - reader.onerror = (error) => { + return new Promise(async (resolve, reject) => { + try { + const spark = new SparkMD5.ArrayBuffer(); + spark.append(await blob.arrayBuffer()); + resolve(spark.end()); + } catch (error) { + console.error('hashBlob error', error); reject(error); - }; - - // 读取文件为 ArrayBuffer - reader.readAsArrayBuffer(file); + } }); }; + +export const hashFile = (file: File): Promise => { + return hashBlob(file); +};