extraFile实现中断文件上传

This commit is contained in:
xzf 2025-10-21 11:29:06 +08:00
parent 45a50ed6cc
commit 9d71fd9c30
5 changed files with 363 additions and 75 deletions

View File

@ -1,3 +1,11 @@
export declare class Upload {
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>;
private controllers;
constructor();
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH"): Promise<any>;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
}

View File

@ -1,11 +1,33 @@
export class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") {
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(file, name, uploadUrl, formData, autoInform, getPercent, uploadId, // 新增上传任务ID用于中断特定上传
method = "POST") {
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);
@ -13,6 +35,7 @@ export class Upload {
}
});
xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) {
resolve({ status: 204 });
@ -31,7 +54,19 @@ export class Upload {
reject(new Error(`HTTP Error: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Network Error"));
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 模式:直接上传文件
@ -52,31 +87,83 @@ export class Upload {
});
}
// 无进度监听模式(直接 fetch
if (isPut) {
// S3 预签名上传
const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
try {
let result;
if (isPut) {
// S3 预签名上传
const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
signal: controller.signal, // 添加中止信号
});
}
const result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
});
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;
}
else {
// 表单上传
const formData2 = new FormData();
for (const key of Object.keys(formData)) {
formData2.append(key, formData[key]);
catch (error) {
this.controllers.delete(id); // 失败后清理控制器
// 人为中断返回204 使general-business处理成功
if (error instanceof DOMException && error.name === 'AbortError') {
return {
status: 204,
};
}
formData2.append(name || "file", file);
const result = await fetch(uploadUrl, {
method,
body: formData2,
});
return result;
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}` : String(file);
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return Array.from(this.controllers.keys());
}
}

View File

@ -1,3 +1,11 @@
export declare class Upload {
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>;
private controllers;
constructor();
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH"): Promise<any>;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
}

View File

@ -2,13 +2,35 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.Upload = void 0;
class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") {
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(file, name, uploadUrl, formData, autoInform, getPercent, uploadId, // 新增上传任务ID用于中断特定上传
method = "POST") {
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);
@ -16,6 +38,7 @@ class Upload {
}
});
xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) {
resolve({ status: 204 });
@ -34,7 +57,19 @@ class Upload {
reject(new Error(`HTTP Error: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Network Error"));
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 模式:直接上传文件
@ -55,32 +90,84 @@ class Upload {
});
}
// 无进度监听模式(直接 fetch
if (isPut) {
// S3 预签名上传
const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
try {
let result;
if (isPut) {
// S3 预签名上传
const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
signal: controller.signal, // 添加中止信号
});
}
const result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
});
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;
}
else {
// 表单上传
const formData2 = new FormData();
for (const key of Object.keys(formData)) {
formData2.append(key, formData[key]);
catch (error) {
this.controllers.delete(id); // 失败后清理控制器
// 人为中断返回204 使general-business处理成功
if (error instanceof DOMException && error.name === 'AbortError') {
return {
status: 204,
};
}
formData2.append(name || "file", file);
const result = await fetch(uploadUrl, {
method,
body: formData2,
});
return result;
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}` : String(file);
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return Array.from(this.controllers.keys());
}
}
exports.Upload = Upload;

View File

@ -1,6 +1,14 @@
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,
@ -8,9 +16,20 @@ export class Upload {
formData: Record<string, any>,
autoInform?: boolean,
getPercent?: Function,
method: "POST" | "PUT" | "PATCH" = "POST"
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) {
@ -18,6 +37,12 @@ export class Upload {
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);
@ -26,6 +51,7 @@ export class Upload {
});
xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) {
resolve({ status: 204 });
@ -42,7 +68,21 @@ export class Upload {
}
};
xhr.onerror = () => reject(new Error("Network Error"));
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) {
@ -64,32 +104,90 @@ export class Upload {
}
// 无进度监听模式(直接 fetch
if (isPut) {
// S3 预签名上传
const headers: Record<string, string> = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
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, // 添加中止信号
});
}
const result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file as any,
});
this.controllers.delete(id); // 成功后清理控制器
return result;
} else {
// 表单上传
const formData2 = new FormData();
for (const key of Object.keys(formData)) {
formData2.append(key, formData[key]);
}
formData2.append(name || "file", file as File);
const result = await fetch(uploadUrl, {
method,
body: formData2,
});
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());
}
}