文章目录
概要
前段时间公司做了一个云盘的项目,已经测试应用过一份分片上传,但是效果不是很理想,在最近一期项目中,又涉及到了大文件的分片上传,重新整理了一份,找了很多方案,要不就是纯后端上传本地文件的,要不就是使用AmazonS3的分片上传,而AmazonS3的分片上传也是基于一整个文件的,用来做前后端联合的分片上传会产生很多问题,本套代码非最终版,但仅限容错补偿的地方,随时可以加入。
整体结构
后台管理内容用若依plus搭建的后台管理框架,但是和本文并没有太大关系,前端使用vue3,后端为springboot,文件服务器采用的minio,或者其他的文件服务器都差不多,由于若依集成的是AmazonS3的文件上传工具,以此为基本情况,处理文件上传的相关内容。
前置准备
minio的搭建教程网上有很多,就不细说了
具体jar aws1.12.540
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.540</version>
</dependency>
正式开始
方案一、分片文件直接上传至minio服务器,最后交由minio文件服务器进行合并,这种方案适用于大文件,但不一定适用于视频上传
基本情况
现在网上大部分的分片上传存在的问题,基本都是复制粘贴的,没差,而且大部分没办法进行前后端联合的分片上传,要不就先把文件上出纳到服务端,然后分片上传进文件服务器,我参考整理了以下的文档链接,这个是可以进行分片上传的,且代码完整
Minio分片上传、断点续传、分片下载、秒传、暂停(断点)下载(Java版-附源码地址)
你们参考这里也是可以的,这个方案也是可行的,接下来给出的是我的处理方案。
一、前端部分,封装组件 VideoUpload
出去element-plus之外,引入的包也就一个SparkMd5,用来计算文件MD5值
<template>
<el-upload
:file-list="fileList"
:on-change="handleFileChange"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:auto-upload="false"
:show-file-list="false"
:limit="limit"
class="upload-file-uploader"
ref="fileUpload"
multiple
>
<el-button type="primary">选择视频文件</el-button>
</el-upload>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<div :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {
{
getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
</div>
</transition-group>
<el-progress
v-if="progressShow"
style="width: 100%;"
:text-inside="true"
:stroke-width="24"
:percentage="progress"
:format="(percentage) => (`${progressText}${percentage}%`)"
/>
</template>
<script setup>
import {
ref} from 'vue';
import {
chunkUploadFile, delOss, initChunkUpload, listByIds, overChunkUpload} from "@/api/system/oss";
import SparkMD5 from 'spark-md5';
// 定义相关变量
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const fileList = ref([]);
const chunkSize = 1024 * 1024 * 5; // 5MB per chunk
const currentChunk = ref(0);
const totalChunks = ref(0);
const currentFile = ref(null);
const loading = ref(false);
const progressShow = ref(false);
const isShow = ref(true);
const fileMd5 = ref(undefined);
const fileSize = ref(undefined);
const progress = ref(0);
const progressText = ref("");
const {
proxy } = getCurrentInstance();
const props = defineProps({
modelValue: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
}
});
watch(() => props.modelValue, async val => {
if (val) {
let temp = 1;
// 首先将值转为数组
let list;
if (Array.isArray(val)) {
list = val;
} else {
await listByIds(val).then(res => {
list = res.data.map(oss => {
oss = {
name: oss.originalName, url: oss.url, ossId: oss.ossId };
return oss;
});
})
}
// 然后将数组转为对象数组
fileList.value = list.map(item => {
item = {
name: item.name, url: item.url, ossId: item.ossId};
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
},{
deep: true, immediate: true });
// 最大并发数
const MAX_CONCURRENT_UPLOADS = 5;
// 文件变化处理
const handleFileChange = async (file, fileList) => {
console.log('file', file);
// 判断是否是 video 类型
if (file?.raw?.type.startsWith('video/')) {
currentFile.value = file.raw;
totalChunks.value = Math.ceil(currentFile.value.size / chunkSize);
currentChunk.value = 0;
fileSize.value = file.size;
fileMd5.value = undefined;
if (totalChunks.value > 0) {
await uploadChunks(currentFile.value);
}
} else {
alert('请选择一个视频文件!');
}
};
// 上传前文件验证
const beforeUpload = async (file) => {
const isLt100M = file.size / 1024 / 1024 / 1024 < 4;
if (!isLt100M) {
alert('上传文件大小不能超过 2GB!');
return false;
}
proxy.$modal.loading("正在上传文件,请稍候...");
progressShow.value = true;
progress.value = 0;
progressText.value = "文件解析中";
// 计算文件 MD5,返回一个同步等待的 Promise
fileMd5.value = await getFileMd5(file)
// 初始化分片上传
number.value++;
progress.value = 0;
progressText.value = "文件上传中";
let initResp = await initChunkUpload({
chunks:totalChunks.value,name:file.name,fileSize:file.size,md5:fileMd5.value});
return initResp.code === 200;
};
// 创建分片
const createChunk = (file, index) => {
const start = index * chunkSize;
const end = Math.min(file.size, start + chunkSize);
return file.slice(start, end);
};
// 上传分片
const uploadChunk = async (file, index) => {
const formData = new FormData();
const chunk = createChunk(file, index);
formData.append('file', chunk);
formData.append('chunk', index + 1);
formData.append('chunks', totalChunks.value);
formData.append('name', file.name);
formData.append('fileSize', file.size);
formData.append('md5', fileMd5.value);
try {
const response = await chunkUploadFile(formData);
if (response.code === 200) {
currentChunk.value++;
console.log(`分片 ${
index + 1} 上传成功`);
} else {
console.error('文件分片上传失败');
}
} catch (error) {
console.error('文件分片上传发生错误', error);
}
};
// 控制并发上传分片
const uploadChunks = async (file) => {
await beforeUpload(file);
// 更新进度条的函数
const updateProgress = () => {
progress.value = ((currentChunk.value / totalChunks.value) * 100).toFixed(2); // 更新进度条的值
};
// 上传分片任务队列
const uploadInQueue = async (index) => {
await uploadChunk(file, index);
updateProgress(); // 每上传一个分片更新进度条
};
// 创建一个控制并发的队列
const executeUploadQueue = async () => {
const promises = [];
for (let i = currentChunk.value; i < totalChunks.value; i++) {
// 如果正在上传的分片数达到最大限制,等待队列空闲
if (promises.length >= MAX_CONCURRENT_UPLOADS) {
// 等待最早的上传任务完成
await Promise.race(promises);
}
// 开始上传当前分片
const uploadPromise = uploadInQueue(i).finally(() => {
// 每个分片上传完成后,移除该任务
promises.splice(promises.indexOf(uploadPromise), 1);
});
// 将当前任务添加到队列中
promises.push(uploadPromise);
}
// 等待所有任务完成
await Promise.all(promises);
};
// 执行上传队列
await executeUploadQueue();
// 所有分片上传完后,执行后续操作
let res = await overChunkUpload({
md5: fileMd5.value });
handleUploadSuccess(res, file);
console.log('所有分片上传成功');
};
// 文件个数超出
function handleExceed () {
proxy.$modal.msgError(`上传文件数量不能超过 ${
props.limit} 个!`);
}
const getFileMd5 = (file) => {
var fileReader = new FileReader();
var blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice;
var chunks = Math.ceil(file.size / chunkSize);
var currentChunk = 0;
var spark = new SparkMD5();
// 初始化进度条
return new Promise((resolve) => {
fileReader.onload = function (e) {
spark.appendBinary(e.target.result); // append binary string
currentChunk++;
// 更新进度条
progress.value = ((currentChunk / chunks) * 100).toFixed(2); // 更新进度条的值
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = () => {
console.error('文件读取失败,无法上传该文件');
};
function loadNext() {
var start = currentChunk * chunkSize;
var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsBinaryString(blobSlice.call(file, start, end));
}
loadNext();
});
};
function handleDelete(index) {
let ossId = fileList.value[index].ossId;
delOss(ossId);
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
}
}
// 上传成功回调
function handleUploadSuccess