oak-frontend-base/src/utils/upload.web.ts

193 lines
6.8 KiB
TypeScript
Raw 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 {
private controllers: Map<string, AbortController> = 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(
file: File | string,
name: string,
uploadUrl: string,
formData: Record<string, any>,
autoInform?: boolean,
getPercent?: Function,
uploadId?: string, // 新增上传任务ID用于中断特定上传
method: "POST" | "PUT" | "PATCH" = "POST",
): Promise<any> {
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); // 清理控制器
if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) {
resolve({ status: 204 });
} else {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch {
resolve({ status: xhr.status, raw: xhr.responseText });
}
}
} 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");
}
xhr.send(file as any);
} else {
// POST / PATCH 模式:构建表单
const formData2 = new FormData();
Object.entries(formData).forEach(([key, value]) => {
formData2.append(key, value);
});
formData2.append(name || "file", file as File);
xhr.send(formData2);
}
});
}
// 无进度监听模式(直接 fetch
try {
let result: Response;
if (isPut) {
// S3 预签名上传
const headers: Record<string, string> = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file as any,
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 as 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') {
return {
status: 204,
};
}
throw error;
}
}
// 中断特定上传
abortUpload(uploadId: string): boolean {
const controller = this.controllers.get(uploadId);
if (controller) {
controller.abort();
this.controllers.delete(uploadId);
return true;
}
return false;
}
// 中断所有上传
abortAllUploads(): void {
this.controllers.forEach((controller, id) => {
controller.abort();
});
this.controllers.clear();
}
// 获取上传状态
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' {
const controller = this.controllers.get(uploadId);
if (!controller) return 'not-found';
if (controller.signal.aborted) return 'aborted';
return 'uploading';
}
// 生成唯一的上传ID
private generateUploadId(file: File | string, uploadUrl: string): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
const fileInfo = file instanceof File ? `${file.name}-${file.size}` : String(file);
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads(): string[] {
return Array.from(this.controllers.keys());
}
}