@kevisual/api 0.0.35 → 0.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/api",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "mod.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"keywords": [],
|
|
19
19
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
|
20
20
|
"license": "MIT",
|
|
21
|
-
"packageManager": "pnpm@10.28.
|
|
21
|
+
"packageManager": "pnpm@10.28.2",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@kevisual/cache": "^0.0.5",
|
|
25
25
|
"@kevisual/query": "^0.0.38",
|
|
26
|
-
"@kevisual/router": "^0.0.
|
|
26
|
+
"@kevisual/router": "^0.0.63",
|
|
27
27
|
"@kevisual/types": "^0.0.12",
|
|
28
28
|
"@kevisual/use-config": "^1.0.28",
|
|
29
|
-
"@types/bun": "^1.3.
|
|
29
|
+
"@types/bun": "^1.3.8",
|
|
30
30
|
"@types/crypto-js": "^4.2.2",
|
|
31
|
-
"@types/node": "^25.0
|
|
31
|
+
"@types/node": "^25.1.0",
|
|
32
32
|
"crypto-js": "^4.2.0",
|
|
33
33
|
"dotenv": "^17.2.3",
|
|
34
34
|
"fast-glob": "^3.3.3"
|
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@kevisual/js-filter": "^0.0.5",
|
|
38
38
|
"@kevisual/load": "^0.0.6",
|
|
39
|
+
"@types/spark-md5": "^3.0.5",
|
|
39
40
|
"es-toolkit": "^1.44.0",
|
|
40
41
|
"eventemitter3": "^5.0.4",
|
|
41
42
|
"fuse.js": "^7.1.0",
|
|
42
43
|
"nanoid": "^5.1.6",
|
|
43
|
-
"path-browserify-esm": "^1.0.6"
|
|
44
|
+
"path-browserify-esm": "^1.0.6",
|
|
45
|
+
"spark-md5": "^3.0.2"
|
|
44
46
|
},
|
|
45
47
|
"exports": {
|
|
46
48
|
".": "./mod.ts",
|
|
@@ -71,23 +71,86 @@ export class QueryResources {
|
|
|
71
71
|
// Blob 类型时 hashContent 返回 Promise
|
|
72
72
|
const hash = hashResult instanceof Promise ? await hashResult : hashResult;
|
|
73
73
|
url.searchParams.set('hash', hash);
|
|
74
|
+
|
|
75
|
+
// 判断是否需要分块上传(文件大于20MB)
|
|
76
|
+
const isBlob = content instanceof Blob;
|
|
77
|
+
const fileSize = isBlob ? content.size : new Blob([content]).size;
|
|
78
|
+
const CHUNK_THRESHOLD = 20 * 1024 * 1024; // 20MB
|
|
79
|
+
|
|
80
|
+
if (fileSize > CHUNK_THRESHOLD && isBlob) {
|
|
81
|
+
// 使用分块上传
|
|
82
|
+
return this.uploadChunkedFile(filepath, content, hash, opts);
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
const formData = new FormData();
|
|
75
|
-
if (
|
|
86
|
+
if (isBlob) {
|
|
76
87
|
formData.append('file', content);
|
|
77
88
|
} else {
|
|
78
89
|
formData.append('file', new Blob([content], { type }));
|
|
79
90
|
}
|
|
80
91
|
return adapter({
|
|
81
92
|
url: url.toString(),
|
|
82
|
-
headers: { ...this.header(opts?.headers, false) },
|
|
83
|
-
params: {
|
|
84
|
-
hash: hash,
|
|
85
|
-
},
|
|
86
93
|
isPostFile: true,
|
|
87
94
|
method: 'POST',
|
|
88
95
|
body: formData,
|
|
96
|
+
timeout: 5 * 60 * 1000, // 5分钟超时
|
|
97
|
+
...opts,
|
|
98
|
+
headers: { ...opts?.headers, ...this.header(opts?.headers, false) },
|
|
99
|
+
params: {
|
|
100
|
+
hash: hash,
|
|
101
|
+
...opts?.params,
|
|
102
|
+
},
|
|
89
103
|
});
|
|
90
104
|
}
|
|
105
|
+
async uploadChunkedFile(filepath: string, file: Blob, hash: string, opts?: DataOpts): Promise<Result<any>> {
|
|
106
|
+
const pathname = `${this.prefix}${filepath}`;
|
|
107
|
+
const filename = path.basename(pathname);
|
|
108
|
+
const url = new URL(pathname, window.location.origin);
|
|
109
|
+
url.searchParams.set('hash', hash);
|
|
110
|
+
url.searchParams.set('chunk', '1');
|
|
111
|
+
console.log(`url,`, url, hash);
|
|
112
|
+
// 预留 eventSource 支持(暂不处理)
|
|
113
|
+
// const createEventSource = opts?.createEventSource;
|
|
114
|
+
|
|
115
|
+
const chunkSize = 5 * 1024 * 1024; // 5MB
|
|
116
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
117
|
+
|
|
118
|
+
for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) {
|
|
119
|
+
const start = currentChunk * chunkSize;
|
|
120
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
121
|
+
const chunkBlob = file.slice(start, end);
|
|
122
|
+
// 转换为 File 类型
|
|
123
|
+
const chunkFile = new File([chunkBlob], filename, { type: file.type || 'application/octet-stream' });
|
|
124
|
+
|
|
125
|
+
const formData = new FormData();
|
|
126
|
+
formData.append('file', chunkFile, filename);
|
|
127
|
+
formData.append('chunkIndex', currentChunk.toString());
|
|
128
|
+
formData.append('totalChunks', totalChunks.toString());
|
|
129
|
+
console.log(`Uploading chunk ${currentChunk + 1}/${totalChunks}`, url.toString());
|
|
130
|
+
try {
|
|
131
|
+
const res = await adapter({
|
|
132
|
+
url: url.toString(),
|
|
133
|
+
isPostFile: true,
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: formData,
|
|
136
|
+
timeout: 5 * 60 * 1000, // 5分钟超时
|
|
137
|
+
...opts,
|
|
138
|
+
headers: { ...opts?.headers, ...this.header(opts?.headers, false) },
|
|
139
|
+
params: {
|
|
140
|
+
hash: hash,
|
|
141
|
+
chunk: '1',
|
|
142
|
+
...opts?.params,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`Error uploading chunk ${currentChunk + 1}/${totalChunks}`, error);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { code: 200, message: '上传成功' };
|
|
153
|
+
}
|
|
91
154
|
async createFolder(folderpath: string, opts?: DataOpts): Promise<Result<any>> {
|
|
92
155
|
const filepath = folderpath.endsWith('/') ? `${folderpath}keep.txt` : `${folderpath}/keep.txt`;
|
|
93
156
|
return this.uploadFile(filepath, '文件夹占位,其他文件不存在,文件夹不存在,如果有其他文件夹,删除当前文件夹占位文件即可', opts);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MD5 from 'crypto-js/md5';
|
|
2
|
+
import SparkMD5 from 'spark-md5';
|
|
2
3
|
|
|
3
4
|
export const hashContent = (str: string | Blob | Buffer): Promise<string> | string => {
|
|
4
5
|
if (typeof str === 'string') {
|
|
@@ -12,57 +13,20 @@ export const hashContent = (str: string | Blob | Buffer): Promise<string> | stri
|
|
|
12
13
|
return '';
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
// 直接计算整个 Blob 的 MD5
|
|
15
17
|
export const hashBlob = (blob: Blob): Promise<string> => {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
resolve(MD5(contentString).toString());
|
|
26
|
-
} else {
|
|
27
|
-
reject(new Error('Empty content'));
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('hashBlob error', error);
|
|
31
|
-
reject(error);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
reader.onerror = (error) => reject(error);
|
|
35
|
-
reader.readAsArrayBuffer(blob);
|
|
18
|
+
return new Promise(async (resolve, reject) => {
|
|
19
|
+
try {
|
|
20
|
+
const spark = new SparkMD5.ArrayBuffer();
|
|
21
|
+
spark.append(await blob.arrayBuffer());
|
|
22
|
+
resolve(spark.end());
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('hashBlob error', error);
|
|
25
|
+
reject(error);
|
|
26
|
+
}
|
|
36
27
|
});
|
|
37
28
|
};
|
|
38
|
-
export const hashFile = (file: File): Promise<string> => {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const reader = new FileReader();
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const content = event.target?.result;
|
|
45
|
-
if (content instanceof ArrayBuffer) {
|
|
46
|
-
const contentString = new TextDecoder().decode(content);
|
|
47
|
-
const hashHex = MD5(contentString).toString();
|
|
48
|
-
resolve(hashHex);
|
|
49
|
-
} else if (typeof content === 'string') {
|
|
50
|
-
const hashHex = MD5(content).toString();
|
|
51
|
-
resolve(hashHex);
|
|
52
|
-
} else {
|
|
53
|
-
throw new Error('Invalid content type');
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error('hashFile error', error);
|
|
57
|
-
reject(error);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
reader.onerror = (error) => {
|
|
62
|
-
reject(error);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// 读取文件为 ArrayBuffer
|
|
66
|
-
reader.readAsArrayBuffer(file);
|
|
67
|
-
});
|
|
30
|
+
export const hashFile = (file: File): Promise<string> => {
|
|
31
|
+
return hashBlob(file);
|
|
68
32
|
};
|