视频分块上传Vue3+SpringBoot3+Minio
文章目录
- 一、简化演示
- 分块上传、合并分块
- 断点续传
- 秒传
- 二、更详细的逻辑和细节问题
- 可能存在的隐患
- 三、代码示例
- 前端代码
- 后端代码
一、简化演示
分块上传、合并分块
前端将完整的视频文件分割成多份文件块,依次上传到后端,后端将其保存到文件系统。前端将文件块上传完毕后,发送合并请求,后端拿取文件块,合并后重新上传到文件系统。
断点续传
前端遍历文件块,每次上传之前,先询问文件块是否存在,只有不存在的情况下,才会上传。
秒传
前端分割视频文件前,先询问此视频是否已经存在,存在则不再上传,后端之间返回视频信息。前端看起来就像是被秒传了。
二、更详细的逻辑和细节问题
- 视频文件和文件块都通过文件本身计算
MD5值
作为唯一标志 - 文件系统使用
Minio
,只要提供buckerName
和path
就可以操作文件 - 后端合并文件块成功后会删除文件块,并以
MD5
值为id
存入数据库 Minio
存储文件块时,依据其md5
值计算path
,比如取前两个字符构建二级文件夹,文件名为md5
值,无后缀。所以只需要提供文件块的md5
值就可以操作文件块。Minio
存储完整视频文件时,依据其md5
值计算path
,同上,文件名为md5
值,携带.mp4
等后缀,所以只需要提供视频文件的md5
值就可以操作视频文件。
- 首先,前端计算视频文件的
MD5
值,记为fileMd5
,传递MD5值来询问后端此视频文件是否存在,后端查询数据库返回结果,如果存在,则前端触发“秒传”。 - 如果不存在,则将视频文件分割成文件块,循环上传,每次循环,首先计算文件块的
md5
值,传递md5值询问后端此文件块是否存在,后端根据md5
判断文件块是否存在,如果存在,前端跳过此文件块上传,直接标记为上传成功,如果不存在,则上传至后端,后端将其保存到minio
。这其实就是“分块上传,断点续传”。 - 最后所有分块文件都上传成功,前端发起合并请求,传递视频文件的
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;}}
相关文章:

视频分块上传Vue3+SpringBoot3+Minio
文章目录 一、简化演示分块上传、合并分块断点续传秒传 二、更详细的逻辑和细节问题可能存在的隐患 三、代码示例前端代码后端代码 一、简化演示 分块上传、合并分块 前端将完整的视频文件分割成多份文件块,依次上传到后端,后端将其保存到文件系统。前…...

深入浅出 -- 系统架构之单体到分布式架构的演变
一、传统模式的技术改革 在很多年以前,其实没有严格意义上的前后端工程师之分,每个后端就是前端,同理,前端也可以是后端,即Ajax、jQuery技术未盛行前的年代。 起初,大部分前端界面很简单,显示的…...
每日一题 第七十期 洛谷 [蓝桥杯 2020 省 AB2] 回文日期
[蓝桥杯 2020 省 AB2] 回文日期 题目描述 2020 年春节期间,有一个特殊的日期引起了大家的注意:2020 年 2 月 2 日。因为如果将这个日期按 yyyymmdd 的格式写成一个 8 8 8 位数是 20200202,恰好是一个回文数。我们称这样的日期是回文日期。…...
蓝桥杯第十四届C++A组(未完)
【规律题】平方差 题目描述 给定 L, R,问 L ≤ x ≤ R 中有多少个数 x 满足存在整数 y,z 使得 。 输入格式 输入一行包含两个整数 L, R,用一个空格分隔。 输出格式 输出一行包含一个整数满足题目给定条件的 x 的数量。 样例输入 1 5 样例输出 …...

职场口才提升之道
职场口才提升之道 在职场中,口才的重要性不言而喻。无论是与同事沟通协作,还是向上级汇报工作,亦或是与客户洽谈业务,都需要具备良好的口才能力。一个出色的职场人,除了拥有扎实的专业技能外,还应具备出色…...
【算法练习】28:选择排序学习笔记
一、选择排序的算法思想 弄懂选择排序算法,先得知道两个概念:未排序序列,已排序序列。 原理:以升序为例,选择排序算法的思想是,先将整个序列当做未排序的序列,以序列的第一个元素开始。然后从左…...

【关于窗口移动求和的两种计算方法】
窗口移动计算方法 例子方法1方法2运行结果: 例子 在很多算法中都会涉及到窗口滑动,比如基于新息序列更新的自适应卡尔曼滤波器算法中便会使用到。 已知一个数列:OCV [1;2;3;4;5;6;7;8;9;10;11;12;13;14;15],定义窗口长度为5,每次…...

Win10文件夹共享(有密码的安全共享)(SMB协议共享)
前言 局域网内(无安全问题,比如自己家里wifi)无密码访问,参考之前的操作视频 【电脑文件全平台共享、播放器推荐】手机、电视、平板播放硬盘中的音、视频资源 下面讲解公共网络如办公室网络、咖啡厅网络等等环境下带密码的安全…...
Client sent an HTTP request to an HTTPS server
背景 最近踩坑了 我发现域名:8000可以访问我的服务 但是域名:443却不行,这很反常 结果发现是nginx配置的问题,需要把http改成https! 原因 如果你的后端服务(运行在8000端口上)已经配置了SS…...

Springboot传参要求
Web.java(这里定义了一个实体类交Web) public class Web{ private int Page; public int getPage() {return Page;}public void setPage(int page) {Page page;} } 1、通过编译器自带的getter、Setter传参 。只是要注意参数的名字是固定的,不能灵活改变。 传参的…...

数字乡村创新实践探索:科技赋能农业现代化与乡村治理体系现代化同步推进
随着信息技术的飞速发展,数字乡村作为乡村振兴的重要战略方向,正日益成为推动农业现代化和乡村治理体系现代化的关键力量。科技赋能下的数字乡村,不仅提高了农业生产的效率和品质,也为乡村治理带来了新的机遇和挑战。本文旨在探讨…...
C语言——找单身狗1
题目描述: 在一个整形数组中,只有一个数字出现一次,其他数组都是成对出现的,找出那个只出现一次的数字。 例如: 数组中:1,2,3,4,5,4,3…...

Day82:服务攻防-开发组件安全Solr搜索Shiro身份Log4j日志本地CVE环境复现
目录 J2EE-组件Solr-本地demo&CVE 命令执行(CVE-2019-17558) 远程命令执行漏洞(CVE-2019-0193) Apache Solr 文件读取&SSRF (CVE-2021-27905) J2EE-组件Shiro-本地demo&CVE CVE_2016_4437 Shiro-550Shiro-721(RCE) CVE-2020-11989(身…...

网络协议——VRRP(虚拟路由冗余协议)原理与配置
1. VRRP概述 单网关出现故障后下联业务中断,配置两个及以上的网关时由于IP地址冲突,导致通讯时断时续甚至通信中断。VRRP组播类的网络层协议 2. 协议版本 VRRP v2: 支持认证,仅适用于IPv4网络 VRRP v3: 不支持认证, 适用于IPv4和IPv6两种网…...

Elasticsearch:我们如何演化处理二进制文档格式
作者:来自 Elastic Sean Story 从二进制文件中提取内容是一个常见的用例。一些 PDF 文件可能非常庞大 — 考虑到几 GB 甚至更多。Elastic 在处理此类文档方面已经取得了长足的进步,今天,我们很高兴地介绍我们的新工具 —— 数据提取服务&…...

第八讲 Sort Aggregate 算法
我们现在将讨论如何使用迄今为止讨论过的 DBMS 组件来执行查询。 1 查询计划【Query Plan】 我们首先来看当一个查询【Query】被解析【Parsed】后会发生什么? 当 SQL 查询被提供给数据库执行引擎,它将通过语法解析器进行检查,然后它会被转换…...

clickhouse MPPDB数据库--新特性使用示例
clickhouse 新特性: 从clickhouse 22.3至最新的版本24.3.2.23,clickhouse在快速发展中,每个版本都增加了一些新的特性,在数据写入、查询方面都有性能加速。 本文根据clickhouse blog中的clickhouse release blog中,学…...

MATLAB多级分组绘图及图例等细节处理 ; MATLAB画图横轴时间纵轴数值按照不同sensorCode分组画不同sensorCode的曲线
平时研究需要大量的绘图Excel有时候又臃肿且麻烦 尤其是当处理大量数据时可能会拖死Windows 示例代码及数据量展示 因为数据量是万级别的折线图也变成"柱状图"了, 不过还能看出大致趋势! 横轴是时间纵轴是传感器数值图例是传感器所在深度 % data readtable(C:\U…...

20240405,数据类型,运算符,程序流程结构
是我深夜爆炸,不能再去补救C了,真的来不及了,不能再三天打鱼两天晒网了,真的来不及了呜呜呜呜 我实在是不知道看什么课,那黑马吧……MOOC的北邮的C正在进行呜呜 #include <iostream> using namespace std; int…...

Prometheus+grafana环境搭建Nginx(docker+二进制两种方式安装)(六)
由于所有组件写一篇幅过长,所以每个组件分一篇方便查看,前五篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabbitmq(docker二进制两种方式安装)(二)-CSDN博客 Prometheusgrafana环…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
C++.OpenGL (20/64)混合(Blending)
混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...

【Veristand】Veristand环境安装教程-Linux RT / Windows
首先声明,此教程是针对Simulink编译模型并导入Veristand中编写的,同时需要注意的是老用户编译可能用的是Veristand Model Framework,那个是历史版本,且NI不会再维护,新版本编译支持为VeriStand Model Generation Suppo…...

rknn toolkit2搭建和推理
安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 ,不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源(最常用) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...