mirror of
https://github.com/rustfs/console.git
synced 2026-08-30 17:14:47 +08:00
feat: pause and resume
This commit is contained in:
+94
-31
@@ -4,6 +4,7 @@ import {
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
UploadPartCommand,
|
||||
ListPartsCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
|
||||
export interface UploadTask {
|
||||
@@ -11,11 +12,14 @@ export interface UploadTask {
|
||||
file: File;
|
||||
bucketName: string;
|
||||
prefix?: string;
|
||||
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'canceled';
|
||||
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'canceled' | 'paused';
|
||||
progress: number;
|
||||
error?: string;
|
||||
abortController?: AbortController;
|
||||
retryCount?: number;
|
||||
uploadId?: string;
|
||||
completedParts?: { ETag: string; PartNumber: number }[];
|
||||
_pauseRequested?: boolean;
|
||||
}
|
||||
|
||||
export interface TaskManagerConfig {
|
||||
@@ -96,6 +100,7 @@ class UploadTaskManager {
|
||||
cancelTask(taskId: string) {
|
||||
const task = this.tasks.find(task => task.id === taskId);
|
||||
if (task) {
|
||||
task._pauseRequested = false;
|
||||
if (task.abortController) {
|
||||
task.abortController.abort();
|
||||
}
|
||||
@@ -129,6 +134,58 @@ class UploadTaskManager {
|
||||
this.isStarted = false;
|
||||
}
|
||||
|
||||
pauseTask(taskId: string) {
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (task && task.status === 'uploading') {
|
||||
task._pauseRequested = true;
|
||||
task.abortController?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async resumeTask(taskId: string) {
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (task && task.status === 'paused') {
|
||||
task._pauseRequested = false; // 恢复前清除暂停标记
|
||||
// 1. 校验 uploadId 是否有效
|
||||
try {
|
||||
if (task.uploadId) {
|
||||
const listParts = await this.s3Client.send(
|
||||
new ListPartsCommand({
|
||||
Bucket: task.bucketName,
|
||||
Key: (task.prefix || '') + task.file.name,
|
||||
UploadId: task.uploadId,
|
||||
})
|
||||
);
|
||||
|
||||
// 2. 以 S3 返回的分片为准,更新 task.completedParts
|
||||
task.completedParts = (listParts.Parts || []).map((p: any) => ({
|
||||
ETag: p.ETag,
|
||||
PartNumber: p.PartNumber,
|
||||
}));
|
||||
|
||||
// 3. 开始断点续传
|
||||
this.startUpload(task);
|
||||
} else {
|
||||
// 没有 uploadId,直接重新上传
|
||||
task.completedParts = [];
|
||||
this.startUpload(task);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.name === 'NoSuchUpload' ||
|
||||
(typeof e.message === 'string' && e.message.includes('Invalid upload id'))
|
||||
) {
|
||||
// uploadId 已失效,重新发起分片上传
|
||||
task.uploadId = undefined;
|
||||
task.completedParts = [];
|
||||
this.startUpload(task);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的队列处理逻辑
|
||||
private async processQueue() {
|
||||
if (!this.isStarted) {
|
||||
@@ -169,8 +226,17 @@ class UploadTaskManager {
|
||||
task.status = 'completed';
|
||||
task.progress = 100;
|
||||
} catch (error: any) {
|
||||
// 检查是否是取消操作
|
||||
// 判断是否为“暂停”操作
|
||||
if (
|
||||
task._pauseRequested &&
|
||||
(error.message === 'Upload paused' || error.message === 'Request aborted')
|
||||
) {
|
||||
task.status = 'paused';
|
||||
return;
|
||||
}
|
||||
// 只有真正取消时才清理
|
||||
if (error.name === 'AbortError' || error.message?.includes('canceled')) {
|
||||
// 这里才调用 AbortMultipartUploadCommand
|
||||
task.status = 'canceled';
|
||||
return;
|
||||
}
|
||||
@@ -262,39 +328,38 @@ class UploadTaskManager {
|
||||
task.abortController = abortController;
|
||||
|
||||
const Key = prefix + file.name;
|
||||
let uploadId: string | undefined;
|
||||
let uploadId: string | undefined = task.uploadId;
|
||||
let completedParts: { ETag: string; PartNumber: number }[] = task.completedParts || [];
|
||||
|
||||
try {
|
||||
// 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
ContentType: file.type || 'application/octet-stream',
|
||||
});
|
||||
|
||||
const createResponse = await this.s3Client.send(createCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
uploadId = createResponse.UploadId;
|
||||
|
||||
if (!uploadId) {
|
||||
// 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
ContentType: file.type || 'application/octet-stream',
|
||||
});
|
||||
const createResponse = await this.s3Client.send(createCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
uploadId = createResponse.UploadId;
|
||||
task.uploadId = uploadId;
|
||||
task.completedParts = [];
|
||||
}
|
||||
if (!uploadId) {
|
||||
throw new Error('Failed to create multipart upload');
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
||||
const completedParts = [];
|
||||
|
||||
// 上传分片
|
||||
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
|
||||
// 检查是否被取消
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Upload canceled');
|
||||
throw new Error('Upload paused');
|
||||
}
|
||||
|
||||
// 跳过已完成分片
|
||||
if (completedParts.some(p => p.PartNumber === partNumber)) continue;
|
||||
const start = (partNumber - 1) * this.chunkSize;
|
||||
const end = Math.min(start + this.chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
@@ -302,19 +367,16 @@ class UploadTaskManager {
|
||||
PartNumber: partNumber,
|
||||
Body: chunk,
|
||||
});
|
||||
|
||||
const { ETag } = await this.s3Client.send(uploadPartCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!ETag) {
|
||||
throw new Error(`Failed to upload part ${partNumber}`);
|
||||
}
|
||||
|
||||
completedParts.push({ ETag, PartNumber: partNumber });
|
||||
task.progress = Math.round((partNumber / totalChunks) * 100);
|
||||
task.completedParts = completedParts;
|
||||
task.progress = Math.round((completedParts.length / totalChunks) * 100);
|
||||
}
|
||||
|
||||
// 完成分片上传
|
||||
const completeCommand = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
@@ -322,13 +384,15 @@ class UploadTaskManager {
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: { Parts: completedParts },
|
||||
});
|
||||
|
||||
await this.s3Client.send(completeCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
// 上传完成后清理 uploadId 和 completedParts
|
||||
task.uploadId = undefined;
|
||||
task.completedParts = undefined;
|
||||
} catch (error: any) {
|
||||
// 如果上传失败,尝试清理分片上传
|
||||
if (uploadId) {
|
||||
// 只在“非暂停”情况下才调用 AbortMultipartUploadCommand
|
||||
if (!task._pauseRequested) {
|
||||
try {
|
||||
const { AbortMultipartUploadCommand } = await import('@aws-sdk/client-s3');
|
||||
const abortCommand = new AbortMultipartUploadCommand({
|
||||
@@ -341,7 +405,6 @@ class UploadTaskManager {
|
||||
console.warn('Failed to cleanup multipart upload:', cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user