多视频分片上传组件
<template>
<div class="img" style="margin-bottom: -16px">
<a-upload
name="file"
listType="picture-card"
:multiple="isMultiple"
:accept="accept"
:action="uploadAction"
:headers="headers"
:data="{biz:bizPath,watermark:watermark,bl:bl}"
:fileList="fileList"
:before-upload="beforeUpload"
:disabled="disabled"
:isMultiple="isMultiple"
:remove="handleDelete"
@preview="handlePreview"
:show-upload-list={showRemoveIcon:this.showRemoveIcon,showPreviewIcon:true}
:class="[(!isMultiple||!this.showRemoveIcon)?'image-upload-single-over':'']"
>
<div >
<div class="iconp">
<a-icon :type="uploadLoading ? 'loading' : 'plus'" />
<div class="ant-upload-text">{{ text }}</div>
</div>
</div>
<!-- 弹出视频显示组件 -->
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel()">
<video v-if="previewVisible" width="100%" controls id="video" >
<source :src="previewImage" />
<object :data="previewImage" width="100%">
<embed :src="previewImage" width="100%"/>
</object>
</video>
</a-modal>
</a-upload>
<a-progress v-if="show" :percent="uploadPercent" :status="statusFlag" />
<j-image-open ref="imagePreview"></j-image-open>
</div>
</template>
<script>
import { httpAction, getAction } from '@/api/manage'
const doc = require('@/assets/fileImage/doc.png')
import Vue from 'vue'
import { randomUUID } from '@/utils/util'
import { ACCESS_TOKEN } from "@/store/mutation-types"
import { getFileAccessHttpUrl } from '@/api/manage'
import JImageOpen from './JImageOpen'
const uidGenerator=()=>{
return '-'+parseInt(Math.random()*10000+1,10);
}
const getFileName=(path)=>{
if(path.lastIndexOf("\\")>=0){
let reg=new RegExp("\\\\","g");
path = path.replace(reg,"/");
}
return path.substring(path.lastIndexOf("/")+1);
}
export default {
name: 'JVideoUpload',
components: {
JImageOpen
},
data(){
return {
uploadPercent:0,
statusFlag:'active',
show:false,
tempThreads: 2,
chunkSize: 10 * 1024 * 1024,
uploadAction:window._CONFIG['domianURL']+"/sys/common/uploadPart",
mergeAction:window._CONFIG['domianURL']+"/sys/common/merge",
msg:"",
successs:false,
uploadLoading:false,
headers:{},
fileList: [],
coverList: [],
previewImage:"",
previewVisible: false,
coverSrc:'',
uuId:'',
a:true,
}
},
props:{
backgroundImage:{
type:String,
required:false,
default:""
},
accept: {
type: String,
required:false,
default: ""
},
imgList:{
type:Array,
required:false,
default:function () { return [] }
},
text:{
type:String,
required:false,
default:"视频"
},
/*这个属性用于控制文件上传的业务路径*/
bizPath:{
type:String,
required:false,
default:"temp"
},
showRemoveIcon:{
type:Boolean,
required:false,
default:true
},
/*这个属性用于控制文件上传是否添加水印*/
watermark:{
type:String,
required:false,
default:"false"
},
/*这个属性用于控制图片上传宽高比例*/
bl:{
type:String,
required:false,
default:""
},
value:{
type:[String,Array],
required:false
},
disabled:{
type:Boolean,
required:false,
default: false
},
isMultiple:{
type:Boolean,
required:false,
default: true
},
number:{
type:Number,
required:false,
default:0
},
index:{
type:Number,
required:false,
default:0
},
classFlag:{
type:String,
required:false,
default:''
}
},
watch:{
value: {
handler(val,oldValue) {
if (val instanceof Array) {
this.initFileList(val.join(','),this.backgroundImage);
} else {
this.initFileList(val,this.backgroundImage);
}
if(!val || val.length==0){
this.picUrl = false;
}
},
//立刻执行handler
immediate: true
},
backgroundImage:{
handler(backgroundImage) {
if(backgroundImage){
this.initFileList(this.value,this.getFileUrl(backgroundImage))
}
},
},
previewVisible:function (val) {
console.log('监听弹层是否打开,关闭视频',this.previewVisible)
if(this.previewVisible === false){
let my_video = document.getElementById("video")
my_video.pause();
}
},
},
created(){
const token = Vue.ls.get(ACCESS_TOKEN);
this.headers = {"X-Access-Token":token}
this.uuId = this.getUUID();
},
mounted(){
},
methods:{
getUUID(){
return randomUUID();
},
getFileUrl(url){
return getFileAccessHttpUrl(url);
},
initFileList(paths,backgroundimg){
if(!paths || paths.length==0){
this.fileList = [];
return;
}
if(this.fileList.length){
return;
}
let fileList = [];
let arr = paths.split(",")
let arr2 = backgroundimg.split(",")
for(var a=0;a<arr.length;a++){
let url = this.getFileUrl(arr[a]);
fileList.push({
uid: uidGenerator(),
message: this.getFileUrl(arr[a]),
name: getFileName(arr[a]),
status: 'done',
url: url,
thumbUrl:this.getFileUrl(arr2[a]),
thumbUrl2:arr2[a],
response:{
status:"history",
message:arr[a]
}
})
}
this.fileList = fileList
},
beforeUpload:function(file){
//验证视频类型
const fileType = file.name.split('.')[file.name.split('.').length - 1]
if("mp4"!==fileType){
this.$message.warn(file.name + "不是mp4格式!");
return false
}
this.show = true;
this.statusFlag = 'active';
file.status = "uploading";//设置文件的状态为上传中
let dt = new Date();
let year = dt.getFullYear();
let month = (dt.getMonth() + 1).toString().padStart(2,'0');
let date = dt.getDate().toString().padStart(2,'0');
let timestamp = year+month+date;
const name = timestamp+"_"+Math.floor(Math.random() * 900000);
this.checkFile(file,name);
return false; //false 不调用组件的上传接口 true 直接调用组件的上传接口
},
async checkFile(file,name) {
let aa = await this.uploadChunks(file,name);
const addFile = {
uid: file.uid,
name: getFileName(aa.message),
thumbUrl: this.getFileUrl(aa.result.filePath),
thumbUrl2: aa.result.filePath,
status: 'done',
response: {
status: 'done',
message: this.getFileUrl(aa.message),
},
url: this.getFileUrl(aa.message),
message: aa.message,
}
this.fileList.push(addFile)
this.$emit('change', this.fileList.map((item) => item.message).join(','))
this.$emit('change2', this.fileList.map((item) => item.thumbUrl2).join(','))
},
//上传分片信息
async uploadChunks(file,name) {
//切割文件获取分片集合
const fileChunkList = this.createFileChunk(file, this.chunkSize);
file.chunkList = fileChunkList.map(({ file }, index) => ({
index,
source: file,
size: file.size
}));
var chunkData = file.chunkList;
return new Promise((resolve, reject) => {
const requestDataList = chunkData.map(value => {
const formData = new FormData();
formData.append("chunk", value.index);
formData.append("file", value.source);
formData.append("bizPath", this.bizPath);
formData.append("fileName", name+file.name+"_"+"part"+value.index);
return { formData, index: value.index, file };
});
try {
const ret = this.sendRequest(requestDataList);
resolve(ret);
} catch (error) {
this.$message.error("上传失败,请重试");
reject("sendRequest 失败", error);
}
}).then(async res => {
let aa = await this.mergeRequest(file,name,file.name,fileChunkList.length);
return aa
});
},
//切割分片
createFileChunk(file, size) {
const fileChunkList = [];
let count = 0;
while (count < file.size) {
fileChunkList.push({
file: file.slice(count, count + size)
});
count += size;
}
return fileChunkList;
},
// 并发,重试请求上传分片
async sendRequest(list) {
var finished = 0;
const retryArr = []; // retryArr.length代表请求数,值代表重试次数
var currentFileInfo;
const total = list.length;
// 所有请求都存放这个promise中
return new Promise((resolve, reject) => {
const handler = () => {
if (list.length) {
const formInfo = list.shift();
const index = formInfo.index;
const http = this.getUploadPart(formInfo.formData);
// 分块不存在或不完整,重新发送该分块内容
http.then(res => {
console.log("分片上传完成回调数据!")
console.log(res)
if (res.success) {
const percent = Math.round((index / total) * 100);
this.uploadPercent = percent;
currentFileInfo = formInfo;
}
return res;
})
.then(res => {
if (res.success) {
finished++;
handler();
} else {
this.$message.error(res.msg);
}
})
.catch(e => {
console.log("失败了么")
if (typeof retryArr[index] !== "number") {
retryArr[index] = 1;
}
if (retryArr[index] >= this.chunkRetry) {
return reject("重试失败", retryArr);
}
// console.log(`${formInfo.file.name}--文件的第 ${index} 个分块,开始进行第 ${retryArr[index]} 次重试`);
retryArr[index]++; // 累加
this.tempThreads++; // 释放当前占用的通道
list.push(formInfo); // 将失败的重新加入队列
handler();
});
}
if (finished >= total) {
console.log("执行了几次==="+finished)
resolve(currentFileInfo); // 输出当前完成上传的文件信息
}
};
// 控制并发
for (let i = 0; i < this.tempThreads; i++) {
handler();
}
});
},
//执行分片上传
getUploadPart(formData) {
return new Promise((resolve, reject) => {
httpAction(this.uploadAction,formData,"post").then((res)=>{
if(res.success){
resolve(res);
}
})
})
},
//执行合并方法
getMerge(mergeFormData) {
return new Promise((resolve, reject) => {
httpAction(this.mergeAction,mergeFormData,"post").then((res)=>{
if(res.success){
resolve(res);
}
})
})
},
//合并分片
async mergeRequest(file,path,fileName,chunkNum) {
return new Promise((resolve, reject) => {
const mergeFormData = new FormData();
mergeFormData.append("path", path);
mergeFormData.append("fileName", fileName);
mergeFormData.append("chunkNum", chunkNum);
mergeFormData.append("bizPath", this.bizPath);
mergeFormData.append("uid", file.uid);
const http = this.getMerge(mergeFormData);
http.then(res => {
if (res.success) {
this.$message.success(fileName + " 文件上传成功!");
if(this.uploadPercent<100){
this.uploadPercent = 100;
}
this.statusFlag = 'success';
resolve(res);
} else {
this.statusFlag = 'exception';
this.$message.error(res.msg);
}
}).catch(err => {
this.statusFlag = 'exception';
reject("合并失败", err);
});
});
},
//不用
handleChange(info) {
let fileList = info.fileList
if (this.number > 0 && this.isMultiple) {
fileList = fileList.slice(-this.number);
}
if (info.file.status === 'done') {
if (info.file.response.success) {
fileList = fileList.map((file) => {
if (file.response) {
const data = JSON.parse(file.response.message);
file.url = getFileAccessHttpUrl(data.newvideo);
this.$emit("success", data)
}
return file;
});
}
} else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} 上传失败.`);
} else if (info.file.status === 'removed') {
this.handleDelete(info.file)
}
this.fileList = fileList
if (info.file.status === 'done' || info.file.status === 'removed') {
if (info.file.status === 'removed') {
this.msg = "";
this.successs = false;
this.handlePathChange()
} else {
let errorMsg = JSON.parse(info.file.response.message);
this.successs = info.file.response.success;
this.msg = errorMsg.uploadStatus;
this.handlePathChange()
}
}
},
// 预览
handlePreview (file) {
this.previewImage = file.url || file.thumbUrl
this.previewImage = this.previewImage+"?index="+this.getUUID();
this.previewVisible = true
},
//不用
coverOpen(file) {
this.$refs.imagePreview.imageOpen(file);
},
//不用
handlePathChange(){
let uploadFiles = this.fileList
let path = ''
if(!uploadFiles || uploadFiles.length==0){
path = ''
}
let arr = [];
if(!this.isMultiple && uploadFiles.length>0){
const data = JSON.parse(uploadFiles[uploadFiles.length-1].response.message);
arr.push(data.newvideo)
}else{
for(let a=0;a<uploadFiles.length;a++){
if(uploadFiles[a].status === 'done' ) {
const data = JSON.parse(uploadFiles[a].response.message);
arr.push(data.newvideo)
}else{
return;
}
}
}
this.$emit('change', arr.join(","));
},
/*删除逻辑
* fileList 找到要删除的file的uid唯一表示去除
* 上传进度条隐藏
* 返回父类更新v-model的值
* */
handleDelete(file){
this.fileList = this.fileList.filter((item) => item.uid!== file.uid)
this.show = false;
this.$emit('change', this.fileList.map((item) => item.message).join(','))
this.$emit('change2', this.fileList.map((item) => item.thumbUrl2).join(','))
},
//视频预览关闭
handleCancel() {
this.previewVisible = false;
},
},
model: {
prop: 'value',
event: 'change',
}
}
</script>
<style scoped>
/deep/ .imgupload .iconp{padding:20px;}
/deep/ .image-upload-single-over .ant-upload-select{display: none}
</style>
引入
<a-row>
<a-col :span="24">
<a-form-model-item label="佐证视频" :labelCol="labelCol2" :wrapperCol="wrapperCol2" prop="filePath">
<j-upload-part-video :accept="accept" @change2="videoSuccess" :backgroundImage="model.cover" v-model="model.filePath" placeholder="请输入佐证视频" ></j-upload-part-video>
</a-form-model-item>
</a-col>
</a-row>
methods: {
//成功后复制需要保存的字段
videoSuccess(d) {
this.$set(this.model, 'cover', d);
},
}
后台
commonController
/**
* 分片上传minio
* @param request
* @param response
* @return
*/
@PostMapping(value = "/uploadPart")
public Result<?> uploadPart(HttpServletRequest request, HttpServletResponse response) {
Result<?> upload = uploadUtil.uploadPart(request, response);
return upload;
}
/**
* 合并分片并删除分片
* @param request
* @param response
* @return
*/
@PostMapping(value = "/merge")
public Result<?> merge(HttpServletRequest request, HttpServletResponse response) {
Result<?> upload = uploadUtil.merge(request, response);
return upload;
}
uploadUtil
/**
* 分片上传minio
* @param request
* @param response
* @return
*/
public Result<?> uploadPart(HttpServletRequest request, HttpServletResponse response) {
String fileName = request.getParameter("fileName");//分片文件名称
//String chunkNum = request.getParameter("chunk");//上传的是第几片
String bizPath = request.getParameter("bizPath");//mino存放文件夹
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile file = multipartRequest.getFile("file");// 获取上传文件对象 blob 对象
if (oConvertUtils.isEmpty(bizPath)) {
bizPath = "temp";
}
Result<ResultVo> result = new Result<>();
String minioPartPath = CommonUtils.uploadPart(file, bizPath, fileName);
if (oConvertUtils.isNotEmpty(minioPartPath)) {
result.setMessage(minioPartPath);
result.setSuccess(true);
//添加文件上传记录日志
//String originalFilename = file.getOriginalFilename();
//fileDataService.save(FileDataApply.NO,bizPath,uploadType,originalFilename,savePath);
} else {
result.setMessage("上传失败!");
result.setSuccess(false);
}
return result;
}
/**
* 合并分片
* @param request
* @param response
* @return
*/
public Result<?> merge(HttpServletRequest request, HttpServletResponse response) {
String path = request.getParameter("path");
String chunkNum = request.getParameter("chunkNum");
String fileName = request.getParameter("fileName");
String bizPath = request.getParameter("bizPath");
String uid = request.getParameter("uid");
if (oConvertUtils.isEmpty(bizPath)) {
bizPath = "temp";
}
String merge = CommonUtils.merge(path, fileName, chunkNum,bizPath);
//获取文件后缀
String suffix = merge.substring(merge.lastIndexOf(".")+1);
String name = merge.substring(0,merge.lastIndexOf("."));
Result<ResultVo> result = new Result<>();
if("mp4".equals(suffix)){
//获取视频封面
String coverImage = videoToCover.coverImageToMinio(merge, name+ "_" + DateUtil.format(new Date(), "yyyyMMdd")+"_"+ RandomUtil.randomNumbers(6));
ResultVo vo = new ResultVo();
vo.setFilePath(coverImage);
vo.setUid(uid);
result.setResult(vo);
}
result.setMessage(merge);
result.setCode(200);
result.setSuccess(true);
return result;
}
CommonUtils
/**
* 分片上传minio
* @param file 分片文件 blob
* @param bizPath 保存minio文件夹
* @param fileName 分片文件名称
* @return
*/
public static String uploadPart(MultipartFile file, String bizPath,String fileName) {
String fileName1 = getFileName(fileName);
return MinioUtil.uploadPart(file,bizPath,fileName1);
}
/**
* 组装需要合并的分片
* @param filePath 自定义文件路径
* @param fileName 原始文件名称
* @param chunkNum 总分片
* @return
*/
public static String merge(String filePath,String fileName,String chunkNum,String bizPath) {
String fileName3 = getFileName(fileName);
bizPath= StrAttackFilter.filter(bizPath);
String minioWjj = filePath.substring(0,filePath.indexOf("_"));
List<String> partList = new ArrayList<>();
//组装全部分片
for (int i = 0; i <Integer.parseInt(chunkNum) ; i++) {
partList.add(bizPath+"/"+ minioWjj+"/"+filePath+fileName3+"_part"+i);
}
//获取后缀
String suffix = fileName3.substring(fileName3.lastIndexOf("."));
String random = fileName3.substring(0,fileName3.lastIndexOf("."));
random = random+"_"+System.currentTimeMillis();
String fileName1 = bizPath+"/"+ minioWjj+"/"+random+suffix;
String merge = MinioUtil.merge(partList, fileName1);
return merge;
}
minioUtils
/**
* 分片上传minio
* @param file 分片文件
* @param bizPath
* @param fileName
* @return
*/
public static String uploadPart(MultipartFile file, String bizPath,String fileName) {
String file_url = "";
bizPath=StrAttackFilter.filter(bizPath);
try {
initMinio(minioUrl, minioName,minioPass);
// 检查存储桶是否已经存在
if(minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
log.info("Bucket already exists.");
} else {
// 创建一个名为ota的存储桶
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("create a new bucket.");
}
InputStream stream = file.getInputStream();
String minioWjj = fileName.substring(0,fileName.indexOf("_"));
String objectName = bizPath+"/"+minioWjj+"/"+fileName;
if(objectName.startsWith("/")){
objectName = objectName.substring(1);
}
//分片上传
PutObjectArgs objectArgs = PutObjectArgs.builder().object(objectName)
.bucket(bucketName)
.contentType(ContentType.contentType(""))
.stream(stream,stream.available(),-1).build();
minioClient.putObject(objectArgs);
stream.close();
file_url = bucketName+"/"+objectName;
}catch (Exception e){
log.error(e.getMessage(), e);
}
return file_url;
}
public static String merge(List<String> fileList,String objectName){
//获取需要合并的分片组装成ComposeSource
List<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());
for (String chunk : fileList){
sourceObjectList.add(
ComposeSource.builder()
.bucket(bucketName)
.object(chunk)
.build()
);
}
//合并分片
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(bucketName)
//合并后的文件的objectname
.object(objectName)
//指定源文件
.sources(sourceObjectList)
.build();
try {
minioClient.composeObject(composeObjectArgs);
} catch (ErrorResponseException e) {
e.printStackTrace();
} catch (InsufficientDataException e) {
e.printStackTrace();
} catch (InternalException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidResponseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (ServerException e) {
e.printStackTrace();
} catch (XmlParserException e) {
e.printStackTrace();
}
List<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());
//执行删除
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(collect)
.build();
Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//如果没有下面try的代码,文件史删除不了的,加上下面的代码就可以删除了
try{
for (Result<DeleteError> result : results){
DeleteError deleteError = result.get();
System.out.println("error in deleteing object"+deleteError.objectName()+";"+deleteError.message());
}
}catch (Exception e){
System.out.println("minio删除文件失败");
e.printStackTrace();
}
String file_url = bucketName+"/"+objectName;
return file_url;
}
主要的实现就是这些,其他辅助的工具类需要的留言