nec-oss 0.1.0
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/dist/index.d.ts +122 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +801 -0
- package/dist/index.js.map +1 -0
- package/dist/nec-oss.iife.js +812 -0
- package/dist/nec-oss.iife.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
var NecOss = (function(exports) {
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
//#region ../core/dist/index.js
|
|
5
|
+
var EventEmitter = class {
|
|
6
|
+
listeners = /* @__PURE__ */ new Map();
|
|
7
|
+
emit(eventName, payload) {
|
|
8
|
+
(this.listeners.get(eventName) ?? /* @__PURE__ */ new Set()).forEach((handler) => {
|
|
9
|
+
handler(payload);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
on(eventName, handler) {
|
|
13
|
+
const handlers = this.listeners.get(eventName) ?? /* @__PURE__ */ new Set();
|
|
14
|
+
handlers.add(handler);
|
|
15
|
+
this.listeners.set(eventName, handlers);
|
|
16
|
+
return () => {
|
|
17
|
+
handlers.delete(handler);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
let nextId = 0;
|
|
22
|
+
function createUploadFile(file) {
|
|
23
|
+
const name = getFileName(file);
|
|
24
|
+
const type = "type" in file ? file.type : "";
|
|
25
|
+
const size = "size" in file ? file.size : 0;
|
|
26
|
+
nextId += 1;
|
|
27
|
+
return {
|
|
28
|
+
id: `upload-${Date.now()}-${nextId}`,
|
|
29
|
+
file,
|
|
30
|
+
name,
|
|
31
|
+
progress: {
|
|
32
|
+
percent: 0,
|
|
33
|
+
loaded: 0,
|
|
34
|
+
total: size
|
|
35
|
+
},
|
|
36
|
+
size,
|
|
37
|
+
status: "pending",
|
|
38
|
+
type
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function normalizeFiles(files) {
|
|
42
|
+
if (isFileList(files)) return Array.from(files);
|
|
43
|
+
if (Array.isArray(files)) return files;
|
|
44
|
+
return [files];
|
|
45
|
+
}
|
|
46
|
+
function isFileList(files) {
|
|
47
|
+
return typeof FileList !== "undefined" && files instanceof FileList;
|
|
48
|
+
}
|
|
49
|
+
function getFileName(file) {
|
|
50
|
+
if ("name" in file && typeof file.name === "string") return file.name;
|
|
51
|
+
return "blob";
|
|
52
|
+
}
|
|
53
|
+
function validateFiles(files, existingFiles, options = {}) {
|
|
54
|
+
const acceptedFiles = [];
|
|
55
|
+
const invalidFiles = [];
|
|
56
|
+
return files.reduce((promise, file) => {
|
|
57
|
+
return promise.then(() => {
|
|
58
|
+
return validateFile(file, {
|
|
59
|
+
acceptedFiles,
|
|
60
|
+
existingFiles,
|
|
61
|
+
files
|
|
62
|
+
}, options).then((result) => {
|
|
63
|
+
if (result.valid) {
|
|
64
|
+
acceptedFiles.push(file);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
invalidFiles.push({
|
|
68
|
+
file: file.file,
|
|
69
|
+
name: file.name,
|
|
70
|
+
reason: result.message ?? "文件校验失败",
|
|
71
|
+
rule: result.rule ?? "custom"
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}, Promise.resolve()).then(() => ({
|
|
76
|
+
acceptedFiles,
|
|
77
|
+
invalidFiles
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
function validateFile(file, context, options) {
|
|
81
|
+
const builtInResult = validateBuiltInRules(file, context, options);
|
|
82
|
+
if (!builtInResult.valid) return Promise.resolve(builtInResult);
|
|
83
|
+
return (options.customValidators ?? []).reduce((promise, validator) => {
|
|
84
|
+
return promise.then((currentResult) => {
|
|
85
|
+
if (!currentResult.valid) return currentResult;
|
|
86
|
+
return Promise.resolve(validator(file, context)).then(normalizeValidationResult);
|
|
87
|
+
});
|
|
88
|
+
}, Promise.resolve({ valid: true }));
|
|
89
|
+
}
|
|
90
|
+
function validateBuiltInRules(file, context, options) {
|
|
91
|
+
if (options.allowEmpty !== true && file.size === 0) return {
|
|
92
|
+
valid: false,
|
|
93
|
+
message: "不允许上传空文件",
|
|
94
|
+
rule: "allowEmpty"
|
|
95
|
+
};
|
|
96
|
+
if (typeof options.maxCount === "number" && context.existingFiles.length + context.acceptedFiles.length >= options.maxCount) return {
|
|
97
|
+
valid: false,
|
|
98
|
+
message: `最多只能上传 ${options.maxCount} 个文件`,
|
|
99
|
+
rule: "maxCount"
|
|
100
|
+
};
|
|
101
|
+
if (typeof options.maxSize === "number" && file.size > options.maxSize) return {
|
|
102
|
+
valid: false,
|
|
103
|
+
message: `文件大小不能超过 ${options.maxSize} 字节`,
|
|
104
|
+
rule: "maxSize"
|
|
105
|
+
};
|
|
106
|
+
if (typeof options.minSize === "number" && file.size < options.minSize) return {
|
|
107
|
+
valid: false,
|
|
108
|
+
message: `文件大小不能小于 ${options.minSize} 字节`,
|
|
109
|
+
rule: "minSize"
|
|
110
|
+
};
|
|
111
|
+
if (options.allowDuplicateName !== true && hasDuplicateName(file, context)) return {
|
|
112
|
+
valid: false,
|
|
113
|
+
message: "不允许上传重名文件",
|
|
114
|
+
rule: "allowDuplicateName"
|
|
115
|
+
};
|
|
116
|
+
if (options.accept && !matchesAccept(file, options.accept)) return {
|
|
117
|
+
valid: false,
|
|
118
|
+
message: "文件类型不符合要求",
|
|
119
|
+
rule: "accept"
|
|
120
|
+
};
|
|
121
|
+
if (options.extensions && options.extensions.length > 0 && !matchesExtension(file, options.extensions)) return {
|
|
122
|
+
valid: false,
|
|
123
|
+
message: "文件扩展名不符合要求",
|
|
124
|
+
rule: "extensions"
|
|
125
|
+
};
|
|
126
|
+
return { valid: true };
|
|
127
|
+
}
|
|
128
|
+
function normalizeValidationResult(result) {
|
|
129
|
+
if (typeof result === "boolean") return { valid: result };
|
|
130
|
+
if (typeof result === "string") return {
|
|
131
|
+
message: result,
|
|
132
|
+
rule: "custom",
|
|
133
|
+
valid: false
|
|
134
|
+
};
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function hasDuplicateName(file, context) {
|
|
138
|
+
return [...context.existingFiles, ...context.files].filter((item) => item.name === file.name).length > 1;
|
|
139
|
+
}
|
|
140
|
+
function matchesAccept(file, accept) {
|
|
141
|
+
return (Array.isArray(accept) ? accept : accept.split(",")).map((item) => item.trim()).filter(Boolean).some((item) => {
|
|
142
|
+
if (item.endsWith("/*")) return file.type.startsWith(item.slice(0, -1));
|
|
143
|
+
if (item.startsWith(".")) return file.name.toLowerCase().endsWith(item.toLowerCase());
|
|
144
|
+
return file.type === item;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function matchesExtension(file, extensions) {
|
|
148
|
+
const normalizedName = file.name.toLowerCase();
|
|
149
|
+
return extensions.some((extension) => {
|
|
150
|
+
const normalizedExtension = extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
|
|
151
|
+
return normalizedName.endsWith(normalizedExtension);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function createUploader(options) {
|
|
155
|
+
return new UploadQueue(options);
|
|
156
|
+
}
|
|
157
|
+
var UploadQueue = class {
|
|
158
|
+
emitter = new EventEmitter();
|
|
159
|
+
files = [];
|
|
160
|
+
taskMap = /* @__PURE__ */ new Map();
|
|
161
|
+
activeCount = 0;
|
|
162
|
+
runningPromise;
|
|
163
|
+
resolveRunning;
|
|
164
|
+
constructor(options) {
|
|
165
|
+
this.options = options;
|
|
166
|
+
}
|
|
167
|
+
add(inputFiles) {
|
|
168
|
+
return validateFiles(normalizeFiles(inputFiles).map(createUploadFile), this.files, this.options.validate).then((result) => {
|
|
169
|
+
result.acceptedFiles.forEach((file) => {
|
|
170
|
+
this.files.push(file);
|
|
171
|
+
this.taskMap.set(file.id, new UploadTaskImpl(file, this));
|
|
172
|
+
});
|
|
173
|
+
this.emitQueueChange();
|
|
174
|
+
return result;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
start() {
|
|
178
|
+
if (this.runningPromise) return this.runningPromise;
|
|
179
|
+
this.runningPromise = new Promise((resolve) => {
|
|
180
|
+
this.resolveRunning = resolve;
|
|
181
|
+
}).finally(() => {
|
|
182
|
+
this.resolveRunning = void 0;
|
|
183
|
+
this.runningPromise = void 0;
|
|
184
|
+
});
|
|
185
|
+
this.runNext();
|
|
186
|
+
return this.runningPromise;
|
|
187
|
+
}
|
|
188
|
+
pauseAll() {
|
|
189
|
+
return this.getTasks().reduce((promise, task) => promise.then(() => task.pause()), Promise.resolve()).then(() => void 0);
|
|
190
|
+
}
|
|
191
|
+
resumeAll() {
|
|
192
|
+
this.files.filter((file) => file.status === "paused").forEach((file) => {
|
|
193
|
+
file.status = "pending";
|
|
194
|
+
});
|
|
195
|
+
this.emitQueueChange();
|
|
196
|
+
return this.start();
|
|
197
|
+
}
|
|
198
|
+
cancelAll() {
|
|
199
|
+
return this.getTasks().reduce((promise, task) => promise.then(() => task.cancel()), Promise.resolve()).then(() => void 0);
|
|
200
|
+
}
|
|
201
|
+
cancel(fileIds) {
|
|
202
|
+
return this.runByFileIds(fileIds, (task) => task.cancel());
|
|
203
|
+
}
|
|
204
|
+
pause(fileIds) {
|
|
205
|
+
return this.runByFileIds(fileIds, (task) => task.pause());
|
|
206
|
+
}
|
|
207
|
+
resume(fileIds) {
|
|
208
|
+
return this.runByFileIds(fileIds, (task) => task.resume());
|
|
209
|
+
}
|
|
210
|
+
retry(fileIds) {
|
|
211
|
+
return this.runByFileIds(fileIds, (task) => this.retryTask(task.file));
|
|
212
|
+
}
|
|
213
|
+
clear() {
|
|
214
|
+
if (!this.files.every((file) => file.status !== "uploading")) throw new Error("上传中不能清空队列");
|
|
215
|
+
this.files.splice(0, this.files.length);
|
|
216
|
+
this.taskMap.clear();
|
|
217
|
+
this.emitQueueChange();
|
|
218
|
+
}
|
|
219
|
+
getFiles() {
|
|
220
|
+
return [...this.files];
|
|
221
|
+
}
|
|
222
|
+
getTasks() {
|
|
223
|
+
return Array.from(this.taskMap.values());
|
|
224
|
+
}
|
|
225
|
+
on(eventName, handler) {
|
|
226
|
+
return this.emitter.on(eventName, handler);
|
|
227
|
+
}
|
|
228
|
+
runByFileIds(fileIds, runner) {
|
|
229
|
+
return normalizeFileIds(fileIds).reduce((promise, fileId) => {
|
|
230
|
+
return promise.then(() => {
|
|
231
|
+
const task = this.taskMap.get(fileId);
|
|
232
|
+
if (!task) throw new Error(`未找到文件任务:${fileId}`);
|
|
233
|
+
return runner(task);
|
|
234
|
+
});
|
|
235
|
+
}, Promise.resolve());
|
|
236
|
+
}
|
|
237
|
+
pauseTask(file) {
|
|
238
|
+
const task = this.taskMap.get(file.id);
|
|
239
|
+
if (!task || file.status !== "uploading") {
|
|
240
|
+
if (file.status === "pending") {
|
|
241
|
+
file.status = "paused";
|
|
242
|
+
this.emitQueueChange();
|
|
243
|
+
}
|
|
244
|
+
return Promise.resolve();
|
|
245
|
+
}
|
|
246
|
+
return Promise.resolve(this.options.provider.transformLifecycleRequest?.({}, {
|
|
247
|
+
file,
|
|
248
|
+
lifecycle: "pause"
|
|
249
|
+
}) ?? {}).then(() => this.getAdapter().pause?.({
|
|
250
|
+
file,
|
|
251
|
+
lifecycle: "pause",
|
|
252
|
+
prepared: task.prepared
|
|
253
|
+
})).then(() => {
|
|
254
|
+
task.abortController?.abort();
|
|
255
|
+
file.status = "paused";
|
|
256
|
+
this.emitQueueChange();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
resumeTask(file) {
|
|
260
|
+
return Promise.resolve(this.options.provider.transformLifecycleRequest?.({}, {
|
|
261
|
+
file,
|
|
262
|
+
lifecycle: "resume"
|
|
263
|
+
}) ?? {}).then(() => this.getAdapter().resume?.({
|
|
264
|
+
file,
|
|
265
|
+
lifecycle: "resume",
|
|
266
|
+
prepared: this.taskMap.get(file.id)?.prepared
|
|
267
|
+
})).then(() => {
|
|
268
|
+
if (file.status === "paused") {
|
|
269
|
+
file.status = "pending";
|
|
270
|
+
this.emitQueueChange();
|
|
271
|
+
}
|
|
272
|
+
return this.start();
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
retryTask(file) {
|
|
276
|
+
if (file.status === "uploading") return Promise.resolve();
|
|
277
|
+
file.status = "pending";
|
|
278
|
+
file.error = void 0;
|
|
279
|
+
file.response = void 0;
|
|
280
|
+
file.rawResponse = void 0;
|
|
281
|
+
file.progress = {
|
|
282
|
+
loaded: 0,
|
|
283
|
+
percent: 0,
|
|
284
|
+
total: file.size
|
|
285
|
+
};
|
|
286
|
+
this.emitQueueChange();
|
|
287
|
+
return this.start();
|
|
288
|
+
}
|
|
289
|
+
cancelTask(file) {
|
|
290
|
+
const task = this.taskMap.get(file.id);
|
|
291
|
+
if (!task || file.status === "pending" || file.status === "paused") {
|
|
292
|
+
file.status = "canceled";
|
|
293
|
+
this.emitQueueChange();
|
|
294
|
+
return Promise.resolve();
|
|
295
|
+
}
|
|
296
|
+
return Promise.resolve(this.options.provider.transformLifecycleRequest?.({}, {
|
|
297
|
+
file,
|
|
298
|
+
lifecycle: "cancel"
|
|
299
|
+
}) ?? {}).then(() => this.getAdapter().cancel?.({
|
|
300
|
+
file,
|
|
301
|
+
lifecycle: "cancel",
|
|
302
|
+
prepared: task?.prepared
|
|
303
|
+
})).then(() => {
|
|
304
|
+
task?.abortController?.abort();
|
|
305
|
+
file.status = "canceled";
|
|
306
|
+
this.emitQueueChange();
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
runNext() {
|
|
310
|
+
const maxConcurrency = Math.max(1, this.options.maxConcurrency ?? 3);
|
|
311
|
+
while (this.activeCount < maxConcurrency) {
|
|
312
|
+
const nextFile = this.files.find((file) => file.status === "pending");
|
|
313
|
+
if (!nextFile) break;
|
|
314
|
+
this.activeCount += 1;
|
|
315
|
+
this.uploadFile(nextFile).finally(() => {
|
|
316
|
+
this.activeCount -= 1;
|
|
317
|
+
this.runNext();
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (this.activeCount === 0 && !this.files.some((file) => file.status === "pending")) {
|
|
321
|
+
this.emitter.emit("complete", { files: this.getFiles() });
|
|
322
|
+
this.resolveRunning?.();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
uploadFile(file) {
|
|
326
|
+
const task = this.taskMap.get(file.id);
|
|
327
|
+
if (!task) return Promise.resolve();
|
|
328
|
+
file.status = "uploading";
|
|
329
|
+
task.abortController = new AbortController();
|
|
330
|
+
this.emitQueueChange();
|
|
331
|
+
return Promise.resolve(this.options.provider.prepare(file)).then((prepared) => {
|
|
332
|
+
task.prepared = prepared;
|
|
333
|
+
return this.getAdapter().upload({
|
|
334
|
+
file,
|
|
335
|
+
onProgress: (progress) => this.handleProgress(file, progress),
|
|
336
|
+
prepared,
|
|
337
|
+
signal: task.abortController?.signal ?? new AbortController().signal
|
|
338
|
+
});
|
|
339
|
+
}).then((result) => this.handleSuccess(file, result, task.prepared)).catch((error) => this.handleError(file, error));
|
|
340
|
+
}
|
|
341
|
+
handleProgress(file, progress) {
|
|
342
|
+
file.progress = progress;
|
|
343
|
+
file.checkpoint = progress.checkpoint;
|
|
344
|
+
this.emitter.emit("progress", {
|
|
345
|
+
file,
|
|
346
|
+
progress
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
handleSuccess(file, result, prepared) {
|
|
350
|
+
const key = result.key ?? prepared?.params.key ?? file.name;
|
|
351
|
+
const context = {
|
|
352
|
+
file,
|
|
353
|
+
lifecycle: "upload",
|
|
354
|
+
params: prepared?.params
|
|
355
|
+
};
|
|
356
|
+
return Promise.resolve(this.options.provider.response?.(result.rawResponse, context) ?? result.rawResponse).then((response) => {
|
|
357
|
+
file.status = "success";
|
|
358
|
+
file.rawResponse = result.rawResponse;
|
|
359
|
+
file.response = response;
|
|
360
|
+
file.checkpoint = result.checkpoint;
|
|
361
|
+
const payload = {
|
|
362
|
+
checkpoint: result.checkpoint,
|
|
363
|
+
key,
|
|
364
|
+
rawResponse: result.rawResponse,
|
|
365
|
+
response,
|
|
366
|
+
url: result.url
|
|
367
|
+
};
|
|
368
|
+
this.emitter.emit("success", {
|
|
369
|
+
file,
|
|
370
|
+
result: payload
|
|
371
|
+
});
|
|
372
|
+
this.emitQueueChange();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
handleError(file, error) {
|
|
376
|
+
if (file.status === "paused" || file.status === "canceled") return;
|
|
377
|
+
const uploadError = {
|
|
378
|
+
checkpoint: file.checkpoint,
|
|
379
|
+
code: getErrorCode(error),
|
|
380
|
+
message: getErrorMessage(error),
|
|
381
|
+
raw: error
|
|
382
|
+
};
|
|
383
|
+
file.status = "error";
|
|
384
|
+
file.error = uploadError;
|
|
385
|
+
this.emitter.emit("error", {
|
|
386
|
+
error: uploadError,
|
|
387
|
+
file
|
|
388
|
+
});
|
|
389
|
+
this.emitQueueChange();
|
|
390
|
+
}
|
|
391
|
+
emitQueueChange() {
|
|
392
|
+
this.emitter.emit("change", { files: this.getFiles() });
|
|
393
|
+
}
|
|
394
|
+
getAdapter() {
|
|
395
|
+
const adapter = this.options.adapter ?? this.options.provider.adapter;
|
|
396
|
+
if (!adapter) throw new Error("上传缺少 adapter,请通过 provider 内置 adapter 或显式传入 adapter");
|
|
397
|
+
return adapter;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var UploadTaskImpl = class {
|
|
401
|
+
abortController;
|
|
402
|
+
prepared;
|
|
403
|
+
constructor(file, queue) {
|
|
404
|
+
this.file = file;
|
|
405
|
+
this.queue = queue;
|
|
406
|
+
}
|
|
407
|
+
cancel() {
|
|
408
|
+
return this.queue.cancelTask(this.file);
|
|
409
|
+
}
|
|
410
|
+
on(eventName, handler) {
|
|
411
|
+
return this.queue.on(eventName, handler);
|
|
412
|
+
}
|
|
413
|
+
pause() {
|
|
414
|
+
return this.queue.pauseTask(this.file);
|
|
415
|
+
}
|
|
416
|
+
resume() {
|
|
417
|
+
return this.queue.resumeTask(this.file);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
function getErrorCode(error) {
|
|
421
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") return error.code;
|
|
422
|
+
return "UPLOAD_ERROR";
|
|
423
|
+
}
|
|
424
|
+
function getErrorMessage(error) {
|
|
425
|
+
if (error instanceof Error) return error.message;
|
|
426
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
|
|
427
|
+
return "文件上传失败";
|
|
428
|
+
}
|
|
429
|
+
function normalizeFileIds(fileIds) {
|
|
430
|
+
return Array.isArray(fileIds) ? fileIds : [fileIds];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/oss/runtime.ts
|
|
435
|
+
const DEFAULT_OSS_STS_URL = "/education/serverApi/oss-sts/token";
|
|
436
|
+
function loadOssSdk(options) {
|
|
437
|
+
return Promise.resolve(options.loadSdk?.() ?? import("ali-oss").then((module) => module.default));
|
|
438
|
+
}
|
|
439
|
+
function buildOssClientOptions(options, token, refreshSTSToken) {
|
|
440
|
+
const runtimeConfig = resolveRuntimeConfig(options, token);
|
|
441
|
+
return {
|
|
442
|
+
accessKeyId: token.accessKeyId,
|
|
443
|
+
accessKeySecret: token.accessKeySecret,
|
|
444
|
+
authorizationV4: options.authorizationV4 ?? true,
|
|
445
|
+
bucket: runtimeConfig.bucket,
|
|
446
|
+
cname: runtimeConfig.cname,
|
|
447
|
+
endpoint: runtimeConfig.endpoint,
|
|
448
|
+
region: runtimeConfig.region,
|
|
449
|
+
...refreshSTSToken ? { refreshSTSToken } : {},
|
|
450
|
+
secure: runtimeConfig.secure,
|
|
451
|
+
stsToken: token.stsToken,
|
|
452
|
+
timeout: options.timeout
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function resolveRuntimeConfig(options, token) {
|
|
456
|
+
const bucket = options.bucket ?? token.bucket;
|
|
457
|
+
const endpoint = options.endpoint ?? token.endpoint;
|
|
458
|
+
const region = options.region ?? token.region ?? getRegionFromEndpoint(endpoint);
|
|
459
|
+
if (!bucket || !region) throw new Error("OSS 缺少 bucket 或 endpoint/region,请确认 STS 响应或 OSS 配置");
|
|
460
|
+
return {
|
|
461
|
+
bucket,
|
|
462
|
+
cname: options.cname ?? token.cname,
|
|
463
|
+
endpoint,
|
|
464
|
+
region,
|
|
465
|
+
secure: options.secure ?? token.secure
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function requestAndTransformStsToken(context, options) {
|
|
469
|
+
const request = {
|
|
470
|
+
headers: {},
|
|
471
|
+
method: "GET",
|
|
472
|
+
query: {},
|
|
473
|
+
url: options.stsUrl ?? DEFAULT_OSS_STS_URL
|
|
474
|
+
};
|
|
475
|
+
return Promise.resolve(options.beforeStsTokenReq?.(request, context) ?? request).then((nextRequest) => requestStsToken(nextRequest, context, options)).then((stsResponse) => {
|
|
476
|
+
return Promise.resolve(options.beforeStsTokenRes?.(stsResponse, context) ?? defaultTransformStsResponse(stsResponse));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function createRefreshStsToken(context, options) {
|
|
480
|
+
if (!options.refreshStsToken && !options.refreshStsUrl) return;
|
|
481
|
+
if (options.refreshStsToken) {
|
|
482
|
+
const refreshStsToken = options.refreshStsToken;
|
|
483
|
+
return () => refreshStsToken(context);
|
|
484
|
+
}
|
|
485
|
+
return () => requestAndTransformRefreshStsToken(context, options);
|
|
486
|
+
}
|
|
487
|
+
function getOssRuntimeConfig(token) {
|
|
488
|
+
const region = token.region ?? getRegionFromEndpoint(token.endpoint);
|
|
489
|
+
if (!token.bucket || !region) throw new Error("STS 响应缺少 bucket 或 endpoint/region");
|
|
490
|
+
return {
|
|
491
|
+
bucket: token.bucket,
|
|
492
|
+
cname: token.cname,
|
|
493
|
+
endpoint: token.endpoint,
|
|
494
|
+
region,
|
|
495
|
+
secure: token.secure
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function requestStsToken(request, context, options) {
|
|
499
|
+
return Promise.resolve(options.requestStsToken?.(request, context) ?? sendRequest(request));
|
|
500
|
+
}
|
|
501
|
+
function requestAndTransformRefreshStsToken(context, options) {
|
|
502
|
+
const request = {
|
|
503
|
+
headers: {},
|
|
504
|
+
method: "GET",
|
|
505
|
+
query: {},
|
|
506
|
+
url: options.refreshStsUrl
|
|
507
|
+
};
|
|
508
|
+
return Promise.resolve(options.beforeRefreshStsTokenReq?.(request, context) ?? request).then((nextRequest) => requestRefreshStsToken(nextRequest, context, options)).then((refreshResponse) => {
|
|
509
|
+
return Promise.resolve(options.beforeRefreshStsTokenRes?.(refreshResponse, context) ?? defaultTransformRefreshStsResponse(refreshResponse));
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function requestRefreshStsToken(request, context, options) {
|
|
513
|
+
return Promise.resolve(options.requestRefreshStsToken?.(request, context) ?? sendRequest(request));
|
|
514
|
+
}
|
|
515
|
+
function sendRequest(request) {
|
|
516
|
+
if (!request.url) return Promise.reject(/* @__PURE__ */ new Error("STS 请求地址不能为空"));
|
|
517
|
+
const method = request.method ?? "GET";
|
|
518
|
+
const headers = request.headers ?? {};
|
|
519
|
+
const url = buildUrl(request.url, request.query);
|
|
520
|
+
const init = {
|
|
521
|
+
headers,
|
|
522
|
+
method
|
|
523
|
+
};
|
|
524
|
+
if (method.toUpperCase() !== "GET" && typeof request.body !== "undefined") {
|
|
525
|
+
init.body = typeof request.body === "string" ? request.body : JSON.stringify(request.body);
|
|
526
|
+
init.headers = {
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
...headers
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return fetch(url, init).then((response) => {
|
|
532
|
+
if (!response.ok) throw new Error(`STS 请求失败:${response.status}`);
|
|
533
|
+
return response.json();
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function defaultTransformRefreshStsResponse(response) {
|
|
537
|
+
const token = defaultTransformStsResponse(response);
|
|
538
|
+
return {
|
|
539
|
+
accessKeyId: token.accessKeyId,
|
|
540
|
+
accessKeySecret: token.accessKeySecret,
|
|
541
|
+
stsToken: token.stsToken
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function defaultTransformStsResponse(response) {
|
|
545
|
+
const candidates = [
|
|
546
|
+
response,
|
|
547
|
+
getObjectValue(response, "data"),
|
|
548
|
+
getObjectValue(getObjectValue(response, "data"), "credentials"),
|
|
549
|
+
getObjectValue(response, "credentials")
|
|
550
|
+
];
|
|
551
|
+
for (const candidate of candidates) {
|
|
552
|
+
if (!isRecord(candidate)) continue;
|
|
553
|
+
const accessKeyId = getStringValue(candidate, ["accessKeyId", "AccessKeyId"]);
|
|
554
|
+
const accessKeySecret = getStringValue(candidate, ["accessKeySecret", "AccessKeySecret"]);
|
|
555
|
+
const stsToken = getStringValue(candidate, [
|
|
556
|
+
"stsToken",
|
|
557
|
+
"securityToken",
|
|
558
|
+
"SecurityToken"
|
|
559
|
+
]);
|
|
560
|
+
const expiration = getStringValue(candidate, ["expiration", "Expiration"]);
|
|
561
|
+
const bucket = getStringValue(candidate, [
|
|
562
|
+
"bucket",
|
|
563
|
+
"bucketName",
|
|
564
|
+
"Bucket",
|
|
565
|
+
"BucketName"
|
|
566
|
+
]);
|
|
567
|
+
const endpoint = getStringValue(candidate, ["endpoint", "Endpoint"]);
|
|
568
|
+
const region = getStringValue(candidate, ["region", "Region"]) ?? getRegionFromEndpoint(endpoint);
|
|
569
|
+
const uploadPrefix = getStringValue(candidate, [
|
|
570
|
+
"uploadPrefix",
|
|
571
|
+
"prefix",
|
|
572
|
+
"Prefix"
|
|
573
|
+
]);
|
|
574
|
+
if (accessKeyId && accessKeySecret && stsToken) return {
|
|
575
|
+
accessKeyId,
|
|
576
|
+
accessKeySecret,
|
|
577
|
+
bucket,
|
|
578
|
+
endpoint,
|
|
579
|
+
expiration,
|
|
580
|
+
region,
|
|
581
|
+
stsToken,
|
|
582
|
+
uploadPrefix
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
throw new Error("STS 响应缺少 accessKeyId/accessKeySecret/stsToken,请配置 beforeStsTokenRes");
|
|
586
|
+
}
|
|
587
|
+
function getRegionFromEndpoint(endpoint) {
|
|
588
|
+
if (!endpoint) return;
|
|
589
|
+
return getEndpointHostname(endpoint).match(/^(?:[^.]+--)?(oss-[a-z0-9-]+)\./)?.[1];
|
|
590
|
+
}
|
|
591
|
+
function getEndpointHostname(endpoint) {
|
|
592
|
+
try {
|
|
593
|
+
return new URL(endpoint).hostname;
|
|
594
|
+
} catch {
|
|
595
|
+
return endpoint.replace(/^https?:\/\//, "").split("/")[0] ?? endpoint;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function buildUrl(url, query = {}) {
|
|
599
|
+
const baseUrl = typeof window !== "undefined" && window.location?.origin ? window.location.origin : "http://localhost";
|
|
600
|
+
const targetUrl = new URL(url, baseUrl);
|
|
601
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
602
|
+
if (typeof value === "undefined" || value === null) return;
|
|
603
|
+
targetUrl.searchParams.set(key, String(value));
|
|
604
|
+
});
|
|
605
|
+
return targetUrl.toString();
|
|
606
|
+
}
|
|
607
|
+
function getObjectValue(source, key) {
|
|
608
|
+
if (!isRecord(source)) return;
|
|
609
|
+
return source[key];
|
|
610
|
+
}
|
|
611
|
+
function getStringValue(source, keys) {
|
|
612
|
+
for (const key of keys) {
|
|
613
|
+
const value = source[key];
|
|
614
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function isRecord(value) {
|
|
618
|
+
return typeof value === "object" && value !== null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/oss/adapter.ts
|
|
623
|
+
function createOssAdapter(options) {
|
|
624
|
+
const runtime = /* @__PURE__ */ new Map();
|
|
625
|
+
return {
|
|
626
|
+
name: "oss",
|
|
627
|
+
cancel(context) {
|
|
628
|
+
return cancelUpload(context, runtime);
|
|
629
|
+
},
|
|
630
|
+
pause(context) {
|
|
631
|
+
runtime.get(context.file.id)?.cancel?.();
|
|
632
|
+
return Promise.resolve();
|
|
633
|
+
},
|
|
634
|
+
resume() {
|
|
635
|
+
return Promise.resolve();
|
|
636
|
+
},
|
|
637
|
+
upload(context) {
|
|
638
|
+
const token = context.prepared.token;
|
|
639
|
+
if (!token) return Promise.reject(/* @__PURE__ */ new Error("OSS 上传缺少 STS Token"));
|
|
640
|
+
return loadOssSdk(options).then((OssClient) => {
|
|
641
|
+
const client = new OssClient(buildOssClientOptions(options, token, getRefreshStsToken(context)));
|
|
642
|
+
const runtimeItem = {
|
|
643
|
+
checkpoint: context.file.checkpoint,
|
|
644
|
+
client
|
|
645
|
+
};
|
|
646
|
+
runtime.set(context.file.id, runtimeItem);
|
|
647
|
+
return runMultipartUpload(context, options, runtimeItem).finally(() => {
|
|
648
|
+
runtime.delete(context.file.id);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function getRefreshStsToken(context) {
|
|
655
|
+
const refreshSTSToken = context.prepared.extra?.refreshSTSToken;
|
|
656
|
+
return typeof refreshSTSToken === "function" ? refreshSTSToken : void 0;
|
|
657
|
+
}
|
|
658
|
+
function runMultipartUpload(context, options, runtime) {
|
|
659
|
+
const params = context.prepared.params;
|
|
660
|
+
const uploadOptions = {
|
|
661
|
+
callback: params.callback,
|
|
662
|
+
checkpoint: runtime.checkpoint,
|
|
663
|
+
headers: params.headers,
|
|
664
|
+
meta: params.meta,
|
|
665
|
+
mime: params.mime,
|
|
666
|
+
parallel: params.parallel ?? options.parallel,
|
|
667
|
+
partSize: params.partSize ?? options.partSize,
|
|
668
|
+
progress(progress, checkpoint, raw) {
|
|
669
|
+
runtime.checkpoint = checkpoint;
|
|
670
|
+
context.onProgress({
|
|
671
|
+
checkpoint,
|
|
672
|
+
loaded: Math.round((context.file.size || 0) * progress),
|
|
673
|
+
percent: Math.round(progress * 1e4) / 100,
|
|
674
|
+
raw,
|
|
675
|
+
total: context.file.size
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
if (typeof AbortController !== "undefined") context.signal.addEventListener("abort", () => {
|
|
680
|
+
runtime.cancel?.();
|
|
681
|
+
});
|
|
682
|
+
const uploadPromise = runtime.client.multipartUpload(params.key, params.file, uploadOptions);
|
|
683
|
+
if (uploadPromise && typeof uploadPromise === "object" && "cancel" in uploadPromise) runtime.cancel = () => {
|
|
684
|
+
const cancel = uploadPromise.cancel;
|
|
685
|
+
if (typeof cancel === "function") cancel.call(uploadPromise);
|
|
686
|
+
};
|
|
687
|
+
return Promise.resolve(uploadPromise).then((rawResponse) => ({
|
|
688
|
+
checkpoint: runtime.checkpoint,
|
|
689
|
+
key: params.key,
|
|
690
|
+
rawResponse,
|
|
691
|
+
url: getResponseUrl(rawResponse)
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
function cancelUpload(context, runtime) {
|
|
695
|
+
const runtimeItem = runtime.get(context.file.id);
|
|
696
|
+
const checkpoint = runtimeItem?.checkpoint ?? context.file.checkpoint;
|
|
697
|
+
runtimeItem?.cancel?.();
|
|
698
|
+
if (!runtimeItem?.client || !checkpoint?.name || !checkpoint.uploadId) return Promise.resolve();
|
|
699
|
+
return Promise.resolve(runtimeItem.client.abortMultipartUpload(checkpoint.name, checkpoint.uploadId)).then(() => void 0);
|
|
700
|
+
}
|
|
701
|
+
function getResponseUrl(response) {
|
|
702
|
+
if (response && typeof response === "object" && "url" in response && typeof response.url === "string") return response.url;
|
|
703
|
+
if (response && typeof response === "object" && "res" in response && response.res && typeof response.res === "object" && "requestUrls" in response.res && Array.isArray(response.res.requestUrls)) return response.res.requestUrls[0];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/oss/provider.ts
|
|
708
|
+
function createOssProvider(options = {}) {
|
|
709
|
+
return {
|
|
710
|
+
adapter: createOssAdapter(options),
|
|
711
|
+
name: "oss",
|
|
712
|
+
prepare(file) {
|
|
713
|
+
return requestAndTransformStsToken({
|
|
714
|
+
file,
|
|
715
|
+
lifecycle: "sts"
|
|
716
|
+
}, options).then((token) => buildUploadParams(file, token, options));
|
|
717
|
+
},
|
|
718
|
+
transformLifecycleRequest(request, context) {
|
|
719
|
+
return Promise.resolve(options.transformLifecycleRequest?.(request, context) ?? request);
|
|
720
|
+
},
|
|
721
|
+
response(response, context) {
|
|
722
|
+
return Promise.resolve(options.response?.(response, context) ?? response);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function buildUploadParams(file, token, options) {
|
|
727
|
+
const context = {
|
|
728
|
+
file,
|
|
729
|
+
lifecycle: "upload",
|
|
730
|
+
token
|
|
731
|
+
};
|
|
732
|
+
return Promise.resolve(options.keyResolver?.(file, context) ?? defaultKeyResolver(file, token, options)).then((key) => {
|
|
733
|
+
const refreshSTSToken = createRefreshStsToken({
|
|
734
|
+
file,
|
|
735
|
+
lifecycle: "sts"
|
|
736
|
+
}, options);
|
|
737
|
+
const params = {
|
|
738
|
+
callback: options.callback,
|
|
739
|
+
extra: {
|
|
740
|
+
...options.extra ?? {},
|
|
741
|
+
...refreshSTSToken ? { refreshSTSToken } : {},
|
|
742
|
+
token
|
|
743
|
+
},
|
|
744
|
+
file: file.file,
|
|
745
|
+
headers: {},
|
|
746
|
+
key,
|
|
747
|
+
meta: {},
|
|
748
|
+
mime: options.mime ?? file.type,
|
|
749
|
+
parallel: options.parallel,
|
|
750
|
+
partSize: options.partSize
|
|
751
|
+
};
|
|
752
|
+
const uploadContext = {
|
|
753
|
+
file,
|
|
754
|
+
lifecycle: "upload",
|
|
755
|
+
params,
|
|
756
|
+
token
|
|
757
|
+
};
|
|
758
|
+
return Promise.resolve(options.beforeUploadReq?.(params, uploadContext) ?? params).then((nextParams) => ({
|
|
759
|
+
extra: {
|
|
760
|
+
...refreshSTSToken ? { refreshSTSToken } : {},
|
|
761
|
+
token
|
|
762
|
+
},
|
|
763
|
+
params: nextParams,
|
|
764
|
+
token
|
|
765
|
+
}));
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function defaultKeyResolver(file, token, options) {
|
|
769
|
+
const safeName = file.name.replace(/^\.+/, "").replace(/[^\w.\\/=-]+/g, "-");
|
|
770
|
+
return `${options.uploadPrefix ? normalizePrefix(options.uploadPrefix) : token.uploadPrefix ? normalizePrefix(token.uploadPrefix) : "uploads/"}${Date.now()}-${safeName}`;
|
|
771
|
+
}
|
|
772
|
+
function normalizePrefix(prefix) {
|
|
773
|
+
if (!prefix) return "";
|
|
774
|
+
return prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/oss/client.ts
|
|
779
|
+
function createOssClient(options = {}) {
|
|
780
|
+
const context = { lifecycle: "sts" };
|
|
781
|
+
return requestAndTransformStsToken(context, options).then((token) => {
|
|
782
|
+
const refreshSTSToken = createRefreshStsToken(context, options);
|
|
783
|
+
return loadOssSdk(options).then((OssClient) => {
|
|
784
|
+
return new OssClient(buildOssClientOptions(options, token, refreshSTSToken));
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/uploader.ts
|
|
791
|
+
let defaultOssProviderOptions = {};
|
|
792
|
+
function createOssUploader(options = {}) {
|
|
793
|
+
return createUploader({
|
|
794
|
+
...options,
|
|
795
|
+
provider: options.provider ?? createOssProvider(defaultOssProviderOptions)
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function setDefaultOssProviderOptions(options) {
|
|
799
|
+
defaultOssProviderOptions = options;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
exports.DEFAULT_OSS_STS_URL = DEFAULT_OSS_STS_URL;
|
|
804
|
+
exports.createOssAdapter = createOssAdapter;
|
|
805
|
+
exports.createOssClient = createOssClient;
|
|
806
|
+
exports.createOssProvider = createOssProvider;
|
|
807
|
+
exports.createOssUploader = createOssUploader;
|
|
808
|
+
exports.getOssRuntimeConfig = getOssRuntimeConfig;
|
|
809
|
+
exports.setDefaultOssProviderOptions = setDefaultOssProviderOptions;
|
|
810
|
+
return exports;
|
|
811
|
+
})({});
|
|
812
|
+
//# sourceMappingURL=nec-oss.iife.js.map
|