视频分块上传Vue3+SpringBoot3+Minio

news2024/11/17 2:54:02

文章目录

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

一、简化演示

分块上传、合并分块

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

断点续传

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

秒传

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

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

  • 视频文件和文件块都通过文件本身计算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;
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UNIAPP(小程序)每十个文章中间一个广告

三十秒刷新一次广告 ad-intervals"30" <template><view style"margin: 30rpx;"><view class"" v-for"(item,index) in 100"><!-- 广告 --><view style"margin-bottom: 20rpx;" v-if"(inde…

Kafka参数介绍

官网参数介绍:Apache KafkaApache Kafka: A Distributed Streaming Platform.https://kafka.apache.org/documentation/#configuration

深入浅出 -- 系统架构之分布式常见理论概念

随着计算机科学和互联网的发展&#xff0c;分布式场景变得越来越常见&#xff0c;能否处理好分布式场景下的问题&#xff0c;成为衡量一个工程师是否合格的标准。本文我们介绍下分布式系统相关的理论知识&#xff0c;这些理论是我们理解和处理分布式问题的基础。 CAP理论 CAP…

小林coding图解计算机网络|TCP篇06|如何理解TCP面向字节流协议、为什么UDP是面向报文的协议、如何解决TCP的粘包问题?

小林coding网站通道&#xff1a;入口 本篇文章摘抄应付面试的重点内容&#xff0c;详细内容还请移步&#xff1a;小林coding网站通道 文章目录 如何理解UDP 是面向报文的协议如何理解字节流如何解决粘包固定长度的消息 特殊字符作为边界自定义消息结构 如何理解UDP 是面向报文的…

代码随想录算法训练营第三十一天| 理论基础、LeetCode 455.分发饼干、376. 摆动序列、53. 最大子序和

一、理论基础 文章讲解&#xff1a;https://programmercarl.com/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 1.贪心的定义 贪心的本质是选择每一阶段的局部最优解&#xff0c;从而达到全局最优解。例如&#xff0c;有一堆钞票&#xff0c…

使用 ChatGPT 创建在线课程:一步一步指南与提示模板

原文&#xff1a;Creating Online Courses with ChatGPT 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 谢谢 作为对你支持的感谢&#xff0c;随意定制本书中列出的任何提示&#xff0c;并将其作为你自己的重新销售。是的&#xff0c;对你免费。 它们都结构良好且用…

移动开发技术历史演化简介h5,跨平台,原生的各种技术实现方案的简单介绍

移动端的开发技术是指针对移动设备如智能手机和平板电脑等便携终端进行应用程序和服务创建的过程。本文将主要介绍一下移动端的开发技术的历史进化历程。讲述h5&#xff0c;跨平台&#xff0c;原生的各种技术实现方案和他们各自的优势与不足。 移动开发&#xff0c;不仅是编程技…

自动化测试框架Robot Framework入门

什么是RF RF是一个基于 Python 的、可扩展的关键字驱动的自动化 验收测试框架、验收测试驱动开发 &#xff08;ATDD&#xff09;、 行为驱动开发 &#xff08;BDD&#xff09; 和机器人流程自动化 &#xff08;RPA&#xff09;。它 可用于分布式、异构环境&#xff0c;其中自动…

Day82:服务攻防-开发组件安全Solr搜索Shiro身份Log4j日志本地CVE环境复现

目录 J2EE-组件Solr-本地demo&CVE 命令执行&#xff08;CVE-2019-17558&#xff09; 远程命令执行漏洞(CVE-2019-0193) Apache Solr 文件读取&SSRF (CVE-2021-27905) J2EE-组件Shiro-本地demo&CVE CVE_2016_4437 Shiro-550Shiro-721(RCE) CVE-2020-11989(身…

macU盘在电脑上读不出来 u盘mac读不出来怎么办 macu盘不能写入

对于Mac用户来说&#xff0c;使用U盘是很常见的操作&#xff0c;但有时候可能会遇到Mac电脑无法读取U盘的情况&#xff0c;这时候就需要使用一些特定的工具软件来帮助我们解决问题。本文就来告诉大家macU盘在电脑上读不出来是怎么回事&#xff0c;u盘mac读不出来怎么办。 一、m…

Java 中 Spring Boot 框架下的 Email 开发

Email 开发 1. 核心依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId> </dependency><dependency><groupId>org.springframework.boot</groupId><…

ubuntu安装sublime3并设置中文

安装Sublime Text 3 在Ubuntu上安装Sublime Text 3可以通过以下步骤进行&#xff1a; 打开终端。 导入Sublime Text 3的GPG密钥&#xff1a; wget -qO- https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key add - 添加Sublime Text 3的存储库&#xff1a; …

Spring Boot 相关知识和工具类

写在前面 此文是对后端开发框架Spring Boot快速入门一文的知识点补充与完善&#xff0c;如果是新手小白建议两篇文章一起食用,上面那篇文章为主&#xff0c;本文为辅&#xff0c;以达到最佳效果&#xff0c;大佬随意。 http 五种与后端的交互方法 Get:主要用于请求数据。当客…

vue2+elementUi的两个el-date-picker日期组件进行联动

vue2elementUi的两个el-date-picker日期组件进行联动 <template><el-form><el-form-item label"起始日期"><el-date-picker v-model"form.startTime" change"startTimeChange" :picker-options"startTimePickerOption…

Map源码解析

基本介绍 其实HashMap底层是个什么东西我们之前也讲过, 就是一个哈希桶(差不多可以看成一个数组), 然后每一个节点又连接着链表/红黑树之类的, 下面让我们看一看具体在源码上是怎样实现的: 常量及其它 -> static final int DEFAULT_INITIAL_CAPACITY 1 << 4; //这个…

解决JavaWeb中IDEA2023新版本无法创建Servlet的问题

出现问题&#xff1a;IDEA右键创建Servlet时&#xff0c;找不到选项 原因分析&#xff1a;IDEA的2023版的已经不支持Servlet了&#xff0c;如果还要使用的话&#xff0c;需要自己创建模板使用 创建模板 右击设置&#xff0c;选择&#xff08;File and Code Templates&#x…

Oracle APEX 23.2版本 使用应用程序工作副本进行协作开发

现状描述&#xff1a; 当前APEX协作开发都是在同一应用程序下进行的&#xff0c;这样做有可能因同一时间对同一数据进行操作造成锁表或其他问题&#xff0c;Oracle APEX23.2版本迭代后新增了部分功能&#xff0c;可以创建应用程序的工作副本来修复错误、添加功能&#xff0c;然…

后端开发框架Spring Boot快速入门

写在前面 推荐将本文与Spring Boot 相关知识和工具类一文结合起来看&#xff0c;本文为主&#xff0c;上面那篇文章为辅&#xff0c;一起食用&#xff0c;以达到最佳效果&#xff0c;当然&#xff0c;大佬随意。 IDEA创建Spring Boot工程 关于Spring Boot框架项目&#xff0…

Win10 下 Vision Mamba(Vim-main)的环境配置(libcuda.so文件无法找到,windows系统运行失败)

目录 1、下载NVIDIA 驱动程序、cuda11.8、cudnn8.6.0 2、在Anaconda中创建环境并激活 3、下载gpu版本的torch 4、配置环境所需要的包 5、安装causal_conv1d和mamba-1p1p1 安装causal_conv1d 安装mamba-1p1p1 6、运行main.py失败 请直接拉到最后查看运行失败的原因&am…

虚幻UE5数字孪生蓝图开发教程

一、背景 这几年&#xff0c;智慧城市/智慧交通/智慧水利等飞速发展&#xff0c;骑士特意为大家做了一个这块的学习路线。 二、这是学习大纲 1.给虚幻UE5初学者准备的智慧城市/数字孪生蓝图开发教程 https://www.bilibili.com/video/BV1894y1u78G 2.UE5数字孪生蓝图开发教学…