@kevisual/api 0.0.1
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 +30 -0
- package/query/index.ts +7 -0
- package/query/kevisual.json +25 -0
- package/query/query-ai/defines/ai.ts +42 -0
- package/query/query-ai/query-ai.ts +101 -0
- package/query/query-app/defines/index.ts +3 -0
- package/query/query-app/defines/user-app-list.ts +62 -0
- package/query/query-app/defines/user-app.ts +33 -0
- package/query/query-app/query-app-define.ts +1 -0
- package/query/query-app/query-app.ts +18 -0
- package/query/query-login/login-cache.ts +204 -0
- package/query/query-login/login-node-cache.ts +132 -0
- package/query/query-login/query-login-browser.ts +12 -0
- package/query/query-login/query-login-node.ts +14 -0
- package/query/query-login/query-login.ts +434 -0
- package/query/query-mark/query-mark.ts +154 -0
- package/query/query-resources/index.ts +71 -0
- package/query/query-shop/defines/query-shop-define.ts +27 -0
- package/query/query-shop/query-shop.ts +17 -0
- package/query/query-upload/core/upload-chunk.ts +134 -0
- package/query/query-upload/core/upload-progress.ts +103 -0
- package/query/query-upload/core/upload.ts +113 -0
- package/query/query-upload/query-upload-browser.ts +51 -0
- package/query/query-upload/query-upload-node.ts +1 -0
- package/query/query-upload/query-upload.ts +11 -0
- package/query/query-upload/utils/filter-files.ts +23 -0
- package/query/query-upload/utils/index.ts +3 -0
- package/query/query-upload/utils/random-id.ts +3 -0
- package/query/query-upload/utils/to-file.ts +105 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomId } from '../utils/random-id.ts';
|
|
2
|
+
import { UploadProgress } from './upload-progress.ts';
|
|
3
|
+
export type ConvertOpts = {
|
|
4
|
+
appKey?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
directory?: string;
|
|
8
|
+
isPublic?: boolean;
|
|
9
|
+
filename?: string;
|
|
10
|
+
/**
|
|
11
|
+
* 是否不检查应用文件, 默认 true,默认不检测
|
|
12
|
+
*/
|
|
13
|
+
noCheckAppFiles?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// createEventSource: (baseUrl: string, searchParams: URLSearchParams) => {
|
|
17
|
+
// return new EventSource(baseUrl + '/api/s1/events?' + searchParams.toString());
|
|
18
|
+
// },
|
|
19
|
+
export type UploadOpts = {
|
|
20
|
+
uploadProgress: UploadProgress;
|
|
21
|
+
/**
|
|
22
|
+
* 创建 EventSource 兼容 nodejs
|
|
23
|
+
* @param baseUrl 基础 URL
|
|
24
|
+
* @param searchParams 查询参数
|
|
25
|
+
* @returns EventSource
|
|
26
|
+
*/
|
|
27
|
+
createEventSource: (baseUrl: string, searchParams: URLSearchParams) => EventSource;
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
token: string;
|
|
30
|
+
FormDataFn: any;
|
|
31
|
+
};
|
|
32
|
+
export const uploadFileChunked = async (file: File, opts: ConvertOpts, opts2: UploadOpts) => {
|
|
33
|
+
const { directory, appKey, version, username, isPublic, noCheckAppFiles = true } = opts;
|
|
34
|
+
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
|
35
|
+
return new Promise(async (resolve, reject) => {
|
|
36
|
+
const taskId = randomId();
|
|
37
|
+
const filename = opts.filename || file.name;
|
|
38
|
+
uploadProgress?.start(`${filename} 上传中...`);
|
|
39
|
+
|
|
40
|
+
const searchParams = new URLSearchParams();
|
|
41
|
+
searchParams.set('taskId', taskId);
|
|
42
|
+
if (isPublic) {
|
|
43
|
+
searchParams.set('public', 'true');
|
|
44
|
+
}
|
|
45
|
+
if (noCheckAppFiles) {
|
|
46
|
+
searchParams.set('noCheckAppFiles', '1');
|
|
47
|
+
}
|
|
48
|
+
const eventSource = createEventSource(baseUrl + '/api/s1/events', searchParams);
|
|
49
|
+
let isError = false;
|
|
50
|
+
// 监听服务器推送的进度更新
|
|
51
|
+
eventSource.onmessage = function (event) {
|
|
52
|
+
console.log('Progress update:', event.data);
|
|
53
|
+
const parseIfJson = (data: string) => {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(data);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const receivedData = parseIfJson(event.data);
|
|
61
|
+
if (typeof receivedData === 'string') return;
|
|
62
|
+
const progress = Number(receivedData.progress);
|
|
63
|
+
const progressFixed = progress.toFixed(2);
|
|
64
|
+
uploadProgress?.set(progress, { ...receivedData, progressFixed, filename, taskId });
|
|
65
|
+
};
|
|
66
|
+
eventSource.onerror = function (event) {
|
|
67
|
+
console.log('eventSource.onerror', event);
|
|
68
|
+
isError = true;
|
|
69
|
+
reject(event);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const chunkSize = 1 * 1024 * 1024; // 1MB
|
|
73
|
+
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
74
|
+
|
|
75
|
+
for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) {
|
|
76
|
+
const start = currentChunk * chunkSize;
|
|
77
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
78
|
+
const chunk = file.slice(start, end);
|
|
79
|
+
|
|
80
|
+
const formData = new FormDataFn();
|
|
81
|
+
formData.append('file', chunk, filename);
|
|
82
|
+
formData.append('chunkIndex', currentChunk.toString());
|
|
83
|
+
formData.append('totalChunks', totalChunks.toString());
|
|
84
|
+
const isLast = currentChunk === totalChunks - 1;
|
|
85
|
+
if (directory) {
|
|
86
|
+
formData.append('directory', directory);
|
|
87
|
+
}
|
|
88
|
+
if (appKey && version) {
|
|
89
|
+
formData.append('appKey', appKey);
|
|
90
|
+
formData.append('version', version);
|
|
91
|
+
}
|
|
92
|
+
if (username) {
|
|
93
|
+
formData.append('username', username);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(baseUrl + '/api/s1/resources/upload/chunk?taskId=' + taskId, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: formData,
|
|
99
|
+
headers: {
|
|
100
|
+
'task-id': taskId,
|
|
101
|
+
Authorization: `Bearer ${token}`,
|
|
102
|
+
},
|
|
103
|
+
}).then((response) => response.json());
|
|
104
|
+
|
|
105
|
+
if (res?.code !== 200) {
|
|
106
|
+
console.log('uploadChunk error', res);
|
|
107
|
+
uploadProgress?.error(res?.message || '上传失败');
|
|
108
|
+
isError = true;
|
|
109
|
+
eventSource.close();
|
|
110
|
+
|
|
111
|
+
uploadProgress?.done();
|
|
112
|
+
reject(new Error(res?.message || '上传失败'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (isLast) {
|
|
116
|
+
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
|
117
|
+
eventSource.close();
|
|
118
|
+
uploadProgress?.done();
|
|
119
|
+
resolve(res);
|
|
120
|
+
}
|
|
121
|
+
// console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.log('Error uploading chunk', error);
|
|
124
|
+
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
|
125
|
+
reject(error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 循环结束
|
|
130
|
+
if (!uploadProgress?.end) {
|
|
131
|
+
uploadProgress?.done();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
interface UploadNProgress {
|
|
2
|
+
start: (msg?: string) => void;
|
|
3
|
+
done: () => void;
|
|
4
|
+
set: (progress: number) => void;
|
|
5
|
+
}
|
|
6
|
+
export type UploadProgressData = {
|
|
7
|
+
progress: number;
|
|
8
|
+
progressFixed: number;
|
|
9
|
+
filename?: string;
|
|
10
|
+
taskId?: string;
|
|
11
|
+
};
|
|
12
|
+
type UploadProgressOpts = {
|
|
13
|
+
onStart?: () => void;
|
|
14
|
+
onDone?: () => void;
|
|
15
|
+
onProgress?: (progress: number, data?: UploadProgressData) => void;
|
|
16
|
+
};
|
|
17
|
+
export class UploadProgress implements UploadNProgress {
|
|
18
|
+
/**
|
|
19
|
+
* 进度
|
|
20
|
+
*/
|
|
21
|
+
progress: number;
|
|
22
|
+
/**
|
|
23
|
+
* 开始回调
|
|
24
|
+
*/
|
|
25
|
+
onStart: (() => void) | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* 结束回调
|
|
28
|
+
*/
|
|
29
|
+
onDone: (() => void) | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* 消息回调
|
|
32
|
+
*/
|
|
33
|
+
onProgress: ((progress: number, data?: UploadProgressData) => void) | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* 数据
|
|
36
|
+
*/
|
|
37
|
+
data: any;
|
|
38
|
+
/**
|
|
39
|
+
* 是否结束
|
|
40
|
+
*/
|
|
41
|
+
end: boolean;
|
|
42
|
+
constructor(uploadOpts: UploadProgressOpts) {
|
|
43
|
+
this.progress = 0;
|
|
44
|
+
this.end = false;
|
|
45
|
+
const mockFn = () => {};
|
|
46
|
+
this.onStart = uploadOpts.onStart || mockFn;
|
|
47
|
+
this.onDone = uploadOpts.onDone || mockFn;
|
|
48
|
+
this.onProgress = uploadOpts.onProgress || mockFn;
|
|
49
|
+
}
|
|
50
|
+
start(msg?: string) {
|
|
51
|
+
this.progress = 0;
|
|
52
|
+
msg && this.info(msg);
|
|
53
|
+
this.end = false;
|
|
54
|
+
this.onStart?.();
|
|
55
|
+
}
|
|
56
|
+
done() {
|
|
57
|
+
this.progress = 100;
|
|
58
|
+
this.end = true;
|
|
59
|
+
this.onDone?.();
|
|
60
|
+
}
|
|
61
|
+
set(progress: number, data?: UploadProgressData) {
|
|
62
|
+
this.progress = progress;
|
|
63
|
+
this.data = data;
|
|
64
|
+
this.onProgress?.(progress, data);
|
|
65
|
+
console.log('uploadProgress set', progress, data);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 开始回调
|
|
69
|
+
*/
|
|
70
|
+
setOnStart(callback: () => void) {
|
|
71
|
+
this.onStart = callback;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 结束回调
|
|
75
|
+
*/
|
|
76
|
+
setOnDone(callback: () => void) {
|
|
77
|
+
this.onDone = callback;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 消息回调
|
|
81
|
+
*/
|
|
82
|
+
setOnProgress(callback: (progress: number, data?: UploadProgressData) => void) {
|
|
83
|
+
this.onProgress = callback;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 打印信息
|
|
87
|
+
*/
|
|
88
|
+
info(msg: string) {
|
|
89
|
+
console.log(msg);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 打印错误
|
|
93
|
+
*/
|
|
94
|
+
error(msg: string) {
|
|
95
|
+
console.error(msg);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 打印警告
|
|
99
|
+
*/
|
|
100
|
+
warn(msg: string) {
|
|
101
|
+
console.warn(msg);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomId } from '../utils/random-id.ts';
|
|
2
|
+
import type { UploadOpts } from './upload-chunk.ts';
|
|
3
|
+
type ConvertOpts = {
|
|
4
|
+
appKey?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
directory?: string;
|
|
8
|
+
/**
|
|
9
|
+
* 文件大小限制
|
|
10
|
+
*/
|
|
11
|
+
maxSize?: number;
|
|
12
|
+
/**
|
|
13
|
+
* 文件数量限制
|
|
14
|
+
*/
|
|
15
|
+
maxCount?: number;
|
|
16
|
+
/**
|
|
17
|
+
* 是否不检查应用文件, 默认 true,默认不检测
|
|
18
|
+
*/
|
|
19
|
+
noCheckAppFiles?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const uploadFiles = async (files: File[], opts: ConvertOpts, opts2: UploadOpts) => {
|
|
23
|
+
const { directory, appKey, version, username, noCheckAppFiles = true } = opts;
|
|
24
|
+
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
|
25
|
+
const length = files.length;
|
|
26
|
+
const maxSize = opts.maxSize || 20 * 1024 * 1024; // 20MB
|
|
27
|
+
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
|
28
|
+
if (totalSize > maxSize) {
|
|
29
|
+
const maxSizeMB = maxSize / 1024 / 1024;
|
|
30
|
+
uploadProgress?.error('有文件大小不能超过' + maxSizeMB + 'MB');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const maxCount = opts.maxCount || 10;
|
|
34
|
+
if (length > maxCount) {
|
|
35
|
+
uploadProgress?.error(`最多只能上传${maxCount}个文件`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
uploadProgress?.info(`上传中,共${length}个文件`);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const formData = new FormDataFn();
|
|
41
|
+
const webkitRelativePath = files[0]?.webkitRelativePath;
|
|
42
|
+
const keepDirectory = webkitRelativePath !== '';
|
|
43
|
+
const root = keepDirectory ? webkitRelativePath.split('/')[0] : '';
|
|
44
|
+
for (let i = 0; i < files.length; i++) {
|
|
45
|
+
const file = files[i];
|
|
46
|
+
if (keepDirectory) {
|
|
47
|
+
// relativePath 去除第一级
|
|
48
|
+
const webkitRelativePath = file.webkitRelativePath.replace(root + '/', '');
|
|
49
|
+
formData.append('file', file, webkitRelativePath); // 保留文件夹路径
|
|
50
|
+
} else {
|
|
51
|
+
formData.append('file', files[i], files[i].name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (directory) {
|
|
55
|
+
formData.append('directory', directory);
|
|
56
|
+
}
|
|
57
|
+
if (appKey && version) {
|
|
58
|
+
formData.append('appKey', appKey);
|
|
59
|
+
formData.append('version', version);
|
|
60
|
+
}
|
|
61
|
+
if (username) {
|
|
62
|
+
formData.append('username', username);
|
|
63
|
+
}
|
|
64
|
+
const searchParams = new URLSearchParams();
|
|
65
|
+
const taskId = randomId();
|
|
66
|
+
searchParams.set('taskId', taskId);
|
|
67
|
+
|
|
68
|
+
if (noCheckAppFiles) {
|
|
69
|
+
searchParams.set('noCheckAppFiles', '1');
|
|
70
|
+
}
|
|
71
|
+
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
|
|
72
|
+
|
|
73
|
+
uploadProgress?.start('上传中...');
|
|
74
|
+
eventSource.onopen = async function (event) {
|
|
75
|
+
const res = await fetch('/api/s1/resources/upload?' + searchParams.toString(), {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
body: formData,
|
|
78
|
+
headers: {
|
|
79
|
+
'task-id': taskId,
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
},
|
|
82
|
+
}).then((response) => response.json());
|
|
83
|
+
|
|
84
|
+
console.log('upload success', res);
|
|
85
|
+
fetch('/api/s1/events/close?taskId=' + taskId);
|
|
86
|
+
eventSource.close();
|
|
87
|
+
uploadProgress?.done();
|
|
88
|
+
resolve(res);
|
|
89
|
+
};
|
|
90
|
+
// 监听服务器推送的进度更新
|
|
91
|
+
eventSource.onmessage = function (event) {
|
|
92
|
+
console.log('Progress update:', event.data);
|
|
93
|
+
const parseIfJson = (data: string) => {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(data);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const receivedData = parseIfJson(event.data);
|
|
101
|
+
if (typeof receivedData === 'string') return;
|
|
102
|
+
const progress = Number(receivedData.progress);
|
|
103
|
+
const progressFixed = progress.toFixed(2);
|
|
104
|
+
console.log('progress', progress);
|
|
105
|
+
uploadProgress?.set(progress, { ...receivedData, taskId, progressFixed });
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
eventSource.onerror = function (event) {
|
|
109
|
+
console.log('eventSource.onerror', event);
|
|
110
|
+
reject(event);
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UploadProgress, UploadProgressData } from './core/upload-progress.ts';
|
|
2
|
+
import { uploadFileChunked } from './core/upload-chunk.ts';
|
|
3
|
+
import { toFile, uploadFiles, randomId } from './query-upload.ts';
|
|
4
|
+
|
|
5
|
+
export { toFile, randomId };
|
|
6
|
+
export { uploadFiles, uploadFileChunked, UploadProgress };
|
|
7
|
+
|
|
8
|
+
type UploadFileProps = {
|
|
9
|
+
onStart?: () => void;
|
|
10
|
+
onDone?: () => void;
|
|
11
|
+
onProgress?: (progress: number, data: UploadProgressData) => void;
|
|
12
|
+
onSuccess?: (res: any) => void;
|
|
13
|
+
onError?: (err: any) => void;
|
|
14
|
+
token?: string;
|
|
15
|
+
};
|
|
16
|
+
export type ConvertOpts = {
|
|
17
|
+
appKey?: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
username?: string;
|
|
20
|
+
directory?: string;
|
|
21
|
+
isPublic?: boolean;
|
|
22
|
+
filename?: string;
|
|
23
|
+
/**
|
|
24
|
+
* 是否不检查应用文件, 默认 true,默认不检测
|
|
25
|
+
*/
|
|
26
|
+
noCheckAppFiles?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const uploadChunk = async (file: File, opts: ConvertOpts, props?: UploadFileProps) => {
|
|
30
|
+
const uploadProgress = new UploadProgress({
|
|
31
|
+
onStart: function () {
|
|
32
|
+
props?.onStart?.();
|
|
33
|
+
},
|
|
34
|
+
onDone: () => {
|
|
35
|
+
props?.onDone?.();
|
|
36
|
+
},
|
|
37
|
+
onProgress: (progress, data) => {
|
|
38
|
+
props?.onProgress?.(progress, data!);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const result = await uploadFileChunked(file, opts, {
|
|
42
|
+
uploadProgress,
|
|
43
|
+
token: props?.token!,
|
|
44
|
+
createEventSource: (url: string, searchParams: URLSearchParams) => {
|
|
45
|
+
return new EventSource(url + '?' + searchParams.toString());
|
|
46
|
+
},
|
|
47
|
+
FormDataFn: FormData,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// console.log('upload)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { uploadFiles } from './core/upload.ts';
|
|
2
|
+
|
|
3
|
+
import { uploadFileChunked } from './core/upload-chunk.ts';
|
|
4
|
+
import { UploadProgress } from './core/upload-progress.ts';
|
|
5
|
+
|
|
6
|
+
export { uploadFiles, uploadFileChunked, UploadProgress };
|
|
7
|
+
|
|
8
|
+
export * from './utils/to-file.ts';
|
|
9
|
+
export { randomId } from './utils/random-id.ts';
|
|
10
|
+
|
|
11
|
+
export { filterFiles } from './utils/filter-files.ts';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 过滤文件, 过滤 .DS_Store, node_modules, 以.开头的文件, 过滤 __开头的文件
|
|
3
|
+
* @param files
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export const filterFiles = (files: File[]) => {
|
|
7
|
+
files = files.filter((file) => {
|
|
8
|
+
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
// 过滤node_modules
|
|
12
|
+
if (file.webkitRelativePath.includes('node_modules')) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// 过滤文件 .DS_Store
|
|
16
|
+
if (file.name === '.DS_Store') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// 过滤以.开头的文件
|
|
20
|
+
return !file.name.startsWith('.');
|
|
21
|
+
});
|
|
22
|
+
return files;
|
|
23
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const getFileExtension = (filename: string) => {
|
|
2
|
+
return filename.split('.').pop();
|
|
3
|
+
};
|
|
4
|
+
const getFileType = (extension: string) => {
|
|
5
|
+
switch (extension) {
|
|
6
|
+
case 'js':
|
|
7
|
+
return 'text/javascript';
|
|
8
|
+
case 'css':
|
|
9
|
+
return 'text/css';
|
|
10
|
+
case 'html':
|
|
11
|
+
return 'text/html';
|
|
12
|
+
case 'json':
|
|
13
|
+
return 'application/json';
|
|
14
|
+
case 'png':
|
|
15
|
+
return 'image/png';
|
|
16
|
+
case 'jpg':
|
|
17
|
+
return 'image/jpeg';
|
|
18
|
+
case 'jpeg':
|
|
19
|
+
return 'image/jpeg';
|
|
20
|
+
case 'gif':
|
|
21
|
+
return 'image/gif';
|
|
22
|
+
case 'svg':
|
|
23
|
+
return 'image/svg+xml';
|
|
24
|
+
case 'webp':
|
|
25
|
+
return 'image/webp';
|
|
26
|
+
case 'ico':
|
|
27
|
+
return 'image/x-icon';
|
|
28
|
+
default:
|
|
29
|
+
return 'text/plain';
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const checkIsBase64 = (content: string) => {
|
|
33
|
+
return content.startsWith('data:');
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 获取文件的目录和文件名
|
|
37
|
+
* @param filename 文件名
|
|
38
|
+
* @returns 目录和文件名
|
|
39
|
+
*/
|
|
40
|
+
export const getDirectoryAndName = (filename: string) => {
|
|
41
|
+
if (!filename) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (filename.startsWith('.')) {
|
|
45
|
+
return null;
|
|
46
|
+
} else {
|
|
47
|
+
filename = filename.replace(/^\/+/, ''); // Remove all leading slashes
|
|
48
|
+
}
|
|
49
|
+
const hasDirectory = filename.includes('/');
|
|
50
|
+
if (!hasDirectory) {
|
|
51
|
+
return { directory: '', name: filename };
|
|
52
|
+
}
|
|
53
|
+
const parts = filename.split('/');
|
|
54
|
+
const name = parts.pop()!; // Get the last part as the file name
|
|
55
|
+
const directory = parts.join('/'); // Join the remaining parts as the directory
|
|
56
|
+
return { directory, name };
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* 把字符串转为文件流,并返回文件流,根据filename的扩展名,自动设置文件类型.
|
|
60
|
+
* 当不是文本类型,自动需要把base64的字符串转为blob
|
|
61
|
+
* @param content 字符串
|
|
62
|
+
* @param filename 文件名
|
|
63
|
+
* @returns 文件流
|
|
64
|
+
*/
|
|
65
|
+
export const toFile = (content: string, filename: string) => {
|
|
66
|
+
// 如果文件名是 a/d/a.js 格式的,则需要把d作为目录,a.js作为文件名
|
|
67
|
+
const directoryAndName = getDirectoryAndName(filename);
|
|
68
|
+
if (!directoryAndName) {
|
|
69
|
+
throw new Error('Invalid filename');
|
|
70
|
+
}
|
|
71
|
+
const { name } = directoryAndName;
|
|
72
|
+
const extension = getFileExtension(name);
|
|
73
|
+
if (!extension) {
|
|
74
|
+
throw new Error('Invalid filename');
|
|
75
|
+
}
|
|
76
|
+
const isBase64 = checkIsBase64(content);
|
|
77
|
+
const type = getFileType(extension);
|
|
78
|
+
|
|
79
|
+
if (isBase64) {
|
|
80
|
+
// Decode base64 string
|
|
81
|
+
const base64Data = content.split(',')[1]; // Remove the data URL prefix
|
|
82
|
+
const byteCharacters = atob(base64Data);
|
|
83
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
84
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
85
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
86
|
+
}
|
|
87
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
88
|
+
const blob = new Blob([byteArray], { type });
|
|
89
|
+
return new File([blob], filename, { type });
|
|
90
|
+
} else {
|
|
91
|
+
const blob = new Blob([content], { type });
|
|
92
|
+
return new File([blob], filename, { type });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 把字符串转为文本文件
|
|
98
|
+
* @param content 字符串
|
|
99
|
+
* @param filename 文件名
|
|
100
|
+
* @returns 文件流
|
|
101
|
+
*/
|
|
102
|
+
export const toTextFile = (content: string = 'keep directory exist', filename: string = 'keep.txt') => {
|
|
103
|
+
const file = toFile(content, filename);
|
|
104
|
+
return file;
|
|
105
|
+
};
|