@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.
@@ -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,3 @@
1
+ export * from './to-file.ts';
2
+ export * from './filter-files.ts';
3
+ export * from './random-id.ts';
@@ -0,0 +1,3 @@
1
+ export const randomId = () => {
2
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
3
+ };
@@ -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
+ };