194 lines
7.8 KiB
JavaScript
194 lines
7.8 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.Upload = void 0;
|
||
class Upload {
|
||
controllers = new Map();
|
||
constructor() {
|
||
this.uploadFile = this.uploadFile.bind(this);
|
||
this.abortUpload = this.abortUpload.bind(this);
|
||
this.abortAllUploads = this.abortAllUploads.bind(this);
|
||
this.getUploadStatus = this.getUploadStatus.bind(this);
|
||
this.getActiveUploads = this.getActiveUploads.bind(this);
|
||
}
|
||
async uploadFile(options) {
|
||
const { file, name, uploadUrl, formData, getPercent, uploadId, method = "POST" } = options;
|
||
const isPut = method === "PUT";
|
||
const id = uploadId || this.generateUploadId(file, uploadUrl);
|
||
// 如果已有相同ID的上传在进行,先中断它
|
||
if (this.controllers.has(id)) {
|
||
this.abortUpload(id);
|
||
}
|
||
// 创建新的 AbortController
|
||
const controller = new AbortController();
|
||
this.controllers.set(id, controller);
|
||
// 进度监听模式
|
||
if (getPercent) {
|
||
return new Promise((resolve, reject) => {
|
||
const xhr = new XMLHttpRequest();
|
||
let percent = 0;
|
||
// 监听中止信号
|
||
controller.signal.addEventListener('abort', () => {
|
||
xhr.abort();
|
||
reject(new DOMException('Upload aborted', 'AbortError'));
|
||
});
|
||
xhr.upload.addEventListener("progress", (event) => {
|
||
if (event.lengthComputable) {
|
||
percent = Math.round((event.loaded / event.total) * 100);
|
||
getPercent(percent);
|
||
}
|
||
});
|
||
xhr.onload = () => {
|
||
this.controllers.delete(id); // 清理控制器
|
||
// 构造类似 Response 的对象,支持 headers.get()
|
||
const headersMap = new Map();
|
||
const headersStr = xhr.getAllResponseHeaders();
|
||
headersStr.split('\r\n').forEach(line => {
|
||
const parts = line.split(': ');
|
||
if (parts.length === 2) {
|
||
headersMap.set(parts[0].toLowerCase(), parts[1]);
|
||
}
|
||
});
|
||
const headers = {
|
||
get: (name) => headersMap.get(name.toLowerCase()) || null,
|
||
has: (name) => headersMap.has(name.toLowerCase()),
|
||
forEach: (callback) => {
|
||
headersMap.forEach(callback);
|
||
}
|
||
};
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
if (xhr.status === 204) {
|
||
resolve({ status: 204, headers });
|
||
}
|
||
else {
|
||
try {
|
||
const data = JSON.parse(xhr.responseText);
|
||
resolve({ ...data, status: xhr.status, headers });
|
||
}
|
||
catch {
|
||
resolve({ status: xhr.status, raw: xhr.responseText, headers });
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
reject(new Error(`HTTP Error: ${xhr.status}`));
|
||
}
|
||
};
|
||
xhr.onerror = () => {
|
||
this.controllers.delete(id);
|
||
// 如果不是因为中止导致的错误
|
||
if (!controller.signal.aborted) {
|
||
reject(new Error("Network Error"));
|
||
}
|
||
};
|
||
xhr.onabort = () => {
|
||
this.controllers.delete(id);
|
||
if (controller.signal.aborted) {
|
||
reject(new DOMException('Upload aborted', 'AbortError'));
|
||
}
|
||
};
|
||
xhr.open(method, uploadUrl);
|
||
if (isPut) {
|
||
// PUT 模式:直接上传文件
|
||
if (file instanceof File) {
|
||
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
||
}
|
||
else if (file instanceof Blob) {
|
||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||
}
|
||
xhr.send(file);
|
||
}
|
||
else {
|
||
// POST / PATCH 模式:构建表单
|
||
const formData2 = new FormData();
|
||
Object.entries(formData).forEach(([key, value]) => {
|
||
formData2.append(key, value);
|
||
});
|
||
formData2.append(name || "file", file);
|
||
xhr.send(formData2);
|
||
}
|
||
});
|
||
}
|
||
// 无进度监听模式(直接 fetch)
|
||
try {
|
||
let result;
|
||
if (isPut) {
|
||
// S3 预签名上传
|
||
const headers = {};
|
||
if (file instanceof File) {
|
||
headers["Content-Type"] = file.type || "application/octet-stream";
|
||
}
|
||
else if (file instanceof Blob) {
|
||
headers["Content-Type"] = "application/octet-stream";
|
||
}
|
||
result = await fetch(uploadUrl, {
|
||
method: "PUT",
|
||
headers,
|
||
body: file,
|
||
signal: controller.signal, // 添加中止信号
|
||
});
|
||
}
|
||
else {
|
||
// 表单上传
|
||
const formData2 = new FormData();
|
||
for (const key of Object.keys(formData)) {
|
||
formData2.append(key, formData[key]);
|
||
}
|
||
formData2.append(name || "file", file);
|
||
result = await fetch(uploadUrl, {
|
||
method,
|
||
body: formData2,
|
||
signal: controller.signal, // 添加中止信号
|
||
});
|
||
}
|
||
this.controllers.delete(id); // 成功后清理控制器
|
||
return result;
|
||
}
|
||
catch (error) {
|
||
this.controllers.delete(id); // 失败后清理控制器
|
||
// 人为中断返回204 使general-business处理成功
|
||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||
throw new DOMException('Upload aborted', 'AbortError');
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
// 中断特定上传
|
||
abortUpload(uploadId) {
|
||
const controller = this.controllers.get(uploadId);
|
||
if (controller) {
|
||
controller.abort();
|
||
this.controllers.delete(uploadId);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
// 中断所有上传
|
||
abortAllUploads() {
|
||
this.controllers.forEach((controller, id) => {
|
||
controller.abort();
|
||
});
|
||
this.controllers.clear();
|
||
}
|
||
// 获取上传状态
|
||
getUploadStatus(uploadId) {
|
||
const controller = this.controllers.get(uploadId);
|
||
if (!controller)
|
||
return 'not-found';
|
||
if (controller.signal.aborted)
|
||
return 'aborted';
|
||
return 'uploading';
|
||
}
|
||
// 生成唯一的上传ID
|
||
generateUploadId(file, uploadUrl) {
|
||
const timestamp = Date.now();
|
||
const random = Math.random().toString(36).substring(2, 9);
|
||
const fileInfo = file instanceof File ? `${file.name}-${file.size}` : file instanceof Blob ? `blob-${file.size}` : file;
|
||
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
|
||
}
|
||
// 获取所有进行中的上传任务
|
||
getActiveUploads() {
|
||
return Array.from(this.controllers.keys());
|
||
}
|
||
}
|
||
exports.Upload = Upload;
|