前端大文件分片MinIO上传

Allen_kai_hui 2024-08-28 16:03:01 阅读 98

大文件分片上传是一种将大文件拆分成多个小文件片段进行上传的技术。这种方式可以提高上传效率,减少网络传输时间,并且在网络不稳定或者上传过程中出现错误时,可以更容易地恢复上传进度。

大文件分片上传的步骤如下:

将大文件分成多个固定大小的片段,通常每个片段的大小在几十KB到几MB之间。逐个上传每个文件片段,可以使用HTTP、FTP等协议进行传输。服务器接收到每个文件片段后,判断其MD5值进行保存或者合并操作。在上传过程中,通过MD5值维护一个上传进度记录,标记已经上传成功的文件片段,以便在上传中断后能够恢复上传进度。当所有文件片段都上传完成后,服务器将文件片段进行合并,得到完整的大文件。

大文件分片上传的好处有:

提高上传速度:将大文件拆分成小片段,可以同时上传多个片段,从而提高上传速度。断点续传:如果在上传过程中发生中断或者错误,可以根据上传进度记录,只重新上传丢失或者出错的文件片段,从而减少网络传输时间。易于管理:将大文件拆分成小片段,可以更方便地管理和存储,避免了一次性上传整个大文件可能导致的内存占用问题。

大文件分片上传技术已经广泛应用于各种云存储、文件传输等领域,为用户提供了更好的上传体验和效率。

视图代码 

 大文件分片需要读取时间所以要给加载状态,下面例子只适合单文件上传且带上传进度展示

<code><template>

<div class="slice-upload" v-loading="loading" element-loading-text="文件分片读取中"code>

element-loading-spinner="el-icon-loading">code>

<form id="fromCont" method="post" style="display: inline-block">code>

<el-button size="small" @click="inputChange" class="file-choose-btn" :disabled="uploading">code>

选择文件

<input v-show="false" id="file" ref="fileValue" :accept="accept" type="file" @change="choseFile" />code>

</el-button>

</form>

<slot name="status"></slot> code>

<div class="el-upload__tip">code>

请上传不超过 <span style="color: #e6a23c">{ { maxCalc }}</span> 的文件code>

</div>

<div class="file-list">code>

<transition name="list" tag="p">code>

<div v-if="file" class="list-item">code>

<i class="el-icon-document mr5"></i>code>

<span>{ { file.name }}

<em v-show="uploading" style="color: #67c23a">上传中....</em></span>code>

<span class="percentage">{ { percentage }}%</span>code>

<el-progress :show-text="false" :text-inside="false" :stroke-width="2" :percentage="percentage" />code>

</div>

</transition>

</div>

</div>

</template>

逻辑代码

需要引入Md5 

npm install spark-md5

<script>

import SparkMD5 from "spark-md5";

import axios from "axios";

import {

getImagecheckFile,//检验是否上传过用于断点续传

Imageinit,//用分片换取minIo上传地址

Imagecomplete,//合并分片

} from "/*接口地址*/";

export default {

name: "sliceUpload",

/**

* 外部数据

* @type {Object}

*/

props: {

/**

* @Description

* 代码注释说明

* 接口url

* @Return

*/

findFileUrl: String,

continueUrl: String,

finishUrl: String,

removeUrl: String,

/**

* @Description

* 代码注释说明

* 最大上传文件大小 100G

* @Return

*/

maxFileSize: {

type: Number,

default: 100 * 1024 * 1024 * 1024,

},

/**

* @Description

* 代码注释说明

* 切片大小

* @Return

*/

sliceSize: {

type: Number,

default: 50 * 1024 * 1024,

},

/**

* @Description

* 代码注释说明

* 是否可以上传

* @Return

*/

show: {

type: Boolean,

default: true,

},

accept: String,

},

/**

* 数据定义

* @type {Object}

*/

data() {

return {

/**

* @Description

* 代码注释说明

* 文件

* @Return

*/

file: null,//源文件

imageSize: 0,//文件大小单位GB

uploadId: "",//上传id

fullPath: "",//上传地址

uploadUrls: [],//分片上传地址集合

hash: "",//文件MD5

/**

* @Description

* 代码注释说明

* 分片文件

* @Return

*/

formDataList: [],

/**

* @Description

* 代码注释说明

* 未上传分片

* @Return

*/

waitUpLoad: [],

/**

* @Description

* 代码注释说明

* 未上传个数

* @Return

*/

waitNum: NaN,

/**

* @Description

* 代码注释说明

* 上传大小限制

* @Return

*/

limitFileSize: false,

/**

* @Description

* 代码注释说明

* 进度条

* @Return

*/

percentage: 0,

percentageFlage: false,

/**

* @Description

* 代码注释说明

* 读取loading

* @Return

*/

loading: false,

/**

* @Description

* 代码注释说明

* 正在上传中

* @Return

*/

uploading: false,

/**

* @Description

* 代码注释说明

* 暂停上传

* @Return

*/

stoped: false,

/**

* @Description

* 代码注释说明

* 上传后的文件数据

* @Return

*/

fileData: {

id: "",

path: "",

},

};

},

/**

* 数据监听

* @type {Object}

*/

watch: {

//监控上传进度

waitNum: {

handler(v, oldVal) {

let p = Math.floor(

((this.formDataList.length - v) / this.formDataList.length) * 100

);

// debugger;

this.percentage = p > 100 ? 100 : p;

},

deep: true,

},

show: {

handler(v, oldVal) {

if (!v) {

this.file = null

}

},

deep: true,

},

},

/**

* 方法集合

* @type {Object}

*/

methods: {

/**

* 代码注释说明

* 内存过滤器

* @param {[type]} ram [description]

* @return {[type]} [description]

*/

ramFilter(bytes) {

if (bytes === 0) return "0";

var k = 1024;

let sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

let i = Math.floor(Math.log(bytes) / Math.log(k));

return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];

},

/**

* 触发上传 文件处理

* @param e

*/

async choseFile(e) {

const fileInput = e.target.files[0]; // 获取当前文件

this.imageSize = this.ramFilter(fileInput.size);//记录文件大小

if (!fileInput && !this.show) {

return;

}

const pattern = /[\u4e00-\u9fa5]/;

if (pattern.test(fileInput?.name)) {

this.$message.warning("请不要上传带有中文名称的镜像文件!");

return;

}

this.file = fileInput; // file 丢全局方便后面用 可以改进为func传参形式

this.percentage = 0;

if (this.file.size < this.maxFileSize) {

this.loading = true;

const FileSliceCap = this.sliceSize; // 分片字节数

let start = 0; // 定义分片开始切的地方

let end = 0; // 每片结束切的地方a

let i = 0; // 第几片

this.formDataList = []; // 分片存储的一个池子 丢全局

this.waitUpLoad = []; // 分片存储的一个池子 丢全局

while (end < this.file.size && this.show) {

/**

* @Description

* 代码注释说明

* 当结尾数字大于文件总size的时候 结束切片

* @Return

*/

start = i * FileSliceCap; // 计算每片开始位置

end = (i + 1) * FileSliceCap; // 计算每片结束位置

var fileSlice = this.file.slice(start, end); // 开始切 file.slice 为 h5方法 对文件切片 参数为 起止字节数

const formData = new window.FormData(); // 创建FormData用于存储传给后端的信息

// formData.append('fileMd5', this.fileMd5) // 存储总文件的Md5 让后端知道自己是谁的切片

formData.append("file", fileSlice); // 当前的切片

formData.append("chunkNumber", i); // 当前是第几片

formData.append("fileName", this.file.name); // 当前文件的文件名 用于后端文件切片的命名 formData.appen 为 formData对象添加参数的方法

this.formDataList.push({ key: i, formData }); // 把当前切片信息 自己是第几片 存入我们方才准备好的池子

i++;

}

//获取文件的MD5值

this.computeFileMD5(this.file, FileSliceCap).then(

(res) => {

if (res) {

this.hash = res;

//console.log("拿到了:", res);

// this.UploadStatus = `文件读取成功(${res}),文件上传中...`;

//通过Md5值查询是否上传过

getImagecheckFile({ fileCode: res }).then(

(res2) => {

this.loading = false;

/**

* @Description

* 代码注释说明

* 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片

* fileUrl:有地址就是秒传因为已经存在该文件了

* shardingIndex:返回哪些已经上传用于断点续传

* @Return

*/

let { fileUrl, shardingIndex } = res2.data.data; //检测是否上传过

if (!fileUrl) {

/**

* @Description

* 代码注释说明

* 当是断点续传时候

* 记得处理一下当前是默认全都没上传过暂不支持断点续传后端无法返回已上传数据如果你家后端牛一点可以在此处理断点续传

* @Return

*/

this.waitUpLoad = this.formDataList;//当前是默认全都没上传过断点续传需处理

this.getFile()

} else {

// debugger;

this.formDataList = [{ key: fileUrl }];

this.waitNum = 1;

this.waitUpLoad = []; // 秒传则没有需要上传的切片

this.$message.success("文件已秒传");

this.$emit("fileinput", {

url: fileUrl,

code: this.hash,

imageSize: this.imageSize,

});

this.waitNum = 0;

// this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.loading = false;

return;

}

this.waitNum = this.waitUpLoad.length; // 记录长度用于百分比展示

},

(err) => {

this.$message.error("获取文件数据失败");

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.loading = false;

return;

}

);

} else {

// this.UploadStatus = "文件读取失败";

}

},

(err) => {

// this.UploadStatus = "文件读取失败";

this.uploading = false;

this.loading = false;

this.$message.error("文件读取失败");

}

);

} else {

//this.limitFileSize = true;

this.$message.error("请上传小于100G的文件");

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

}

},

//准备上传

getFile() {

/**

* @Description

* 代码注释说明

* 确定按钮

* @Return

*/

if (this.file === null) {

this.$message.error("请先上传文件");

return;

}

this.percentageFlage = this.percentage == 100;

this.sliceFile(); // 上传切片

},

async sliceFile() {

/**

* @Description

* 代码注释说明

* 如果已上传文件且生成了文件路径

* @Return

*/

if (this.fileData.path) {

return;

}

/**

* @Description

* 代码注释说明

* 如果是切片已全部上传 但还未完成合并及移除chunk操作 没有生成文件路径时

* @Return

*/

if (this.percentageFlage && !this.fileData.path) {

this.finishUpload();

return;

}

this.uploading = true;

this.stoped = false;

//提交切片

this.upLoadFileSlice();

},

async upLoadFileSlice() {

if (this.stoped) {

this.uploading = false;

return;

}

/**

* @Description

* 代码注释说明

* 剩余切片数为0时调用结束上传接口

* @Return

*/

try {

let suffix = /\.([0-9A-z]+)$/.exec(this.file.name)[1]; // 文件后缀名也就是文件类型

let data = {

bucketName: "static",//桶的名字

contentType: this.file.type || suffix,//文件类型

filename: this.file.name,//文件名字

partCount: this.waitUpLoad.length,//分片多少也就是分了多少个

};

//根据分片长度获取分片上传地址以及上传ID和文件地址

Imageinit(data).then((res) => {

if (res.data.code == 200 && res.data.data) {

this.uploadId = res.data.data.uploadId;//文件对应的id

this.fullPath = res.data.data.fullPath;//上传合并的地址

this.uploadUrls = res.data.data.uploadUrls;//每个分片对应的位置

if (this.uploadUrls && this.uploadUrls.length) {

/**

* 用于并发上传 parallelRun

*/

// this.waitUpLoad.forEach((item, i) => {

// item.formData.append("Upurl", this.uploadUrls[i]);

// });

// this.parallelRun(this.waitUpLoad)

// return;

let i = 0;//第几个分片对应地址

/**

* 文件分片合并

*/

const complete = () => {

Imagecomplete({

bucketName: "static",//MinIO桶名称

fullPath: this.fullPath,//Imageinit返回的上传地址

uploadId: this.uploadId,//Imageinit返回的上传id

}).then(

(res) => {

if (res.data.data) {

this.uploading = false;

this.$emit("fileinput", {

url: "/*minIo桶地址*/" + this.fullPath,//最终文件路径表单提交用

code: this.hash,//md5值校验

imageSize: this.imageSize,//文件大小

name: this.file.name,//文件名

});

this.$message({

type: "success",

message: "上传镜像成功",

});

this.$refs.fileValue.value = ''

this.uploading = false;

} else {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("合并失败");

}

},

(err) => {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("合并失败");

}

);

};

/**

* 分片上传

*/

const send = async () => {

if (!this.show) {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

return;

}

/**

* 没有可上传的请求合并

*/

if (i >= this.uploadUrls.length) {

// alert('发送完毕')

// 发送完毕

complete();

return;

}

if (this.waitNum == 0) return;

/**

* 通过AXIOS的put将对应的分片文件传到对应的桶里

*/

try {

axios

.put(

this.uploadUrls[i],

this.waitUpLoad[i].formData.get("file")

)

.then(

(result) => {

/*上传一个分片成功就对应减少一个再接着下一个分片上传

*/

this.waitNum--;

i++;

send();

},

(err) => {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("上传失败");

}

);

} catch (error) {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("上传失败");

}

};

send(); // 发送请求

}

}

});

} catch (error) {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("上传失败");

}

},

inputChange() {

this.$refs["fileValue"].dispatchEvent(new MouseEvent("click"));

},

/**

* 用于并发分片上传

* requestList 上传列表 max几个上传并发执行

*/

async parallelRun(requestList, max = 10) {

const requestSliceList = [];

for (let i = 0; i < requestList.length; i += max) {

requestSliceList.push(requestList.slice(i, i + max));

}

for (let i = 0; i < requestSliceList.length; i++) {

const group = requestSliceList[i];

console.log(group);

try {

const res = await Promise.all(group.map(fn => axios.put(

fn.formData.get("Upurl"),

fn.formData.get("file")

)));

res.forEach(item => {

this.waitNum--

})

console.log('接口返回值为:', res);

if (this.waitNum === 0) {

//alert('发送完毕')

// 发送完毕

this.complete();

return;

}

// const res = await Promise.all(group.map(fn => fn));

} catch (error) {

console.error(error);

}

}

},

complete() {

Imagecomplete({

bucketName: "static",//对应的桶

fullPath: this.fullPath,//桶的地址

uploadId: this.uploadId,//桶的id

}).then(

(res) => {

if (res.data.data) {

this.uploading = false;

this.$emit("fileinput", {

url: "/*minIo桶地址*/" + this.fullPath,//'https://asgad/fileinput'+'/1000/20240701/xxx.zip'

code: this.hash,//文件MD5值

imageSize: this.imageSize,//文件大小

});

this.$message({

type: "success",

message: "上传镜像成功",

});

this.$refs.fileValue.value = ''

this.uploading = false;

} else {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("合并失败");

}

},

(err) => {

this.file = null;

this.$refs.fileValue.value = ''

this.uploading = false;

this.$message.error("合并失败");

}

);

},

/**

* 获取大文件的MD5数值

* @param {*} file 文件

* @param {*} n 分片大小单位M

*/

computeFileMD5(file, n = 50 * 1024 * 1024) {

//("开始计算...", file);

return new Promise((resolve, reject) => {

let blobSlice =

File.prototype.slice ||

File.prototype.mozSlice ||

File.prototype.webkitSlice;

let chunkSize = n; // 默认按照一片 50MB 分片

let chunks = Math.ceil(file.size / chunkSize); // 片数

let currentChunk = 0;

let spark = new SparkMD5.ArrayBuffer();

let fileReader = new FileReader();

let that = this;

fileReader.onload = function (e) {

//console.log("read chunk nr", currentChunk + 1, "of", chunks);

spark.append(e.target.result);

currentChunk++;

// console.log("执行进度:" + (currentChunk / chunks) * 100 + "%");

if (currentChunk < chunks && that.show) {

loadNext();

} else {

// console.log("finished loading");

let md5 = spark.end(); //最终md5值

spark.destroy(); //释放缓存

if (currentChunk === chunks) {

resolve(md5);

} else {

reject(e);

}

}

};

fileReader.onerror = function (e) {

reject(e);

};

function loadNext() {

let start = currentChunk * chunkSize;

let end =

start + chunkSize >= file.size ? file.size : start + chunkSize;

fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));

}

loadNext();

});

},

},

};

</script>

页面样式 

 自行修改

<!-- 当前组件页面样式定义 -->

<style lang="scss" scoped>code>

.file-choose-btn {

overflow: hidden;

position: relative;

input {

position: absolute;

font-size: 100px;

right: 0;

top: 0;

opacity: 0;

cursor: pointer;

}

}

.tips {

margin-top: 30px;

font-size: 14px;

font-weight: 400;

color: #606266;

}

.file-list {

margin-top: 10px;

}

.list-item {

display: block;

margin-right: 10px;

color: #606266;

line-height: 25px;

margin-bottom: 5px;

width: 90%;

.percentage {

float: right;

}

}

.list-enter-active,

.list-leave-active {

transition: all 1s;

}

.list-enter,

.list-leave-to

/* .list-leave-active for below version 2.1.8 */

{

opacity: 0;

transform: translateY(-30px);

}

</style>

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。