@kevisual/api 0.0.34 → 0.0.37

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.34",
3
+ "version": "0.0.37",
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.1",
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.62",
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.6",
29
+ "@types/bun": "^1.3.8",
30
30
  "@types/crypto-js": "^4.2.2",
31
- "@types/node": "^25.0.10",
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",
@@ -60,7 +60,7 @@ export class QueryResources {
60
60
  }
61
61
  async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
62
62
  const url = `${this.prefix}${filepath}`;
63
- return this.get({}, { url, method: 'GET', headers: this.header(opts?.headers, false), isText: true });
63
+ return this.get({}, { url, method: 'GET', ...opts, headers: this.header(opts?.headers, false), isText: true });
64
64
  }
65
65
  async uploadFile(filepath: string, content: string | Blob, opts?: DataOpts): Promise<Result<any>> {
66
66
  const pathname = `${this.prefix}${filepath}`;
@@ -71,23 +71,83 @@ 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 (content instanceof Blob) {
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('chunked', '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 chunk = file.slice(start, end);
122
+
123
+ const formData = new FormData();
124
+ formData.append('file', chunk, filename);
125
+ formData.append('chunkIndex', currentChunk.toString());
126
+ formData.append('totalChunks', totalChunks.toString());
127
+ console.log(`Uploading chunk ${currentChunk + 1}/${totalChunks}`, url.toString());
128
+ try {
129
+ const res = await adapter({
130
+ url: url.toString(),
131
+ isPostFile: true,
132
+ method: 'POST',
133
+ body: formData,
134
+ timeout: 5 * 60 * 1000, // 5分钟超时
135
+ ...opts,
136
+ headers: { ...opts?.headers, ...this.header(opts?.headers, false) },
137
+ params: {
138
+ hash: hash,
139
+ ...opts?.params,
140
+ },
141
+ });
142
+ console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
143
+ } catch (error) {
144
+ console.error(`Error uploading chunk ${currentChunk + 1}/${totalChunks}`, error);
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ return { code: 200, message: '上传成功' };
150
+ }
91
151
  async createFolder(folderpath: string, opts?: DataOpts): Promise<Result<any>> {
92
152
  const filepath = folderpath.endsWith('/') ? `${folderpath}keep.txt` : `${folderpath}/keep.txt`;
93
153
  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
- const reader = new FileReader();
18
- reader.onload = async () => {
19
- try {
20
- const content = reader.result;
21
- if (typeof content === 'string') {
22
- resolve(MD5(content).toString());
23
- } else if (content) {
24
- const contentString = new TextDecoder().decode(content);
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
- reader.onload = async (event) => {
43
- try {
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
  };