当前位置: 首页 > news >正文

在线学堂-3.媒资管理模块(二)

在线学堂-3.媒资管理模块(二)

1.上传视频

1.1 需求分析

1.教学机构人员进入媒资管理列表查询自己上传的媒资文件

点击“媒资管理”

在这里插入图片描述

进入媒资管理列表页面查询本机构上传的媒资文件。

在这里插入图片描述

2.教育机构用户在"媒资管理"页面中点击 “上传视频” 按钮

在这里插入图片描述

点击“上传视频”打开上传页面

在这里插入图片描述

3.选择要上传的文件,自动执行文件上传

在这里插入图片描述

4.视频上传成功会自动处理,处理完成可以预览视频

在这里插入图片描述

1.2 断点续传技术

1.2.1 什么是断点续传

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传

什么是断点续传:

断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性

断点续传流程如下图:

在这里插入图片描述

流程如下:

1.前端上传前先把文件分成块

2.一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传

3.各分块上传完成最后在服务端合并文件

1.2.2 分块与合并测试

为了更好的理解文件分块上传的原理,下边用java代码测试文件的分块与合并

文件分块的流程如下:

1.获取源文件长度

2.根据设定的分块文件的大小计算出块数

3.从源文件读数据依次向每一个块文件写数据

测试代码如下:

package media.mediaTest;import io.minio.UploadObjectArgs;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;/*** 大文件处理测试*/
public class BigFileTest {//测试文件分块方法@Testpublic void testChunk() throws IOException {//源文件File sourceFile = new File("E:\\内容回顾.mp4");//分块保存的目录String chunkPath = "E:\\update\\check\\";File chunkFolder = new File(chunkPath);if (!chunkFolder.exists()) {chunkFolder.mkdirs();}//分块大小 1MBlong chunkSize = 1024 * 1024 * 1;//分块数量long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println("分块总数:"+chunkNum);//缓冲区大小byte[] b = new byte[1024];//使用RandomAccessFile访问文件,RandomAccessFile随机流具有读写功能RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//分块for (int i = 0; i < chunkNum; i++) {//创建分块文件File file = new File(chunkPath + i);if(file.exists()){file.delete();}boolean newFile = file.createNewFile();if (newFile) {//向分块文件中写数据RandomAccessFile raf_write = new RandomAccessFile(file, "rw");int len = -1;while ((len = raf_read.read(b)) != -1) {raf_write.write(b, 0, len);if (file.length() >= chunkSize) {break;}}raf_write.close();System.out.println("完成分块"+i);}}raf_read.close();}}

文件合并流程:

1.找到要合并的文件并按文件合并的先后进行排序

2.创建合并文件

3.依次从合并的文件中读取数据向合并文件写入数

文件合并的测试代码 :

	//测试文件合并方法,合并后的md5是否有源文件的md5相等,是相等,就没有问题@Testpublic void testMerge() throws IOException {//块文件保存目录File chunkFolder = new File("E:\\update\\check\\");//原始文件File originalFile = new File("E:\\内容回顾.mp4");//合并后的文件File mergeFile = new File("E:\\内容回顾2.mp4");if (mergeFile.exists()) {mergeFile.delete();}//创建新的合并文件mergeFile.createNewFile();//用于写文件RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");//指针指向文件顶端raf_write.seek(0);//缓冲区byte[] b = new byte[1024];//分块列表File[] fileArray = chunkFolder.listFiles();// 转成集合,便于排序List<File> fileList = Arrays.asList(fileArray);// 从小到大排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());}});//合并文件for (File chunkFile : fileList) {RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "rw");int len = -1;while ((len = raf_read.read(b)) != -1) {raf_write.write(b, 0, len);}raf_read.close();}raf_write.close();//校验文件try (FileInputStream fileInputStream = new FileInputStream(originalFile);FileInputStream mergeFileStream = new FileInputStream(mergeFile);) {//取出原始文件的md5String originalMd5 = DigestUtils.md5Hex(fileInputStream);//取出合并文件的md5进行比较String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);if (originalMd5.equals(mergeFileMd5)) {System.out.println("合并文件成功");} else {System.out.println("合并文件失败");}}}
1.2.3 视频上传流程(断点续传流程)

下图是上传视频的整体流程:

在这里插入图片描述

1.前端对文件进行分块

2.前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传

3.如果分块文件不存在则前端开始上传

4.前端请求媒资服务上传分块

5.媒资服务将分块上传至MinIO

6.前端将分块上传完毕请求媒资服务合并分块

7.媒资服务判断分块上传完成则请求MinIO合并文件

8.合并完成校验合并后的文件是否完整,如果不完整则删除文件

1.2.4 minio合并文件测试

1.将分块文件上传至minio

//将分块文件上传至minio
@Test
public void uploadChunk(){String chunkFolderPath = "D:\\develop\\upload\\chunk\\";File chunkFolder = new File(chunkFolderPath);//分块文件File[] files = chunkFolder.listFiles();//将分块文件上传至miniofor (int i = 0; i < files.length; i++) {try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket("testbucket").object("chunk/" + i).filename(files[i].getAbsolutePath()).build();minioClient.uploadObject(uploadObjectArgs);System.out.println("上传分块成功"+i);} catch (Exception e) {e.printStackTrace();}}}

2.通过minio的合并文件

//合并文件,要求分块文件最小5M
@Test
public void test_merge() throws Exception {List<ComposeSource> sources = Stream.iterate(0, i -> ++i).limit(6).map(i -> ComposeSource.builder().bucket("testbucket").object("chunk/".concat(Integer.toString(i))).build()).collect(Collectors.toList());ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket("testbucket").object("merge01.mp4").sources(sources).build();minioClient.composeObject(composeObjectArgs);}
//清除分块文件
@Test
public void test_removeObjects(){//合并分块完成将分块文件清除List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i).limit(6).map(i -> new DeleteObject("chunk/".concat(Integer.toString(i)))).collect(Collectors.toList());RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("testbucket").objects(deleteObjects).build();Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);results.forEach(r->{DeleteError deleteError = null;try {deleteError = r.get();} catch (Exception e) {e.printStackTrace();}});
}

使用minio合并文件报错:java.lang.IllegalArgumentException: source testbucket/chunk/0: size 1048576 must be greater than 5242880

minio合并文件默认分块最小5M,我们将分块改为5M再次测试

在这里插入图片描述

1.3 接口定义

根据上传视频流程,定义接口,与前端的约定是操作成功返回{code:0}否则返回{code:-1}

从课程资料中拷贝RestResponse.java类到base工程下的model包下

定义接口如下:

package com.xuecheng.media.api;import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;/*** 大文件上传接口,上传视频*/
@Api(value = "大文件上传接口", tags = "大文件上传接口")
@RestController
public class BigFilesController {@Autowiredprivate MediaFileService mediaFileService;@ApiOperation(value = "文件上传前检查文件")@PostMapping("/upload/checkfile")public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {return mediaFileService.checkFile(fileMd5);}@ApiOperation(value = "分块文件上传前的检测")//传一个md5和分块序号就可以判断存不存在@PostMapping("/upload/checkchunk")public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {return mediaFileService.checkChunk(fileMd5,chunk);}@ApiOperation(value = "上传分块文件")@PostMapping("/upload/uploadchunk")public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {//创建临时文件File tempFile = File.createTempFile("minio", "temp");//上传的文件拷贝到临时文件file.transferTo(tempFile);//文件路径String absolutePath = tempFile.getAbsolutePath();return mediaFileService.uploadChunk(fileMd5,chunk,absolutePath);}}

响应结果集

base模块下的model包中

package com.xuecheng.base.model;/*** 通用结果类型*/
@Data
@ToString
public class RestResponse<T>
{/*** 响应编码,0为正常,-1错误*/private int code;/*** 响应提示信息*/private String msg;/*** 响应内容*/private T result;public RestResponse() {this(0, "success");}public RestResponse(int code, String msg) {this.code = code;this.msg = msg;}/*** 错误信息的封装*/public static <T> RestResponse<T> validfail(String msg) {RestResponse<T> response = new RestResponse<T>();response.setCode(-1);response.setMsg(msg);return response;}public static <T> RestResponse<T> validfail(T result, String msg) {RestResponse<T> response = new RestResponse<T>();response.setCode(-1);response.setResult(result);response.setMsg(msg);return response;}/*** 添加正常响应数据(包含响应内容)* @return RestResponse Rest服务封装相应数据*/public static <T> RestResponse<T> success(T result) {RestResponse<T> response = new RestResponse<T>();response.setResult(result);return response;}public static <T> RestResponse<T> success(T result, String msg) {RestResponse<T> response = new RestResponse<T>();response.setResult(result);response.setMsg(msg);return response;}/*** 添加正常响应数据(不包含响应内容)* @return RestResponse Rest服务封装相应数据*/public static <T> RestResponse<T> success() {return new RestResponse<T>();}public Boolean isSuccessful() {return this.code == 0;}}

1.4 上传分块开发

1.4.1 DAO开发

向媒资数据库的文件表插入记录,使用自动生成的Mapper接口即可满足要求

1.4.2 Service开发
1.4.2.1 检查文件和分块

接口完成进行接口实现,首先实现检查文件方法和检查分块方法

在MediaFileService中定义service接口如下:

package com.xuecheng.media.service;/*** 媒资文件管理业务类*/
public interface MediaFileService {/*** 检查文件是否存在* fileMd5 文件的md5* @return false不存在,true存在*/public RestResponse<Boolean> checkFile(String fileMd5);/*** 检查分块是否存在* fileMd5  文件的md5* chunkIndex  分块序号* @return false不存在,true存在*/public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex);
}

service接口实现方法:

@Override
public RestResponse<Boolean> checkFile(String fileMd5) {//查询文件信息MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if (mediaFiles != null) {//桶String bucket = mediaFiles.getBucket();//存储目录String filePath = mediaFiles.getFilePath();//文件流InputStream stream = null;try {stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());if (stream != null) {//文件已存在return RestResponse.success(true);}} catch (Exception e) {}}//文件不存在return RestResponse.success(false);
}@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {//得到分块文件目录String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//得到分块文件的路径String chunkFilePath = chunkFileFolderPath + chunkIndex;//文件流InputStream fileInputStream = null;try {fileInputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket_videoFiles).object(chunkFilePath).build());if (fileInputStream != null) {//分块已存在return RestResponse.success(true);}} catch (Exception e) {}//分块未存在return RestResponse.success(false);
}//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
1.4.2.2 上传分块

定义service接口

package com.xuecheng.media.service;/*** 媒资文件管理业务类*/
public interface MediaFileService {/*** 上传分块* fileMd5  文件md5* chunk  分块序号* localChunkFilePath  分块文件本地路径*/public RestResponse uploadChunk(String fileMd5,int chunk,String localChunkFilePath);
}

接口实现:

@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {//得到分块文件的目录路径String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//得到分块文件的路径String chunkFilePath = chunkFileFolderPath + chunk;//mimeTypeString mimeType = getMimeType(null);//将文件存储至minIOboolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_videoFiles, chunkFilePath);if (!b) {log.debug("上传分块文件失败:{}", chunkFilePath);return RestResponse.validfail(false, "上传分块失败");}log.debug("上传分块文件成功:{}",chunkFilePath);return RestResponse.success(true);}
1.4.2.3 上传分块测试

启动前端工程,进入上传视频界面进行前后端联调测试

前端对文件分块的大小为5MB,SpringBoot web默认上传文件的大小限制为1MB,这里需要在media-api工程修改配置如下:(在nacos里面配置)

spring:servlet:multipart:max-file-size: 50MBmax-request-size: 50MB

max-file-size:单个文件的大小限制

Max-request-size: 单次请求的大小限制

1.5 合并分块开发

1.5.1 service开发

定义service接口:

package com.xuecheng.media.service;public interface MediaFileService {/*** 合并分块* companyId  机构id* fileMd5  文件md5* chunkTotal 分块总和* uploadFileParamsDto 文件信息*/public RestResponse mergechunks(Long companyId,String fileMd5,int chunkTotal,UploadFileParamsDto uploadFileParamsDto);
}

service实现:

@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {//=====获取分块文件路径=====String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//组成将分块文件路径组成 List<ComposeSource>List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucket_videoFiles).object(chunkFileFolderPath.concat(Integer.toString(i))).build()).collect(Collectors.toList());//=====合并=====//文件名称String fileName = uploadFileParamsDto.getFilename();//文件扩展名String extName = fileName.substring(fileName.lastIndexOf("."));//合并文件路径String mergeFilePath = getFilePathByMd5(fileMd5, extName);try {//合并文件ObjectWriteResponse response = minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucket_videoFiles).object(mergeFilePath).sources(sourceObjectList).build());log.debug("合并文件成功:{}",mergeFilePath);} catch (Exception e) {log.debug("合并文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);return RestResponse.validfail(false, "合并文件失败。");}// ====验证md5====//下载合并后的文件File minioFile = downloadFileFromMinIO(bucket_videoFiles,mergeFilePath);if(minioFile == null){log.debug("下载合并后文件失败,mergeFilePath:{}",mergeFilePath);return RestResponse.validfail(false, "下载合并后文件失败。");}try (InputStream newFileInputStream = new FileInputStream(minioFile)) {//minio上文件的md5值String md5Hex = DigestUtils.md5Hex(newFileInputStream);//比较md5值,不一致则说明文件不完整if(!fileMd5.equals(md5Hex)){return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");}//文件大小uploadFileParamsDto.setFileSize(minioFile.length());}catch (Exception e){log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");}finally {if(minioFile!=null){minioFile.delete();}}//文件入库currentProxy.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videoFiles,mergeFilePath);//=====清除分块文件=====clearChunkFiles(chunkFileFolderPath,chunkTotal);return RestResponse.success(true);
}/*** 从minio下载文件* bucket 桶* objectName 对象名称* @return 下载后的文件*/
public File downloadFileFromMinIO(String bucket,String objectName){//临时文件File minioFile = null;FileOutputStream outputStream = null;try{InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build());//创建临时文件minioFile=File.createTempFile("minio", ".merge");outputStream = new FileOutputStream(minioFile);IOUtils.copy(stream,outputStream);return minioFile;} catch (Exception e) {e.printStackTrace();}finally {if(outputStream!=null){try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}}return null;
}
/*** 得到合并后的文件的地址* fileMd5 文件id即md5值* fileExt 文件扩展名*/
private String getFilePathByMd5(String fileMd5,String fileExt){return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}/*** 清除分块文件* chunkFileFolderPath 分块文件路径* chunkTotal 分块文件总数*/
private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){try {List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i)))).collect(Collectors.toList());RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);results.forEach(r->{DeleteError deleteError = null;try {deleteError = r.get();} catch (Exception e) {e.printStackTrace();log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e);}});} catch (Exception e) {e.printStackTrace();log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);}
}
1.5.2 接口层完善

下边完善接口层

@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5,@RequestParam("fileName") String fileName,@RequestParam("chunkTotal") int chunkTotal) throws Exception {Long companyId = 1232141425L;UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();uploadFileParamsDto.setFileType("001002");uploadFileParamsDto.setTags("课程视频");uploadFileParamsDto.setRemark("");uploadFileParamsDto.setFilename(fileName);return mediaFileService.mergechunks(companyId,fileMd5,chunkTotal,uploadFileParamsDto);}
1.5.3 合并分块测试

下边进行前后端联调:

1.上传一个视频测试合并分块的执行逻辑,进入service方法逐行跟踪

2.断点续传测试

上传一部分后,停止刷新浏览器再重新上传,通过浏览器日志发现已经上传过的分块不再重新上传

在这里插入图片描述

1.6 其它问题

1.6.1 分块文件清理问题

上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?

1.在数据库中有一张文件表记录minio中存储的文件信息

2.文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成

3.当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录

面试:断点续传怎么实现

前端对文件分块。前端上传每一块文件之前都会向服务端确认该块文件是否已经上传。前端把文件上传到服务端,服务端把文件保存到minio中。当所有分块上传完毕,服务端就合并所有分块。前端发送一个md5值,后端也计算一个md5值,比较如果相等代表文件没有损坏,然后服务端就会把合并后的文件上传到分布式文件系统minio中

清理:文件表记录了文件的信息,给文件记录一个状态(上传中),根据上传时间(每24小时扫描),上传中没有完成上传的文件,清理掉这些文件在分布式文件系统中的位置,清空这些分块

http://www.dtcms.com/a/265150.html

相关文章:

  • 软件反调试(2)- 基于窗口列表的检测
  • 外侧三兵策略
  • 睿抗省赛2023
  • 【通识】机器学习相关
  • YOLOv11剪枝与量化(二)通道剪枝技术原理
  • 【Ragflow】30.离线环境迁移方案
  • 数据库9:数据库字符编码调整与校队(排序)规则
  • STM32F103_Bootloader程序开发11 - 实现 App 安全跳转至 Bootloader
  • UI 设计|审美积累 | 拟物化风格(Skeuomorphism)
  • 基于Jeecgboot3.8.1的vue3版本前后端分离的flowable流程管理平台
  • ai之RAG本地知识库--基于OCR和文本解析器的新一代RAG引擎:RAGFlow 认识和源码剖析
  • 学习笔记(29):训练集与测试集划分详解:train_test_split 函数深度解析
  • SimBa:实现深度强化学习参数scaling up
  • 多路I/O转接服务器(select、poll、epoll)
  • 跨境贸易的主要挑战是什么?
  • monorepo + Turborepo --- 构建仓库结构
  • 如何设置电脑定时休眠?操作指南详解
  • 从 PostgreSQL 到 DolphinDB:数据实时同步一站式解决方案
  • 金融安全生命线:用AWS EventBridge和CloudTrail构建主动式入侵检测系统
  • 少样本学习在计算机视觉中的应用:原理、挑战与最新突破
  • Java 导出PDF 1、内容可以插入自定义表格 2、内容插入图片
  • Python3 学习(菜鸟)-06迭代器与生成器
  • 碰一碰矩阵发布源码开发技术揭秘-支持OEM贴牌搭建
  • 在幸狐RV1106板子上用gcc14.2本地编译安装apache2.4.63,开启http2和tls1.3,并且https支持XP系统的IE6-8浏览器
  • 《汇编语言:基于X86处理器》第6章 条件处理(2)
  • 为什么我画的频谱图和audacity、audition不一样?
  • containerd 项目主要目录简要说明
  • Flink-1.19.0源码详解-番外补充3-StreamGraph图
  • 精准定义 RediSearch 索引 Schema
  • LeetCode Hot 100 哈希【Java和Golang解法】