视频分块上传Vue3+SpringBoot3+Minio

文章目录

  • 一、简化演示
      • 分块上传、合并分块
      • 断点续传
      • 秒传
  • 二、更详细的逻辑和细节问题
      • 可能存在的隐患
  • 三、代码示例
      • 前端代码
      • 后端代码

一、简化演示

分块上传、合并分块

前端将完整的视频文件分割成多份文件块,依次上传到后端,后端将其保存到文件系统。前端将文件块上传完毕后,发送合并请求,后端拿取文件块,合并后重新上传到文件系统。
在这里插入图片描述

断点续传

前端遍历文件块,每次上传之前,先询问文件块是否存在,只有不存在的情况下,才会上传。
请添加图片描述

秒传

前端分割视频文件前,先询问此视频是否已经存在,存在则不再上传,后端之间返回视频信息。前端看起来就像是被秒传了。
请添加图片描述

二、更详细的逻辑和细节问题

  • 视频文件和文件块都通过文件本身计算MD5值作为唯一标志
  • 文件系统使用Minio,只要提供buckerNamepath就可以操作文件
  • 后端合并文件块成功后会删除文件块,并以MD5值为id存入数据库
  • Minio存储文件块时,依据其md5值计算path,比如取前两个字符构建二级文件夹,文件名为md5值,无后缀。所以只需要提供文件块的md5值就可以操作文件块。
  • Minio存储完整视频文件时,依据其md5值计算path,同上,文件名为md5值,携带.mp4等后缀,所以只需要提供视频文件的md5值就可以操作视频文件。
  1. 首先,前端计算视频文件的MD5值,记为fileMd5,传递MD5值来询问后端此视频文件是否存在,后端查询数据库返回结果,如果存在,则前端触发“秒传”。
  2. 如果不存在,则将视频文件分割成文件块,循环上传,每次循环,首先计算文件块的md5值,传递md5值询问后端此文件块是否存在,后端根据md5判断文件块是否存在,如果存在,前端跳过此文件块上传,直接标记为上传成功,如果不存在,则上传至后端,后端将其保存到minio。这其实就是“分块上传,断点续传”。
  3. 最后所有分块文件都上传成功,前端发起合并请求,传递视频文件的md5值和所有文件块的md5值到后端,后端进行文件块合并、文件块的删除、合并文件的上传,将信息存储在mysql数据库,将执行结果告知前端。这就是“合并分块”

可能存在的隐患

一个视频文件的文件块没有全部上传完成就终止,此时文件块将一直保存在minio中,如果之后此视频再也没有发起过上传请求,那么这些文件块都是是一种垃圾。

可以写一个定时任务,遍历Minio没有后缀的文件块,判断其创建时间距离当前是否足够久,是则删除。

三、代码示例

前端代码

<template><div class="p-2"><el-button icon="Plus" plain type="primary" @click="handleAdd">新增</el-button><!-- 添加或修改media对话框 --><el-dialog v-model="dialog.visible" :title="dialog.title" append-to-body width="500px"><el-form ref="mediaFormRef" :model="form" :rules="rules" label-width="80px"><el-form-item label="上传视频" prop="originalName" v-show="dialog.title=='添加视频'"><el-uploadref="uploadRef":http-request="onUpload":before-upload="beforeUpload":limit="1"action="#"class="upload-demo"><template #trigger><el-button type="primary">选择视频</el-button></template><template #tip><div class="el-upload__tip">支持分块上传、端点续传</div></template></el-upload></el-form-item><el-form-item v-show="percentageShow"><el-progress :percentage="percentage" style="width: 100%"/></el-form-item></el-form></el-dialog></div>
</template><script lang="ts" name="Media" setup>
import type {UploadInstance, UploadRawFile, UploadRequestOptions, UploadUserFile} from 'element-plus'
import SparkMD5 from "spark-md5";
import {HttpStatus} from "@/enums/RespEnum";const dialog = reactive<DialogOption>({visible: false,title: ''
});
//上传视频
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + "/media/media/image"); // 上传的图片服务器地址
const uploadRef = ref<UploadInstance>()
const needUpload = ref(true)
const chunkSize = 5*1024*1024;const percentage = ref(0)
const percentageShow = ref(false)/** 新增按钮操作 */
const handleAdd = () => {dialog.visible = true;dialog.title = "添加视频";percentageShow.value = false;
}//获取文件的MD5
const getFileMd5 = (file:any) => {return new Promise((resolve, reject) => {let fileReader = new FileReader()fileReader.onload = function (event) {let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)resolve(fileMd5)}fileReader.readAsArrayBuffer(file)})
}//在上传之前,使用视频md5判断视频是否已经存在
const beforeUpload = async (rawFile: UploadRawFile) => {needUpload.value = true;const fileMd5 = await getFileMd5(rawFile);form.value.id = fileMd5;const rsp = await getMedia(fileMd5);if(!!rsp.data && rsp.data['id'] == fileMd5){needUpload.value = false;proxy?.$modal.msgWarning("视频文件已存在,请勿重复上传。文件名为"+rsp.data['originalName'])}
}//分块上传、合并分块
const onUpload = async (options: UploadRequestOptions) => {if(!needUpload.value){//秒传percentageShow.value = true;percentage.value = 100;dialog.visible = false;return;}percentageShow.value = true;const file = options.fileconst totalChunks = Math.ceil(file.size / chunkSize);let isUploadSuccess = true;//记录分块文件是否上传成功//合并文件参数let mergeVo = {"chunksMd5": [] as string[],"videoMd5": undefined as string | undefined,"videoName": file.name,"videoSize": file.size,"remark": undefined as string | undefined}//循环切分文件,并上传分块文件for(let i=0; i<totalChunks; ++i){const start = i * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);//计算 chunk md5const md5 = await getFileMd5(chunk);mergeVo.chunksMd5.push(md5);// 准备FormDataconst formData = new FormData();formData.append('file', chunk);formData.append('filename', file.name);formData.append('chunkIndex', i.toString());formData.append('totalChunks', totalChunks.toString());formData.append('md5', md5);//上传当前分块try {//先判断这个分块是否已经存在const isExistRsp = await isChunkExist({"md5": formData.get("md5")});const isExist = isExistRsp.data;//不存在则上传if (!isExist){const rsp = await addChunk(formData);console.log(`Chunk ${i + 1}/${totalChunks} uploaded`, rsp.data);}else {console.log(`Chunk ${i + 1}/${totalChunks} is exist`);}percentage.value = (i)*100 / totalChunks;} catch (error) {isUploadSuccess = false;console.error(`Error uploading chunk ${i + 1}`, error);proxy?.$modal.msgError(`上传分块${i + 1}出错`);break;}}//合并分块文件if(isUploadSuccess){proxy?.$modal.msgSuccess("分块文件上传成功")mergeVo.videoMd5 = form.value.id;//beforeUpload已经计算过视频文件的md5//合并文件const rsp = await mergeChunks(mergeVo);if (rsp.code == HttpStatus.SUCCESS){//合并文件后,实际上媒资已经插入数据库。percentage.value = 100;proxy?.$modal.msgSuccess("文件合并成功")proxy?.$modal.msgSuccess("视频上传成功")}else{proxy?.$modal.msgSuccess("文件合并异常")}}else {proxy?.$modal.msgSuccess("文件未上传成功,请重试或联系管理员")}
}</script>
export const getMedia = (id: string | number): AxiosPromise<MediaVO> => {return request({url: '/media/media/' + id,method: 'get'});
};/*** 分块文件是否存在* */
export const isChunkExist = (data: any) => {return request({url: '/media/media/video/chunk',method: 'get',params: data});
};/*** 上传分块文件* */
export const addChunk = (data: any) => {return request({url: '/media/media/video/chunk',method: 'post',data: data});
};/*** 合并分块文件* */
export const mergeChunks = (data: any) => {return request({url: '/media/media/video/chunk/merge',method: 'post',data: data});
};

后端代码

@RestController
@RequestMapping("/media")
public class MediaFilesController extends BaseController {/*** 获取media详细信息** @param id 主键*/@GetMapping("/{id}")public R<MediaFilesVo> getInfo(@NotNull(message = "主键不能为空")@PathVariable String id) {return R.ok(mediaFilesService.queryById(id));}@Log(title = "视频分块文件上传")@PostMapping(value = "/video/chunk")public R<String> handleChunkUpload(@RequestParam("file") MultipartFile file,@RequestParam("md5") String md5,@RequestParam("filename") String filename,@RequestParam("chunkIndex") int chunkIndex,@RequestParam("totalChunks") int totalChunks) {if (ObjectUtil.isNull(file)) {return R.fail("上传文件不能为空");}Boolean b = mediaFilesService.handleChunkUpload(file, md5);if (b){return R.ok();}else {return R.fail();}}@Log(title = "分块文件是否已经存在")@GetMapping(value = "/video/chunk")public R<Boolean> isChunkExist(@RequestParam("md5") String md5) {return R.ok(mediaFilesService.isChunkExist(md5));}@Log(title = "合并视频文件")@PostMapping(value = "/video/chunk/merge")public R<Boolean> mergeChunks(@RequestBody MediaVideoMergeBo bo) {bo.setCompanyId(LoginHelper.getDeptId());Boolean b = mediaFilesService.mergeChunks(bo);if (b){return R.ok();}else {return R.fail();}}
}

关于如何操作Minio等文件系统,不详细写明解释。只需要知道,给Minio提供文件本身、bucketName、path即可完成上传、下载、删除等操作。具体代码不同的包都不一样。

@Service
public class MediaFilesServiceImpl implements MediaFilesService {@Autowiredprivate MediaFilesMapper mediaFilesMapper;/*** 分块文件上传* <br/>* 分块文件不存放mysql信息,同时文件名不含后缀,只有md5* @param file 文件* @param md5  md5* @return {@link Boolean}*/@Overridepublic Boolean handleChunkUpload(MultipartFile file, String md5) {//只上传至minioOssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");try {storage.upload(file.getInputStream(), path, file.getContentType(), minioProperties.getVideoBucket());} catch (IOException e) {throw new RuntimeException(e);}return true;}@Overridepublic Boolean isChunkExist(String md5) {OssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");return storage.doesFileExist(minioProperties.getVideoBucket(), path);}@Overridepublic Boolean mergeChunks(MediaVideoMergeBo bo) {OssClient storage = OssFactory.instance();String originalfileName = bo.getVideoName();String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());//创建临时文件,用来存放合并文件String tmpDir = System.getProperty("java.io.tmpdir");String tmpFileName = UUID.randomUUID().toString() + ".tmp";File tmpFile = new File(tmpDir, tmpFileName);try(FileOutputStream fOut = new FileOutputStream(tmpFile);) {//将分块文件以流的形式copy到临时文件List<String> chunksMd5 = bo.getChunksMd5();chunksMd5.forEach(chunkMd5 -> {String chunkPath = getPathByMD5(chunkMd5, "");InputStream chunkIn = storage.getObjectContent(minioProperties.getVideoBucket(), chunkPath);IoUtil.copy(chunkIn, fOut);});//合并文件上传到minioString videoMd5 = bo.getVideoMd5();String path = getPathByMD5(videoMd5, suffix);storage.upload(tmpFile, path, minioProperties.getVideoBucket());//删除分块文件chunksMd5.forEach(chunkMd5->{String chunkPath = getPathByMD5(chunkMd5, "");storage.delete(chunkPath, minioProperties.getVideoBucket());});} catch (Exception e) {throw new RuntimeException(e);}finally {if (tmpFile.exists()){tmpFile.delete();}}//上传信息到mysqlMediaFiles mediaFiles = new MediaFiles();mediaFiles.setId(bo.getVideoMd5());mediaFiles.setCompanyId(bo.getCompanyId());mediaFiles.setOriginalName(originalfileName);mediaFiles.setFileSuffix(suffix);mediaFiles.setSize(bo.getVideoSize());mediaFiles.setPath(getPathByMD5(bo.getVideoMd5(), suffix));mediaFiles.setRemark(bo.getRemark());mediaFiles.setAuditStatus(MediaStatusEnum.UNREVIEWED.getValue());return mediaFilesMapper.insert(mediaFiles) > 0;}/*** 通过md5生成文件路径* <br/>* 比如* md5 = 6c4acb01320a21ccdbec089f6a9b7ca3* <br/>* path = 6/c/md5 + suffix* @param prefix 前缀* @param suffix 后缀* @return {@link String}*/public String getPathByMD5(String md5, String suffix) {// 文件路径String path = md5.charAt(0) + "/" + md5.charAt(1) + "/" + md5;return path + suffix;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/298501.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

6、【单例模式】确保了一个类在程序运行期间只有一个实例

你好&#xff0c;我是程序员雪球 在软件设计中&#xff0c;单例模式是一种常见的设计模式。它确保了一个类在程序运行期间只有一个实例&#xff0c;并提供了全局访问该实例的方式。单例模式在许多场景中都有广泛的应用&#xff0c;例如共享资源管理、数据库连接、日志记录器等…

多线程3

线程安全 线程可能会出现这些情况 导致两个线程不能达到自己想要去循环的次数&#xff0c;可能两个线程各10000&#xff0c;那么他们就会出现不到5000甚至不到5000的情况。 出现线程的不安全原因&#xff1a; 1.线程在系统中是随机调度,抢占式执行的.[线程不安全的, 罪魁祸首…

考研回忆录【二本->211】

备考时长差不多快一年半&#xff0c;从22年的11月底开始陆陆续续地准备考研&#xff0c;因为开始的早所以整个备考过程显得压力不是很大&#xff0c;中途还去一些地方旅游&#xff0c;我不喜欢把自己绷得太紧。虽然考的不是很好&#xff0c;考完我甚至都没准备复试&#xff0c;…

【软件工程】详细设计(一)

1. 引言 1.1 编写目的 该文档的目的是描述《学生成绩管理系统》项目的详细设计&#xff0c;其主要内容包括&#xff1a; 系统功能简介 系统详细设计简述 各个模块的实现逻辑 最小模块组件的伪代码 本文档的预期的读者是&#xff1a; 开发人员 项目管理人员 测试人员 …

docker容器技术篇:Docker API配置与常用操作

docker容器技术篇&#xff1a;Docker API配置与使用 一、API具体是什么&#xff1f; 百科解释应用程序接口&#xff08;API&#xff09;&#xff0c;又称为应用编程接口&#xff0c;就是软件系统不同组成部分衔接的约定&#xff0c;蒙了吧&#xff01;&#xff01;&#xff0…

解决沁恒ch592单片机在tmos中使用USB总线时,接入USB Hub无法枚举频繁Reset的问题

开发产品时采用了沁恒ch592&#xff0c;做USB开发时遇到了一个奇葩的无法枚举问题。 典型症状 使用USB线直连电脑时没有问题&#xff0c;可以正常使用。 如果接入某些特定方案的USB Hub&#xff08;例如GL3510、GL3520&#xff09;&#xff0c;可能会出现以下2种情况&#xf…

【NLP练习】中文文本分类-Pytorch实现

中文文本分类-Pytorch实现 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、准备工作 1. 任务说明 本次使用Pytorch实现中文文本分类。主要代码与文本分类代码基本一致&#xff0c;不同的是本次任务使用…

MyBatis 解决上篇的参数绑定问题以及XML方式交互

前言 上文:MyBatis 初识简单操作-CSDN博客 上篇文章我们谈到的Spring中如何使用注解对Mysql进行交互 但是我们发现我们返回出来的数据明显有问题 我们发现后面三个字段的信息明显没有展示出来 下面我们来谈谈解决方案 解决方案 这里的原因本质上是因为mysql中和对象中的字段属性…

【微服务】------核心组件架构选型

1.微服务简介 微服务架构&#xff08;Microservice Architecture&#xff09;是一种架构概念&#xff0c;旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦&#xff0c;从而降低系统的耦合性&#xff0c;并提供更加灵活的服务支持。 2.微服务技术选型 区域内容…

【零基础学数据结构】顺序表实现书籍存储

目录 书籍存储的实现规划 ​编辑 前置准备&#xff1a; 书籍结构体&#xff1a; 书籍展示的初始化和文件加载 书籍展示的销毁和文件保存 书籍展示的容量检查 书籍展示的尾插实现 书籍展示的书籍增加 书籍展示的书籍打印 书籍删除展示数据 书籍展示修改数据 在指定位置之前…

2024年第八届人工智能与虚拟现实国际会议(AIVR 2024)即将召开!

2024年第八届人工智能与虚拟现实国际会议&#xff08;AIVR 2024&#xff09;将2024年7月19-21日在日本福冈举行。人工智能与虚拟现实的发展对推动科技进步、促进经济发展、提升人类生活质量等具有重要意义。AIVR 2024将携手各专家学者&#xff0c;共同挖掘智能与虚拟的无限可能…

加速度:电子元器件营销网站的功能和开发周期

据工信部预计&#xff0c;到2023年&#xff0c;我国电子元器件销售总额将达到2.1万亿元。随着资本的涌入&#xff0c;在这个万亿级赛道&#xff0c;市场竞争变得更加激烈的同时&#xff0c;行业数字化发展已是大势所趋。电子元器件B2B商城平台提升数据化驱动能力&#xff0c;扩…

【机器学习】如何通过群体智慧解决机器学习的挑战“

机器学习的发展日新月异&#xff0c;但其成功实施的关键之一仍然是获取高质量的、标注良好的数据集。在这篇文章中&#xff0c;我们将探讨如何通过群体智慧来构建和改善机器学习的数据集&#xff0c;尤其是通过reCAPTCHA和带有目的的游戏&#xff08;Games with a Purpose, GWA…

齐护机器人方位传感器指南针罗盘陀螺仪

一、方位传感器原理及功能说明 齐护方位传感器是一款集成了三轴磁传感器芯片的方位传感器模块。适用于无人机、机器人、移动和个人手持设备中的罗盘&#xff08;指南针&#xff09;、导航和游戏等高精度应用。模块可以感应XYZ平面角度外&#xff0c;还可实现1至2的水平面角度罗…

Python | Leetcode Python题解之第10题正则表达式匹配

题目&#xff1a; 题解&#xff1a; class Solution:def isMatch(self, s: str, p: str) -> bool:m, n len(s), len(p)dp [False] * (n1)# 初始化dp[0] Truefor j in range(1, n1):if p[j-1] *:dp[j] dp[j-2]# 状态更新for i in range(1, m1):dp2 [False] * (n1) …

Transformer位置编码详解

在处理自然语言时候&#xff0c;因Transformer是基于注意力机制&#xff0c;不像RNN有词位置顺序信息&#xff0c;故需要加入词的位置信息来显示的表明词的上下文关系。具体是将词经过位置编码(positional encoding)&#xff0c;然后与emb词向量求和&#xff0c;作为编码块(Enc…

备考2024年思维100春季线上比赛?来做做官方模拟题(附答案)

2024年春季思维100活动第一阶段线上比赛&#xff08;4月20日&#xff0c;星期六&#xff0c;上午&#xff09;的报名正在进行中&#xff0c;更多安排和需要提前了解的关键点可以见我前面写的文章&#xff0c;或者直接联系我获取相关资料。 【提醒】2024年春季的思维100在线比赛…

递归算法解读

递归&#xff08;Recursion&#xff09;是计算机科学中的一个重要概念&#xff0c;它指的是一个函数&#xff08;或过程&#xff09;在其定义中直接或间接地调用自身。递归函数通过把问题分解为更小的相似子问题来解决原问题&#xff0c;这些更小的子问题也使用相同的解决方案&…

ClickHouse笔记

1. 简介 开发背景: ClickHouse 由 Yandex 于 2016 年开源&#xff0c;目的是提供高性能的 OLAP 解决方案。性能: ClickHouse 能够以极高的速度处理大量数据&#xff0c;每秒可以处理数亿到十亿多行数据。架构: 它使用 C 编写&#xff0c;提供丰富的数据类型、数据库引擎和表引…

深度学习方法;乳腺癌分类

乳腺癌的类型很多&#xff0c;但大多数常见的是浸润性导管癌、导管原位癌和浸润性小叶癌。浸润性导管癌(IDC)是最常见的乳腺癌类型。这些都是恶性肿瘤的亚型。大约80%的乳腺癌是浸润性导管癌(IDC)&#xff0c;它起源于乳腺的乳管。 浸润性是指癌症已经“侵袭”或扩散到周围的乳…