oak-frontend-base/es/utils/upload.web.js

190 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export 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());
}
}