视频分块上传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-upload
						ref="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.file
  const 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 md5
		const md5 = await getFileMd5(chunk);
    	mergeVo.chunksMd5.push(md5);
		// 准备FormData
		const 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 {
	@Autowired
	private MediaFilesMapper mediaFilesMapper;
    
        /**
     * 分块文件上传
     * <br/>
     * 分块文件不存放mysql信息,同时文件名不含后缀,只有md5
     * @param file 文件
     * @param md5  md5
     * @return {@link Boolean}
     */
    @Override
    public Boolean handleChunkUpload(MultipartFile file, String md5) {
        //只上传至minio
        OssClient 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;
    }
    
	@Override
    public Boolean isChunkExist(String md5) {
        OssClient storage = OssFactory.instance();
        String path = getPathByMD5(md5, "");
        return storage.doesFileExist(minioProperties.getVideoBucket(), path);
    }

	@Override
    public 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);
            });
            //合并文件上传到minio
            String 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();
            }
        }
        //上传信息到mysql
        MediaFiles 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;
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++教学——从入门到精通 9.比大小

如果叫你比较a,b,c的大小并排序都会吧&#xff0c;先用我们学过的方法做 #include"iostream" using namespace std; int main(){int a,b,c;cin>>a>>b>>c;if(a>b&&a>c){if(b>c)cout<<c<<" "<<b;else…

Vue2电商前台项目(二):完成Home首页模块业务

一、项目开发的步骤 1、书写静态页面&#xff08;HTML&#xff0c;CSS&#xff09; 2、拆分组件 3、获取服务器的数据动态展示 4、完成相应的动态业务逻辑 经过分析之后&#xff0c;Home首页可以拆分为7个组件&#xff1a;TypeNav三级联动导航&#xff0c;ListContainer&…

先进电机技术 —— 无线电机

一、背景 无线电能传输电机是一种创新的电机设计&#xff0c;它结合了无线电能传输技术与传统的电机工作原理。这种电机的主要特点是通过无线方式传输电能&#xff0c;从而消除了传统电机中需要有线连接的限制&#xff0c;提高了系统的灵活性和可靠性。 无线电能传输技术主要…

C51实现每秒向电脑发送数据(UART的含义)

其实核心的问题是&#xff1a;串口的通信方式 异步串行是指UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;&#xff0c;UART包含TTL电平的串口和RS232电平的串口 UART要实现异步通信的&#xff1a; UART是异步串行接口&#xff0c;通信双方使用时…

LeetCode每日一题之专题一:双指针 ——快乐数

快乐数OJ链接&#xff1a;202. 快乐数 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 题目分析: 为了房便叙述&#xff0c;将「对于⼀个正整数&#xff0c;每⼀次将该数替换为它每个位置上的数字的平方和」这⼀个 操作记为 x 操作&#xff1b; 题目告诉我们&#…

Shell脚本之基础-2

目录 一、字符处理 cut命令 awk命令 sed命令 字符串排序 二、条件判断 文件类型判断 文件权限判断 两个文件的判断 整数比较 字符串判断 多重判断 三、流程控制 if分支 if else 双分支结构 case分支 for循环 while循环 一、字符处理 cut命令 命令格式&#x…

Python 金融数据分析工具库之zvt使用详解

​​​​​​​ 概要 Python在金融数据分析领域有着广泛的应用,而zvt库作为一款强大的金融数据分析工具,为开发者提供了丰富的功能和灵活的应用接口。本文将深入介绍zvt库的安装、特性、基本功能、高级功能、实际应用场景,并总结其在金融数据分析中的价值和优势。 安装 …

mysql故障排查

MySQL是目前企业最常见的数据库之一日常维护管理的过程中&#xff0c;会遇到很多故障汇总了常见的故障&#xff0c;MySQL默认配置无法满足高性能要求 一 MySQL逻辑架构图 客户端和连接服务核心服务功能存储擎层数据存储层 二 MySQL单实例常见故障 故障1 ERROR 2002 (HY000)…

(echarts)title和legend不重叠/legend图例滚动显示不换行

(echarts)title和legend不重叠/legend图例滚动显示不换行 title和legend都被放置在了不同的位置&#xff0c;从而避免了重叠。你可以根据实际的图表布局和需求调整left&#xff08;水平位置&#xff09;和top&#xff08;垂直位置&#xff09;等属性&#xff0c;确保它们不会相…

【SCI绘图】【箱型图系列1 python】多类对比及各类下属子类对比

SCI&#xff0c;CCF&#xff0c;EI以及核心期刊绘图宝典&#xff0c;爆款更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【箱型图系列1】多类对比各类下属子类对比 文末附带完整代码&#xff1a; 1.环境准备 python 3 from matplotlib import pyp…

QT-QPainter

QT-QPainter 1.QPainter画图  1.1 概述  1.1 QPainter设置  1.2 QPainter画线  1.3 QPainter画矩形  1.4 QPainter画圆  1.5 QPainter画圆弧  1.6 QPainter画扇形 2.QGradient  2.1 QLinearGradient线性渐变  2.2 QRadialGradient径向渐变  2.3 QConicalGr…

【Unity每日一记】如何从0到1将特效图集制作成一个特效

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

Prometheus+grafana环境搭建Nginx(docker+二进制两种方式安装)(六)

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

数据分析python代码——数据填充

在Python中&#xff0c;我们通常使用pandas库来处理和分析数据。数据填充是数据预处理的一个重要步骤&#xff0c;用于处理数据中的缺失值。以下是使用pandas库进行数据填充的示例代码&#xff1a; 在数据分析中&#xff0c;处理缺失值&#xff08;空值&#xff09;是一个重要…

基于微信小程序的实验室预约系统的设计与开发

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

c语言文件操作(超详细)

前言 这次的博客&#xff0c;可以让大家快速掌握文件操作&#xff0c;方便大家快速找到不懂的内容 文件操作的作用以及基础 1. 为什么使用文件&#xff1f; 如果没有文件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&…

(arxiv2401) CrossMAE

作者团队来自加州大学伯克利分校&#xff08;UC Berkeley&#xff09;和加州大学旧金山分校&#xff08;UCSF&#xff09;。论文主要探讨了在MAE的解码中&#xff0c;图像patch之间的依赖性&#xff0c;并提出了一种新的预训练框架 CrossMAE。 论文的主要贡献包括&#xff1a; …

代码随想录-算法训练营day02【滑动窗口、螺旋矩阵】

专栏笔记&#xff1a;https://blog.csdn.net/weixin_44949135/category_10335122.html https://docs.qq.com/doc/DUGRwWXNOVEpyaVpG?uc71ed002e4554fee8c262b2a4a4935d8977.有序数组的平方 &#xff0c;209.长度最小的子数组 &#xff0c;59.螺旋矩阵II &#xff0c;总结 建议…

(源码+部署+讲解)基于Spring Boot和Vue的大学生快递代取服务平台的设计与实现

一、引言 本报告旨在详细阐述基于Spring Boot后端框架和Vue前端框架的大学生快递代取服务平台的设计与实现过程。该平台旨在为大学生提供便捷的快递代取服务&#xff0c;解决因时间冲突或距离过远而无法及时取件的问题。通过该平台&#xff0c;用户可以发布代取需求&#xff0c…

[中级]软考_软件设计_计算机组成与体系结构_07_存储系统

存储系统 层次划存储概念图局促性原理分类存储器位置存取方式按内容存储按地址存储 工作方式拓展 往年真题 高速缓存(cache)概念案例解析&#xff1a;求取平均时间 Cache与主存的地址映射映像往年真题 主存编制计算编址大小的求取编址与计算存储单元编址内容总容量求取例题解析…