@windwalker-io/unicorn-next 0.1.0 → 0.1.2
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/.editorconfig +18 -18
- package/.gulp.json +7 -7
- package/bin/release.mjs +47 -47
- package/dist/chunks/button-radio.js +1 -1
- package/dist/chunks/button-radio.js.map +1 -1
- package/dist/chunks/checkboxes-multi-select.js.map +1 -1
- package/dist/chunks/field-cascade-select.js +1 -1
- package/dist/chunks/field-cascade-select.js.map +1 -1
- package/dist/chunks/field-file-drag.js +1 -1
- package/dist/chunks/field-file-drag.js.map +1 -1
- package/dist/chunks/field-flatpickr.js +2 -2
- package/dist/chunks/field-flatpickr.js.map +1 -1
- package/dist/chunks/field-modal-select.js +1 -1
- package/dist/chunks/field-modal-select.js.map +1 -1
- package/dist/chunks/field-modal-tree.js +3 -3
- package/dist/chunks/field-modal-tree.js.map +1 -1
- package/dist/chunks/field-multi-uploader.js +1 -1
- package/dist/chunks/field-multi-uploader.js.map +1 -1
- package/dist/chunks/field-repeatable.js +1 -1
- package/dist/chunks/field-repeatable.js.map +1 -1
- package/dist/chunks/field-single-image-drag.js +1 -1
- package/dist/chunks/field-single-image-drag.js.map +1 -1
- package/dist/chunks/form.js +1 -1
- package/dist/chunks/form.js.map +1 -1
- package/dist/chunks/grid.js +1 -1
- package/dist/chunks/grid.js.map +1 -1
- package/dist/chunks/http-client.js +1 -1
- package/dist/chunks/http-client.js.map +1 -1
- package/dist/chunks/iframe-modal.js +1 -1
- package/dist/chunks/iframe-modal.js.map +1 -1
- package/dist/chunks/keep-tab.js +1 -1
- package/dist/chunks/keep-tab.js.map +1 -1
- package/dist/chunks/legacy.js +7 -5
- package/dist/chunks/legacy.js.map +1 -1
- package/dist/chunks/list-dependent.js +1 -1
- package/dist/chunks/list-dependent.js.map +1 -1
- package/dist/chunks/s3-multipart-uploader.js +1 -1
- package/dist/chunks/s3-multipart-uploader.js.map +1 -1
- package/dist/chunks/s3-uploader.js +1 -1
- package/dist/chunks/s3-uploader.js.map +1 -1
- package/dist/chunks/show-on.js +3 -2
- package/dist/chunks/show-on.js.map +1 -1
- package/dist/chunks/tinymce.js +25 -15
- package/dist/chunks/tinymce.js.map +1 -1
- package/dist/chunks/ui-bootstrap5.js +9 -2
- package/dist/chunks/ui-bootstrap5.js.map +1 -1
- package/dist/chunks/unicorn.js +154 -101
- package/dist/chunks/unicorn.js.map +1 -1
- package/dist/chunks/validation.js +10 -8
- package/dist/chunks/validation.js.map +1 -1
- package/dist/editor.css +1 -1
- package/dist/index.d.ts +72 -42
- package/dist/multi-level-menu.css +1 -1
- package/dist/switcher.css +1 -1
- package/dist/unicorn.js +72 -72
- package/fusionfile.mjs +155 -155
- package/package.json +103 -102
- package/scss/bootstrap/multi-level-menu.scss +121 -121
- package/scss/editor.scss +116 -116
- package/scss/field/file-drag.scss +102 -102
- package/scss/field/single-image-drag.scss +88 -88
- package/scss/field/vue-drag-uploader.scss +160 -160
- package/scss/switcher.scss +156 -156
- package/src/app.ts +128 -128
- package/src/bootstrap/button-radio.ts +208 -208
- package/src/bootstrap/keep-tab.ts +155 -155
- package/src/composable/index.ts +21 -21
- package/src/composable/useCheckboxesMultiSelect.ts +22 -22
- package/src/composable/useFieldCascadeSelect.ts +9 -9
- package/src/composable/useFieldFileDrag.ts +9 -9
- package/src/composable/useFieldFlatpickr.ts +3 -3
- package/src/composable/useFieldModalSelect.ts +6 -6
- package/src/composable/useFieldModalTree.ts +3 -3
- package/src/composable/useFieldMultiUploader.ts +3 -3
- package/src/composable/useFieldRepeatable.ts +9 -9
- package/src/composable/useFieldSingleImageDrag.ts +5 -5
- package/src/composable/useForm.ts +43 -43
- package/src/composable/useGrid.ts +57 -57
- package/src/composable/useHttp.ts +9 -8
- package/src/composable/useIframeModal.ts +10 -9
- package/src/composable/useListDependent.ts +26 -26
- package/src/composable/useQueue.ts +13 -13
- package/src/composable/useS3Uploader.ts +32 -32
- package/src/composable/useShowOn.ts +9 -9
- package/src/composable/useStack.ts +13 -13
- package/src/composable/useTinymce.ts +29 -29
- package/src/composable/useTomSelect.ts +72 -72
- package/src/composable/useUIBootstrap5.ts +48 -48
- package/src/composable/useUniDirective.ts +32 -32
- package/src/composable/useValidation.ts +39 -39
- package/src/data.ts +34 -36
- package/src/events.ts +82 -73
- package/src/legacy/legacy.ts +190 -186
- package/src/legacy/loader.ts +125 -125
- package/src/module/checkboxes-multi-select.ts +54 -54
- package/src/module/field-cascade-select.ts +292 -292
- package/src/module/field-file-drag.ts +292 -292
- package/src/module/field-flatpickr.ts +127 -127
- package/src/module/field-modal-select.ts +174 -174
- package/src/module/field-modal-tree.ts +27 -27
- package/src/module/field-multi-uploader.ts +361 -361
- package/src/module/field-repeatable.ts +202 -202
- package/src/module/field-single-image-drag.ts +468 -468
- package/src/module/form.ts +223 -223
- package/src/module/grid.ts +465 -465
- package/src/module/http-client.ts +248 -243
- package/src/module/iframe-modal.ts +167 -167
- package/src/module/list-dependent.ts +321 -321
- package/src/module/s3-multipart-uploader.ts +300 -300
- package/src/module/s3-uploader.ts +234 -234
- package/src/module/show-on.ts +175 -173
- package/src/module/tinymce.ts +276 -263
- package/src/module/ui-bootstrap5.ts +116 -107
- package/src/module/validation.ts +1026 -1019
- package/src/plugin/index.ts +1 -1
- package/src/plugin/php-adapter.ts +72 -65
- package/src/polyfill/form-request-submit.ts +31 -31
- package/src/polyfill/index.ts +9 -9
- package/src/service/animate.ts +58 -58
- package/src/service/crypto.ts +27 -27
- package/src/service/dom-watcher.ts +62 -62
- package/src/service/dom.ts +265 -265
- package/src/service/helper.ts +48 -48
- package/src/service/index.ts +10 -10
- package/src/service/lang.ts +122 -122
- package/src/service/loader.ts +152 -152
- package/src/service/router.ts +118 -118
- package/src/service/ui.ts +525 -497
- package/src/service/uri.ts +106 -106
- package/src/types/base.ts +9 -9
- package/src/types/index.ts +4 -4
- package/src/types/modal-tree.ts +12 -12
- package/src/types/plugin.ts +6 -6
- package/src/types/shims.d.ts +18 -18
- package/src/types/ui.ts +6 -6
- package/src/unicorn.ts +79 -63
- package/src/utilities/arr.ts +25 -25
- package/src/utilities/base.ts +9 -9
- package/src/utilities/data.ts +48 -48
- package/src/utilities/index.ts +5 -5
- package/src/utilities/tree.ts +20 -20
- package/src/vue/components/ModalTree/ModalTreeApp.vue +175 -175
- package/src/vue/components/ModalTree/TreeItem.vue +262 -262
- package/src/vue/components/ModalTree/TreeModal.vue +225 -225
- package/tests/test.js +4 -4
- package/tsconfig.js.json +25 -25
- package/tsconfig.json +17 -17
- package/vite.assets.config.ts +61 -61
- package/vite.config.test.ts +36 -36
- package/vite.config.ts +112 -112
- package/dist/unicorn-next.css +0 -12
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
import { AxiosProgressEvent, AxiosResponseHeaders } from 'axios';
|
|
2
|
-
import { Mixin } from 'ts-mixer';
|
|
3
|
-
import { createQueue, useHttpClient } from '../composable';
|
|
4
|
-
import { EventHandler, EventMixin } from '../events';
|
|
5
|
-
import type { MaybePromise } from '../types';
|
|
6
|
-
import { mergeDeep } from '../utilities';
|
|
7
|
-
import { ApiReturn } from './http-client';
|
|
8
|
-
|
|
9
|
-
declare type RoutingOptions = {
|
|
10
|
-
init: string;
|
|
11
|
-
sign: string;
|
|
12
|
-
complete: string;
|
|
13
|
-
abort: string;
|
|
14
|
-
} | ((action: RouteActions) => MaybePromise<string>);
|
|
15
|
-
|
|
16
|
-
declare type RouteActions = 'init' | 'sign' | 'complete' | 'abort';
|
|
17
|
-
declare type RequestHandler = <T = Record<string, any>>(action: RouteActions, data: Record<string, any>) => Promise<T>;
|
|
18
|
-
|
|
19
|
-
export interface S3MultipartUploaderOptions {
|
|
20
|
-
profile?: string;
|
|
21
|
-
chunkSize: number;
|
|
22
|
-
concurrency: number;
|
|
23
|
-
routes: RoutingOptions;
|
|
24
|
-
requestHandler?: RequestHandler;
|
|
25
|
-
onProgress?: ProgressEventHandler;
|
|
26
|
-
ACL?: string;
|
|
27
|
-
extra?: Record<string, any>;
|
|
28
|
-
|
|
29
|
-
// maxRetries?: number;
|
|
30
|
-
// endpoint: string;
|
|
31
|
-
// subfolder?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const defaultOptions: Partial<S3MultipartUploaderOptions> = {
|
|
35
|
-
chunkSize: 5 * 1024 * 1024, // 5MB
|
|
36
|
-
concurrency: 2,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export interface S3MultipartUploaderRequestOptions {
|
|
40
|
-
onProgress?: ProgressEventHandler;
|
|
41
|
-
filename?: string;
|
|
42
|
-
ContentType?: string;
|
|
43
|
-
ContentDisposition?: string;
|
|
44
|
-
ACL?: string;
|
|
45
|
-
extra?: Record<string, any>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class S3MultipartUploader extends Mixin(EventMixin) {
|
|
49
|
-
options: S3MultipartUploaderOptions;
|
|
50
|
-
|
|
51
|
-
constructor(options: Partial<S3MultipartUploaderOptions>) {
|
|
52
|
-
super();
|
|
53
|
-
this.options = mergeDeep({}, defaultOptions, options);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async upload(
|
|
57
|
-
file: string | File | Blob,
|
|
58
|
-
path: string,
|
|
59
|
-
options: S3MultipartUploaderRequestOptions = {}
|
|
60
|
-
): Promise<{ url: string; }> {
|
|
61
|
-
const extra: Record<string, any> = { ...(this.options.extra ?? {}), ...(options.extra ?? {}) };
|
|
62
|
-
|
|
63
|
-
if (typeof file === 'string') {
|
|
64
|
-
file = new Blob([file], { type: options['ContentType'] || 'text/plain' });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (file instanceof Blob && !(file instanceof File)) {
|
|
68
|
-
if (path.endsWith('.{ext}')) {
|
|
69
|
-
throw new Error('If using Blob or file data string, you must provide a valid file extension in the path.');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
file = new File([file], 'blob', { type: file.type });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (file instanceof File) {
|
|
76
|
-
extra['ContentType'] = options['ContentType'] || file.type;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (options.ACL || this.options.ACL) {
|
|
80
|
-
extra.ACL = options.ACL || this.options.ACL;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
path = this.replaceExt(path, file);
|
|
84
|
-
|
|
85
|
-
const initData: Record<string, any> = { extra, path, profile: this.options.profile };
|
|
86
|
-
|
|
87
|
-
if (options['filename']) {
|
|
88
|
-
initData['filename'] = options['filename'];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
this.trigger('start', file, initData);
|
|
92
|
-
|
|
93
|
-
// @Request sign
|
|
94
|
-
const { id } = await this.request<{ id: string; }>(
|
|
95
|
-
'init',
|
|
96
|
-
initData
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const chunkSize = this.options.chunkSize;
|
|
101
|
-
const chunks = Math.ceil(file.size / chunkSize);
|
|
102
|
-
|
|
103
|
-
let uploadedBytes = 0;
|
|
104
|
-
let parts: { ETag: string, PartNumber: number }[] = [];
|
|
105
|
-
let currentPart = 1;
|
|
106
|
-
const queue = createQueue(this.options.concurrency);
|
|
107
|
-
const promises = [];
|
|
108
|
-
const partsUploaded: Record<number, number> = {};
|
|
109
|
-
|
|
110
|
-
// Loop from 1 to chunks
|
|
111
|
-
while (currentPart <= chunks) {
|
|
112
|
-
const partNumber = currentPart;
|
|
113
|
-
|
|
114
|
-
// Push to queue
|
|
115
|
-
const p = queue.push(async () => {
|
|
116
|
-
const { blob, etag } = await this.uploadPart(
|
|
117
|
-
file as File,
|
|
118
|
-
{
|
|
119
|
-
id,
|
|
120
|
-
path,
|
|
121
|
-
partNumber,
|
|
122
|
-
chunkSize,
|
|
123
|
-
onUploadProgress: (e) => {
|
|
124
|
-
partsUploaded[partNumber] = e.loaded;
|
|
125
|
-
|
|
126
|
-
const uploaded = Object.values(partsUploaded).reduce((sum, a) => sum + a, 0);
|
|
127
|
-
|
|
128
|
-
this.updateProgress(uploaded, file.size, options);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
uploadedBytes += blob.size;
|
|
134
|
-
|
|
135
|
-
this.updateProgress(uploadedBytes, file.size, options);
|
|
136
|
-
|
|
137
|
-
parts.push({ ETag: etag, PartNumber: partNumber });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
promises.push(p);
|
|
141
|
-
|
|
142
|
-
currentPart++;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
await Promise.all(promises);
|
|
146
|
-
|
|
147
|
-
// @Request sign
|
|
148
|
-
const { url } = await this.request<{ url: string }>(
|
|
149
|
-
'complete',
|
|
150
|
-
{
|
|
151
|
-
id,
|
|
152
|
-
path,
|
|
153
|
-
parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
154
|
-
profile: this.options.profile,
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
this.trigger('success', url);
|
|
159
|
-
|
|
160
|
-
return { url };
|
|
161
|
-
} catch (e) {
|
|
162
|
-
await this.abort(id, path);
|
|
163
|
-
|
|
164
|
-
throw e;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
protected async uploadPart(
|
|
169
|
-
file: File,
|
|
170
|
-
payload: {
|
|
171
|
-
id: string;
|
|
172
|
-
path: string;
|
|
173
|
-
partNumber: number;
|
|
174
|
-
chunkSize: number;
|
|
175
|
-
onUploadProgress: (e: AxiosProgressEvent) => void;
|
|
176
|
-
}
|
|
177
|
-
) {
|
|
178
|
-
const http = await useHttpClient();
|
|
179
|
-
const { id, path, partNumber, chunkSize, onUploadProgress } = payload;
|
|
180
|
-
|
|
181
|
-
const start = (partNumber - 1) * chunkSize;
|
|
182
|
-
const end = Math.min(partNumber * chunkSize, file.size);
|
|
183
|
-
|
|
184
|
-
const blob = file.slice(start, end);
|
|
185
|
-
|
|
186
|
-
// @Request sign
|
|
187
|
-
const { url } = await this.request<{ url: string; }>(
|
|
188
|
-
'sign',
|
|
189
|
-
{
|
|
190
|
-
id,
|
|
191
|
-
path,
|
|
192
|
-
partNumber,
|
|
193
|
-
profile: this.options.profile,
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// PUT to S3
|
|
198
|
-
const res = await http.put(
|
|
199
|
-
url,
|
|
200
|
-
blob,
|
|
201
|
-
{
|
|
202
|
-
onUploadProgress,
|
|
203
|
-
}
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const etag = String((res.headers as AxiosResponseHeaders).get('ETag') || '');
|
|
207
|
-
|
|
208
|
-
return { blob, etag };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
protected async request<T = Record<string, any>>(action: RouteActions, body: Record<string, any>): Promise<T> {
|
|
212
|
-
if (this.options.requestHandler) {
|
|
213
|
-
return this.options.requestHandler<T>(action, body);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const http = await useHttpClient();
|
|
217
|
-
|
|
218
|
-
const res = await http.post<ApiReturn<T>>(await this.resolveRoute(action), body);
|
|
219
|
-
|
|
220
|
-
return res.data.data;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async abort(id: string, path: string) {
|
|
224
|
-
await this.request(
|
|
225
|
-
'abort',
|
|
226
|
-
{
|
|
227
|
-
id,
|
|
228
|
-
path,
|
|
229
|
-
profile: this.options.profile,
|
|
230
|
-
}
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
updateProgress(loaded: number, total: number, options: S3MultipartUploaderRequestOptions) {
|
|
235
|
-
const percentage = (loaded / total) * 100;
|
|
236
|
-
|
|
237
|
-
const event: ProgressEvent = { percentage, loaded, total };
|
|
238
|
-
|
|
239
|
-
this.trigger('progress', event);
|
|
240
|
-
|
|
241
|
-
this.options.onProgress?.(event);
|
|
242
|
-
|
|
243
|
-
if (options.onProgress) {
|
|
244
|
-
options.onProgress(event);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async resolveRoute(action: RouteActions): Promise<string> {
|
|
249
|
-
if (typeof this.options.routes === 'function') {
|
|
250
|
-
return this.options.routes(action);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return this.options.routes[action];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
setChunkSize(size: number): this {
|
|
257
|
-
this.options.chunkSize = size;
|
|
258
|
-
|
|
259
|
-
return this;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
setChunkSizeInMiB(size: number): this {
|
|
263
|
-
this.options.chunkSize = size * 1024 * 1024;
|
|
264
|
-
|
|
265
|
-
return this;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
replaceExt(path: string, file: File | Blob): string {
|
|
269
|
-
if (file instanceof File) {
|
|
270
|
-
const fileExt = file.name.split('.').pop();
|
|
271
|
-
|
|
272
|
-
if (path.endsWith('.{ext}')) {
|
|
273
|
-
return path.replace(/\.{ext}$/, fileExt ? '.' + fileExt : '');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return path;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
on(
|
|
281
|
-
event: 'start',
|
|
282
|
-
handler: (file: File, data: { path: string; extra: Record<string, any>; [name: string]: any; }) => void
|
|
283
|
-
): this;
|
|
284
|
-
on(event: 'success', handler: (url: string) => void): this;
|
|
285
|
-
on(event: 'progress', handler: (event: ProgressEvent) => void): this;
|
|
286
|
-
on(event: string | string[], handler: EventHandler): this {
|
|
287
|
-
return super.on(event, handler);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
type ProgressEvent = {
|
|
292
|
-
percentage: number;
|
|
293
|
-
loaded: number;
|
|
294
|
-
total: number;
|
|
295
|
-
};
|
|
296
|
-
type ProgressEventHandler = (e: ProgressEvent) => void;
|
|
297
|
-
|
|
298
|
-
export interface S3MultipartUploaderModule {
|
|
299
|
-
S3MultipartUploader: typeof S3MultipartUploader;
|
|
300
|
-
}
|
|
1
|
+
import { AxiosProgressEvent, AxiosResponseHeaders } from 'axios';
|
|
2
|
+
import { Mixin } from 'ts-mixer';
|
|
3
|
+
import { createQueue, useHttpClient } from '../composable';
|
|
4
|
+
import { EventHandler, EventMixin } from '../events';
|
|
5
|
+
import type { MaybePromise } from '../types';
|
|
6
|
+
import { mergeDeep } from '../utilities';
|
|
7
|
+
import { ApiReturn } from './http-client';
|
|
8
|
+
|
|
9
|
+
declare type RoutingOptions = {
|
|
10
|
+
init: string;
|
|
11
|
+
sign: string;
|
|
12
|
+
complete: string;
|
|
13
|
+
abort: string;
|
|
14
|
+
} | ((action: RouteActions) => MaybePromise<string>);
|
|
15
|
+
|
|
16
|
+
declare type RouteActions = 'init' | 'sign' | 'complete' | 'abort';
|
|
17
|
+
declare type RequestHandler = <T = Record<string, any>>(action: RouteActions, data: Record<string, any>) => Promise<T>;
|
|
18
|
+
|
|
19
|
+
export interface S3MultipartUploaderOptions {
|
|
20
|
+
profile?: string;
|
|
21
|
+
chunkSize: number;
|
|
22
|
+
concurrency: number;
|
|
23
|
+
routes: RoutingOptions;
|
|
24
|
+
requestHandler?: RequestHandler;
|
|
25
|
+
onProgress?: ProgressEventHandler;
|
|
26
|
+
ACL?: string;
|
|
27
|
+
extra?: Record<string, any>;
|
|
28
|
+
|
|
29
|
+
// maxRetries?: number;
|
|
30
|
+
// endpoint: string;
|
|
31
|
+
// subfolder?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultOptions: Partial<S3MultipartUploaderOptions> = {
|
|
35
|
+
chunkSize: 5 * 1024 * 1024, // 5MB
|
|
36
|
+
concurrency: 2,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface S3MultipartUploaderRequestOptions {
|
|
40
|
+
onProgress?: ProgressEventHandler;
|
|
41
|
+
filename?: string;
|
|
42
|
+
ContentType?: string;
|
|
43
|
+
ContentDisposition?: string;
|
|
44
|
+
ACL?: string;
|
|
45
|
+
extra?: Record<string, any>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class S3MultipartUploader extends Mixin(EventMixin) {
|
|
49
|
+
options: S3MultipartUploaderOptions;
|
|
50
|
+
|
|
51
|
+
constructor(options: Partial<S3MultipartUploaderOptions>) {
|
|
52
|
+
super();
|
|
53
|
+
this.options = mergeDeep({}, defaultOptions, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async upload(
|
|
57
|
+
file: string | File | Blob,
|
|
58
|
+
path: string,
|
|
59
|
+
options: S3MultipartUploaderRequestOptions = {}
|
|
60
|
+
): Promise<{ url: string; }> {
|
|
61
|
+
const extra: Record<string, any> = { ...(this.options.extra ?? {}), ...(options.extra ?? {}) };
|
|
62
|
+
|
|
63
|
+
if (typeof file === 'string') {
|
|
64
|
+
file = new Blob([file], { type: options['ContentType'] || 'text/plain' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (file instanceof Blob && !(file instanceof File)) {
|
|
68
|
+
if (path.endsWith('.{ext}')) {
|
|
69
|
+
throw new Error('If using Blob or file data string, you must provide a valid file extension in the path.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
file = new File([file], 'blob', { type: file.type });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (file instanceof File) {
|
|
76
|
+
extra['ContentType'] = options['ContentType'] || file.type;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.ACL || this.options.ACL) {
|
|
80
|
+
extra.ACL = options.ACL || this.options.ACL;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
path = this.replaceExt(path, file);
|
|
84
|
+
|
|
85
|
+
const initData: Record<string, any> = { extra, path, profile: this.options.profile };
|
|
86
|
+
|
|
87
|
+
if (options['filename']) {
|
|
88
|
+
initData['filename'] = options['filename'];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.trigger('start', file, initData);
|
|
92
|
+
|
|
93
|
+
// @Request sign
|
|
94
|
+
const { id } = await this.request<{ id: string; }>(
|
|
95
|
+
'init',
|
|
96
|
+
initData
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const chunkSize = this.options.chunkSize;
|
|
101
|
+
const chunks = Math.ceil(file.size / chunkSize);
|
|
102
|
+
|
|
103
|
+
let uploadedBytes = 0;
|
|
104
|
+
let parts: { ETag: string, PartNumber: number }[] = [];
|
|
105
|
+
let currentPart = 1;
|
|
106
|
+
const queue = createQueue(this.options.concurrency);
|
|
107
|
+
const promises = [];
|
|
108
|
+
const partsUploaded: Record<number, number> = {};
|
|
109
|
+
|
|
110
|
+
// Loop from 1 to chunks
|
|
111
|
+
while (currentPart <= chunks) {
|
|
112
|
+
const partNumber = currentPart;
|
|
113
|
+
|
|
114
|
+
// Push to queue
|
|
115
|
+
const p = queue.push(async () => {
|
|
116
|
+
const { blob, etag } = await this.uploadPart(
|
|
117
|
+
file as File,
|
|
118
|
+
{
|
|
119
|
+
id,
|
|
120
|
+
path,
|
|
121
|
+
partNumber,
|
|
122
|
+
chunkSize,
|
|
123
|
+
onUploadProgress: (e) => {
|
|
124
|
+
partsUploaded[partNumber] = e.loaded;
|
|
125
|
+
|
|
126
|
+
const uploaded = Object.values(partsUploaded).reduce((sum, a) => sum + a, 0);
|
|
127
|
+
|
|
128
|
+
this.updateProgress(uploaded, file.size, options);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
uploadedBytes += blob.size;
|
|
134
|
+
|
|
135
|
+
this.updateProgress(uploadedBytes, file.size, options);
|
|
136
|
+
|
|
137
|
+
parts.push({ ETag: etag, PartNumber: partNumber });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
promises.push(p);
|
|
141
|
+
|
|
142
|
+
currentPart++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await Promise.all(promises);
|
|
146
|
+
|
|
147
|
+
// @Request sign
|
|
148
|
+
const { url } = await this.request<{ url: string }>(
|
|
149
|
+
'complete',
|
|
150
|
+
{
|
|
151
|
+
id,
|
|
152
|
+
path,
|
|
153
|
+
parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
154
|
+
profile: this.options.profile,
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
this.trigger('success', url);
|
|
159
|
+
|
|
160
|
+
return { url };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
await this.abort(id, path);
|
|
163
|
+
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected async uploadPart(
|
|
169
|
+
file: File,
|
|
170
|
+
payload: {
|
|
171
|
+
id: string;
|
|
172
|
+
path: string;
|
|
173
|
+
partNumber: number;
|
|
174
|
+
chunkSize: number;
|
|
175
|
+
onUploadProgress: (e: AxiosProgressEvent) => void;
|
|
176
|
+
}
|
|
177
|
+
) {
|
|
178
|
+
const http = await useHttpClient();
|
|
179
|
+
const { id, path, partNumber, chunkSize, onUploadProgress } = payload;
|
|
180
|
+
|
|
181
|
+
const start = (partNumber - 1) * chunkSize;
|
|
182
|
+
const end = Math.min(partNumber * chunkSize, file.size);
|
|
183
|
+
|
|
184
|
+
const blob = file.slice(start, end);
|
|
185
|
+
|
|
186
|
+
// @Request sign
|
|
187
|
+
const { url } = await this.request<{ url: string; }>(
|
|
188
|
+
'sign',
|
|
189
|
+
{
|
|
190
|
+
id,
|
|
191
|
+
path,
|
|
192
|
+
partNumber,
|
|
193
|
+
profile: this.options.profile,
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// PUT to S3
|
|
198
|
+
const res = await http.put(
|
|
199
|
+
url,
|
|
200
|
+
blob,
|
|
201
|
+
{
|
|
202
|
+
onUploadProgress,
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const etag = String((res.headers as AxiosResponseHeaders).get('ETag') || '');
|
|
207
|
+
|
|
208
|
+
return { blob, etag };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
protected async request<T = Record<string, any>>(action: RouteActions, body: Record<string, any>): Promise<T> {
|
|
212
|
+
if (this.options.requestHandler) {
|
|
213
|
+
return this.options.requestHandler<T>(action, body);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const http = await useHttpClient();
|
|
217
|
+
|
|
218
|
+
const res = await http.post<ApiReturn<T>>(await this.resolveRoute(action), body);
|
|
219
|
+
|
|
220
|
+
return res.data.data;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async abort(id: string, path: string) {
|
|
224
|
+
await this.request(
|
|
225
|
+
'abort',
|
|
226
|
+
{
|
|
227
|
+
id,
|
|
228
|
+
path,
|
|
229
|
+
profile: this.options.profile,
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
updateProgress(loaded: number, total: number, options: S3MultipartUploaderRequestOptions) {
|
|
235
|
+
const percentage = (loaded / total) * 100;
|
|
236
|
+
|
|
237
|
+
const event: ProgressEvent = { percentage, loaded, total };
|
|
238
|
+
|
|
239
|
+
this.trigger('progress', event);
|
|
240
|
+
|
|
241
|
+
this.options.onProgress?.(event);
|
|
242
|
+
|
|
243
|
+
if (options.onProgress) {
|
|
244
|
+
options.onProgress(event);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async resolveRoute(action: RouteActions): Promise<string> {
|
|
249
|
+
if (typeof this.options.routes === 'function') {
|
|
250
|
+
return this.options.routes(action);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return this.options.routes[action];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setChunkSize(size: number): this {
|
|
257
|
+
this.options.chunkSize = size;
|
|
258
|
+
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setChunkSizeInMiB(size: number): this {
|
|
263
|
+
this.options.chunkSize = size * 1024 * 1024;
|
|
264
|
+
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
replaceExt(path: string, file: File | Blob): string {
|
|
269
|
+
if (file instanceof File) {
|
|
270
|
+
const fileExt = file.name.split('.').pop();
|
|
271
|
+
|
|
272
|
+
if (path.endsWith('.{ext}')) {
|
|
273
|
+
return path.replace(/\.{ext}$/, fileExt ? '.' + fileExt : '');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return path;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
on(
|
|
281
|
+
event: 'start',
|
|
282
|
+
handler: (file: File, data: { path: string; extra: Record<string, any>; [name: string]: any; }) => void
|
|
283
|
+
): this;
|
|
284
|
+
on(event: 'success', handler: (url: string) => void): this;
|
|
285
|
+
on(event: 'progress', handler: (event: ProgressEvent) => void): this;
|
|
286
|
+
on(event: string | string[], handler: EventHandler): this {
|
|
287
|
+
return super.on(event, handler);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type ProgressEvent = {
|
|
292
|
+
percentage: number;
|
|
293
|
+
loaded: number;
|
|
294
|
+
total: number;
|
|
295
|
+
};
|
|
296
|
+
type ProgressEventHandler = (e: ProgressEvent) => void;
|
|
297
|
+
|
|
298
|
+
export interface S3MultipartUploaderModule {
|
|
299
|
+
S3MultipartUploader: typeof S3MultipartUploader;
|
|
300
|
+
}
|