import Embitter from "../tools/emmitter";
import Slice from "./sliceUpload";
// 上传状态
const statusMap = {
fail: 0, // 失败
success: 1, // 胜利
inProcessing: 2, // 进行中
paused: 3, // 暂停
canceled: 4, // 已勾销
wait: 5 // 未开始
};
interface IConfig {
file: File;
parallel: number;
partSize: number;
}
class Client extends Embitter {
private file: File;
private parallel: number; // 分片并行数量
private partSize: number; // 分片大小
private status: number; // 0 失败 1 胜利 2 进行中 3 暂停 4 勾销 5 未开始 6 报错
private partInfo: IPartInfo;
private partsArray: Slice[]; // 保护所有分片
private sliceUploadSuccess: Set<number>; // 上传胜利的分片 partIndex
private sliceUploadRunning: Set<number>; // 上传中的分片 partIndex
private sliceEvent: Embitter;
constructor(config: IConfig) {super();
this.file = config.file;
this.parallel = config.parallel || defaultParallel;
this.partInfo = {
partSize: config.partSize,
partNum: Math.ceil(config.file.size / config.partSize),
};
this.status = statusMap.wait; // 默认未开始
this.partsArray = [];
// 记录胜利的分片上传
this.sliceUploadSuccess = new Set([]);
// 记录上传中的分片
this.sliceUploadRunning = new Set([]);
// 监听分片上传
this.sliceEvent = new Embitter();}
getConfig() {
return {
file: this.file,
partInfo: this.partInfo,
sliceEvent: this.sliceEvent,
status: this.status,
};
}
private startUploadAllSlice() {this.sliceEvent.on("success", (partIndex: number) => {if (!this.sliceUploadSuccess.has(partIndex)) {this.sliceUploadSuccess.add(partIndex);
this.sliceUploadRunning.delete(partIndex);
const progress = this.sliceUploadSuccess.size / this.partInfo.partNum;
this.emmit("progress", progress);
if (progress >= 1) {this.endSliceUpload();
} else {this._resumeUpload();
}
}
});
this.sliceEvent.on("error", (partIndex: number, err: any) => {this.emmit("error", err);
this.sliceUploadRunning.delete(partIndex);
this.status = statusMap.paused; // 上传出现异常就先暂停上传
this.emmit("pause");
});
const that = this;
// 创立分片上传实例
this.partsArray = Array.from(new Array(this.partInfo.partNum),
(x, i) =>
new Slice({
client: that,
partIndex: i + 1,
})
);
// 开始分片上传
this._resumeUpload();}
/**
* 上传文件:开始上传或者复原上传
* @param file
* @returns {Promise<void>}
*/
private async uploadFile(): Promise<void> {if (this.status !== statusMap.inProcessing) {
this.status = statusMap.inProcessing;
this.emmit("start");
}
// 开始上传
this.startUploadAllSlice();}
/**
* 开始上传
* @param file
* @returns {Promise<void>}
*/
public async startUpload(): Promise<void> {if (this.status !== statusMap.wait) {console.log("startUpload status error", this.status);
this.emmit("error", errorMap.hasStarted);
return;
}
this.uploadFile();}
// 完结分片上传事务
private async endSliceUpload() {if (statusMap.canceled === this.status) {return;}
const tagList = [];
for (const val of this.partsArray) {
tagList.push({
partIndex: val.partIndex,
partTag: val.partTag,
});
}
// 完结分片上传事务
const res = await action.endMultipartUpload({tagList});
if (!res.hasOwnProperty("err")) {
this.status = statusMap.success;
this.emmit("success", {
resourceId: res.resourceId,
preResourceId: res.preResourceId,
url: res.path,
});
}
}
/**
* 暂停上传
* @returns {Promise<void>}
*/
public async pauseUpload() {
// 只有文件正在上传中,才能够暂停
if (this.status !== statusMap.inProcessing) {this.emmit("error", errorMap.noInProcess);
return;
}
this.status = statusMap.paused;
this.emmit("pause");
}
/**
* 分片上传开始
* @returns
*/
private _resumeUpload() {
const todoParts = this.partsArray.filter((part) =>
!this.sliceUploadSuccess.has(part.partIndex) &&
!this.sliceUploadRunning.has(part.partIndex)
);
let job;
while (
this.sliceUploadRunning.size < this.parallel &&
![statusMap.paused, statusMap.canceled].includes(this.status) &&
(job = todoParts.shift())
) {this.sliceUploadRunning.add(job.partIndex);
job.startUpload();}
}
/**
* 复原上传
* @returns {Promise<void>}
*/
public async resumeUpload() {if (this.status !== statusMap.paused) {this.emmit("error", errorMap.noCanResume);
return;
}
this.status = statusMap.inProcessing;
this.emmit("start");
this._resumeUpload();}
/**
* 破除上传
* @returns {Promise<void>}
*/
public async abortUpload(): Promise<void> {
// 只有文件正在上传中或者上传曾经暂停,才能够勾销上传
if (
this.status !== statusMap.inProcessing &&
this.status !== statusMap.paused
) {this.emmit("error", errorMap.noCanCancel);
return;
}
const result = await action.abortUpload();
// 勾销上传胜利
if (!result.err) {
this.status = statusMap.canceled;
this.emmit("abort");
this.emmit("progress", 0);
return;
}
this.emmit("error", result.err);
}
}
export default Client;
import {generateMD5} from "../tools/utils";
import action from "../action";
import Client from "./index";
class Slice {
// 以下是独有属性
private sliceFile: Blob;
private md5Content: string;
private signatureUrl: string;
private _status: number; // 0 失败 1 胜利 2 进行中 3 暂停 4 勾销 5 未开始 6 报错
private _partTag: string; // 上传胜利会有标签
private _partIndex: number;
private client: Client; // 上传实例
constructor(config: ISliceConfig) {
this.client = config.client;
this._partIndex = config.partIndex;
this._status = statusMap.wait;
this._partTag = "";
}
get partTag() {return this._partTag;}
get status() {return this._status;}
get partIndex() {return this._partIndex;}
/**
* 获取分片签名上传 url
*/
private async getSignatureUrl() {const { file, sliceEvent} = this.client.getConfig();
const res = await action.getMultipartUrl(this._partIndex, {
headers: {
"FileContent-MD5": this.md5Content,
"FileContent-Type": file.type || "application/octet-stream",
},
});
if (!res.err) {this.signatureUrl = res.signatureUrl;} else {if (this.getCurrentStatus() === statusMap.canceled) {return;}
this._status = statusMap.fail;
sliceEvent.emmit("error", this.partIndex, res.err);
}
}
private generateSliceFile() {const { partInfo, file} = this.client.getConfig();
const start = (this._partIndex - 1) * partInfo.partSize;
let end = start + partInfo.partSize;
if (this._partIndex === partInfo.partNum) {end = file.size;}
this.sliceFile = file.slice(start, end);
}
private isContinueUpload() {const status = this.getCurrentStatus();
return ![statusMap.canceled, statusMap.paused].includes(status);
}
private getCurrentStatus() {const { status} = this.client.getConfig();
return status;
}
private async uploadSlice() {const { file, sliceEvent} = this.client.getConfig();
const res = await action.startPartUpload(
this.signatureUrl,
this.sliceFile,
{
headers: {
"Content-MD5": this.md5Content,
"Content-Type": file.type || "application/octet-stream",
},
}
);
if (!res.err) {
this._status = statusMap.success;
try {this._partTag = JSON.parse(res.headers.etag);
} catch (error) {this._partTag = res.headers.etag;}
sliceEvent.emmit("success", this._partIndex);
} else {if (this.getCurrentStatus() === statusMap.canceled) {return;}
this._status = statusMap.fail;
sliceEvent.emmit("error", this._partIndex, res.err);
}
}
// 上传分片入口
async startUpload() {if (this.isContinueUpload() && !this.sliceFile) {this.generateSliceFile();
}
if (this.isContinueUpload() && !this.md5Content && this.sliceFile) {this.md5Content = await generateMD5(this.sliceFile);
}
if (this.isContinueUpload() && !this.signatureUrl && this.md5Content) {await this.getSignatureUrl();
}
if (this.isContinueUpload() && this.signatureUrl) {await this.uploadSlice();
}
return this._status === statusMap.success;
}
}
export default Slice;
import xyUpload from "./index.ts";
const client = new xyUpload({
file,
parallel: 3,
partSize: 2000000,
});
client.on("progress", (res) => {console.log("progress", res);
});
client.on("success", (res) => {resolve(res);
});
client.on("error", (res) => {reject(res);
});
client.on("start", () => {console.log("开始了");
});
// 开始上传
client.startUpload();
// 暂停上传
client.pauseUpload();
// 持续上传
client.resumeUpload();
// 破除上传
client.abortUpload();