4.3 媒资管理模块 - Minio系统上传图片与视频

文章目录

  • 一、上传图片
    • 1.1 需求分析
    • 1.2 数据模型
      • 1.2.1 media_files 媒资信息表
    • 1.3 准备Minio环境
      • 1.3.1 桶环境
      • 1.3.2 连接Minio参数
      • 1.3.3 Minio配置类
    • 1.4 接口定义
      • 1.4.1 上传图片接口请求参数
      • 1.4.2 上传图片接口返回值
      • 1.4.3 接口代码
    • 1.5 MediaFilesMapper
    • 1.6 MediaFileServiceImpl
    • 1.7 事物优化
    • 1.8 测试
  • 二、上传视频
    • 2.0 响应结果类
    • 2.1 断点续传
    • 2.2 测试Demo
      • 2.2.1 分块测试
      • 2.2.2 合并测试
    • 2.3 测试MinIo
      • 2.3.1 上传分块
      • 2.3.2 合并分块
    • 2.4 上传视频
      • 2.4.1 文件上传前校验
        • 2.4.1.1 BigFilesController
        • 2.4.1.2 MediaFileServiceImpl
      • 2.4.2 上传分块文件
        • 2.4.2.1 BigFilesController
        • 2.4.2.2 MediaFileServiceImpl
      • 2.4.3 合并分块文件
        • 2.4.3.1 BigFilesController
        • 2.4.3.2 MediaFileServiceImpl
      • 2.4.4 测试

一、上传图片

此模块涉及到修改/添加课程,之前已经写过接口了,在下面这篇文章中

3.2 内容管理模块 - 课程分类、新增课程、修改课程-CSDN博客

1.1 需求分析

我们的文件要存储在minio文件系统中

在新增课程界面上传课程图片,也可以修改课程图片

这一步上传图片是请求的媒资服务接口,并不是内容管理服务接口

image-20231222215414543

在课程基本信息中有一个“pic”字段,要存储图片的路径

image-20231222215700731

pic字段值类似“/mediafiles/2022/09/18/a16da7a132559daf9e1193166b3e7f52.jpg”,其中“mediafiles”就是桶,后面就是文件具体的对象名

在我们前端功能.env环境变量配置文件定义了图片服务器地址

如果想要展示图片,那就要将“http://192.168.101.65:9000”拼接上pic字段值

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么不将“http://192.168.101.65:9000”直接写在数据库里

因为如果上线之后,地址就不好修改了


为什么要单独设置一个媒资管理服务

①管理文件的信息,我们使用media_files表记录文件信息

不管是图片、视频、文档,全会记录在这个表中

image-20231222223702229

②把文件本身放在分布式文件系统(minio)中管理起来

方便管理,媒资服务就是一个统一的文件管理功能


详细流程

image-20231222223956542

1、前端进入上传图片界面

2、上传图片,请求媒资管理服务。

3、媒资管理服务将图片文件存储在MinIO。

4、媒资管理记录文件信息到数据库。

5、前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。

1.2 数据模型

我们怎么区分文件是否已经上传

可以通过文件的MD5值,同一个文件的MD5值相同

前端上传的时候就会把文件MD5值传给我们后台,后台拿着MD5值去数据库查询,如果有这个MD5值了,说明文件已经上传成功了,不需要再上传了

1.2.1 media_files 媒资信息表

image-20231222224302847

file_path存储路径,相当于java代码中的对象名

url:可以通过http访问文件的路径

url = /bucket/+file_path

但是有例外,如果是上传了一个avi视频的话,那file_path里面存的就是avi结尾的视频地址,但是avi的视频最终是不能播放的,我们需要把这个视频进行转码生成MP4格式的视频,此时url就是存储的MP4的视频地址、

如果是图片的话url = /bucket/+file_path这个等式是一个样子的

/*** 媒资信息*/
@Data
@TableName("media_files")
public class MediaFiles implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.ASSIGN_ID)private String id;/*** 机构ID*/private Long companyId;/*** 机构名称*/private String companyName;/*** 文件名称*/private String filename;/*** 文件类型(文档,音频,视频)*/private String fileType;/*** 标签*/private String tags;/*** 存储目录*/private String bucket;/*** 存储路径*/private String filePath;/*** 文件标识*/private String fileId;/*** 媒资文件访问地址*/private String url;/*** 上传人*/private String username;/*** 上传时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createDate;/*** 修改时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime changeDate;/*** 状态,1:未处理,视频处理完成更新为2*/private String status;/*** 备注*/private String remark;/*** 审核状态*/private String auditStatus;/*** 审核意见*/private String auditMind;/*** 文件大小*/private Long fileSize;}

除此之外还会涉及到课程基本信息表course_base,表中pic字段存储课程图片的路径

image-20231222215700731

1.3 准备Minio环境

1.3.1 桶环境

  • mediafiles桶

    视频以外的文件都存储在这个桶内

image-20231222230701829

  • video 桶

    存放视频

    image-20231222230711368

并且两个桶的Access Policy权限都要改成public

image-20231222230808165

1.3.2 连接Minio参数

在media-service工程中配置minio的相关信息,要配置在nacos中

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.101.65:3306/xcplus_media?serverTimezone=UTC&userUnicode=true&useSSL=false&username: rootpassword: mysqlcloud:config:#配置本地优先override-none: true  minio:#地址endpoint: http://192.168.101.65:9000 #账号accessKey: minioadmin #密码secretKey: minioadmin #两个桶bucket:files: mediafiles videofiles: video

本地bootstrap.yaml文件

spring:application:name: media-servicecloud:nacos:server-addr: 192.168.101.65:8848discovery:namespace: ${spring.profiles.active}group: xuecheng-plus-projectconfig:namespace: ${spring.profiles.active}group: xuecheng-plus-projectfile-extension: yamlrefresh-enabled: trueshared-configs:- data-id: logging-${spring.profiles.active}.yamlgroup: xuecheng-plus-commonrefresh: true#profiles默认为devprofiles:active: dev

1.3.3 Minio配置类

在media-service工程中进行配置

@Configuration
public class MinioConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.accessKey}")private String accessKey;@Value("${minio.secretKey}")private String secretKey;@Beanpublic MinioClient minioClient() {MinioClient minioClient =MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();return minioClient;}
}

1.4 接口定义

下面的内容其实是完成标红的地方

image-20231222235649629

做这一部分的时候可能发现course_base课程基本信息表和media_files表没有关联,那怎么将图片的url存放到course_base课程基本信息表中的pic字段呢?

当上传完图片之后,会点击保存,这个时候就会对course_base表中的各种信息进行操作了

image-20231223214409515

但是我感觉没有很好的方法让course_base表

1.4.1 上传图片接口请求参数

和media_file相关的文件上传的基本信息

/*** @description 上传普通文件请求参数*/
@Data
public class UploadFileParamsDto {/*** 文件名称*/private String filename;/*** 文件类型(文档,音频,视频)*/private String fileType;/*** 文件大小*/private Long fileSize;/*** 标签*/private String tags;/*** 上传人*/private String username;/*** 备注*/private String remark;}

1.4.2 上传图片接口返回值

这些信息其实是文件表中的信息

{"id": "a16da7a132559daf9e1193166b3e7f52","companyId": 1232141425,"companyName": null,"filename": "1.jpg","fileType": "001001","tags": "","bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg","fileId": "a16da7a132559daf9e1193166b3e7f52","url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg","timelength": null,"username": null,"createDate": "2022-09-12T21:57:18","changeDate": null,"status": "1","remark": "","auditStatus": null,"auditMind": null,"fileSize": 248329
}

根据上面返回信息封装一个Dto

但是我们不会直接使用media_files的实体类,假如前端多需要几个参数的话,我们还需要修改

在media-model工程中添加如下实体类

@Data
public class UploadFileResultDto extends MediaFiles {}

1.4.3 接口代码

    /*** 对于请求内容:Content-Type: multipart/form-data;* 前端向后端传输一个文件,那后端程序就属于一个消费者,我们指定一下类型* form-data; name="filedata"; filename="具体的文件名称"* <p>* 我们可以使用@RequestPart指定一下前端向后端传输文件的名称* 用MultipartFile类型接收前端向后端传输的文件*/@ApiOperation("上传图片")@PostMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {//此时已经接收到文件了,目前作为临时文件存储在内存中//1.创建一个临时文件,前缀是"minio",后缀是“.temp”File tempFile = File.createTempFile("minio", ".temp");//2.将上传后的文件传输到临时文件中filedata.transferTo(tempFile);//3.取出临时文件的绝对路径String localFilePath = tempFile.getAbsolutePath();Long companyId = 1232141425L; //先写死,写认证授权系统时再进行//4.准备上传文件的信息UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();//filedata.getOriginalFilename()获取原始文件名称uploadFileParamsDto.setFilename(filedata.getOriginalFilename());//文件大小uploadFileParamsDto.setFileSize(filedata.getSize());//文件类型 001001在数据字典中代表图片uploadFileParamsDto.setFileType("001001");//调用service上传图片return mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);}

1.5 MediaFilesMapper

我们需要保存文件信息

/*** 媒资信息 Mapper 接口*/
public interface MediaFilesMapper extends BaseMapper<MediaFiles> {}

1.6 MediaFileServiceImpl

媒资管理业务类 - 下面的代码其实就做了上传图片与文件信息入库

    @Autowiredprivate MinioClient minioClient;//除了视频文件以外的桶@Value("${minio.bucket.files}")private String bucket_medialFiles;//视频文件桶@Value("${minio.bucket.videofiles}")private String bucket_video;/*** 上传文件** @param companyId           机构id* @param uploadFileParamsDto 上传文件信息* @param localFilePath       文件磁盘路径* @return 文件信息*/@Transactional@Overridepublic UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {//TODO 1.将文件上传到Minio//TODO 1.1 获取文件扩展名String filename = uploadFileParamsDto.getFilename();String extension = filename.substring(filename.lastIndexOf("."));//TODO 1.2 根据文件扩展名获取mimeTypeString mimeType = this.getMimeType(extension);//TODO 1.3 bucket,从nacos中读取//TODO 1.4 ObjectName约定在MinIo系统中存储的目录是年/月/日/图片文件//得到文件的路径defaultFolderPathString defaultFolderPath = this.getDefaultFolderPath();//最终存储的文件名是MD5值String fileMd5 = this.getFileMd5(new File(localFilePath));String ObjectName = defaultFolderPath + fileMd5 + extension;//TODO 1.5 上传文件到Minioboolean result = this.addMediaFilesToMinIO(localFilePath, bucket_medialFiles, ObjectName, mimeType);if (!result) {XueChengPlusException.cast("上传文件失败");}//TODO 2.将文件信息保存到数据库MediaFiles mediaFiles = this.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_medialFiles, ObjectName);if (mediaFiles == null){XueChengPlusException.cast("文件上传后保存信息失败");}UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);return uploadFileResultDto;}/*** @param companyId           机构id* @param fileMd5             文件md5值* @param uploadFileParamsDto 上传文件的信息* @param bucket              桶* @param objectName          对象名称* @return com.xuecheng.media.model.po.MediaFiles* @description 将文件信息添加到文件表*/public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {//根据文件MD5值向数据库查找文件信息MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if (mediaFiles == null) {mediaFiles = new MediaFiles();BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);mediaFiles.setId(fileMd5);//文件信息的主键是文件的MD5值mediaFiles.setCompanyId(companyId);//机构IDmediaFiles.setBucket(bucket);//桶mediaFiles.setFilePath(objectName);//对象名mediaFiles.setFileId(fileMd5);//file_id字段mediaFiles.setUrl("/" + bucket + "/" + objectName);//urlmediaFiles.setCreateDate(LocalDateTime.now());//上传时间mediaFiles.setStatus("1");//状态 1正常 0不展示mediaFiles.setAuditStatus("002003");//审核状态 002003审核通过int insert = mediaFilesMapper.insert(mediaFiles);if (insert <= 0) {log.debug("向数据库保存文件失败bucket:{},objectName:{}", bucket, objectName);}}return mediaFiles;}/*** 将文件上传到MinIo** @param bucket        桶* @param localFilePath 文件在本地的路径* @param objectName    上传到MinIo系统中时的文件名称* @param mimeType      上传的文件类型*/private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()//桶,也就是目录.bucket(bucket)//指定本地文件的路径.filename(localFilePath)//上传到minio中的对象名,上传的文件存储到哪个对象中.object(objectName).contentType(mimeType)//构建.build();minioClient.uploadObject(uploadObjectArgs);log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);return true;} catch (Exception e) {e.printStackTrace();log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());return false;}}/*** 根据扩展名获取mimeType** @param extension 扩展名*/private String getMimeType(String extension) {if (extension == null) {//目的是防止空指针异常extension = "";}ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);//通用mimeType,字节流String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;if (extensionMatch != null) {mimeType = extensionMatch.getMimeType();}return mimeType;}/*** @return 获取文件默认存储目录路径 年/月/日*/private String getDefaultFolderPath() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");return sdf.format(new Date()).replace("-", "/") + "/";}/*** 获取文件的md5** @param file 文件* @return MD5值*/private String getFileMd5(File file) {try (FileInputStream fileInputStream = new FileInputStream(file)) {return DigestUtils.md5Hex(fileInputStream);} catch (Exception e) {e.printStackTrace();return null;}}

1.7 事物优化

在下面这篇文章中也有事物的方法,感兴趣的话可以查看一下1.6

Redis - 优惠券秒杀、库存超卖、分布式锁、Redisson

我在uploadFile上传文件的方法中加了@Transactional方法

也就是说在uploadFile方法执行之前就会开启事物

但是addMediaFilesToMinIO方法会通过网络访问Minio分布式文件系统上传文件,但是通过网络访问的话这个时间可长可短,假如说网络访问时间长的话,事物的时间就会比较长,占用数据库资源的时间就会长,极端情况下会导致数据库的连接不够用

所在当方法中有网络访问的话,千万不要使用数据库的@Transactional事物进行控制

所以此时我们在addMediaFilesToDb方法上添加@Transactional控制事物的方法就好了


public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
....
}

但是现在也会出现一个问题,我们在uploadFile方法中调用addMediaFilesToDb方法的时候并不会触发addMediaFilesToDb方法上的@Transactional,这种情况也是事物失效的场景之一

分析一下这种情况出现的原因

在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务

image-20231223224851440

如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制

断该方法是否可以事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解

image-20231223224941165

在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法

如下图所示,在addMediaFilesToDb方法中的this指代的并不是代理对象,uploadFile方法中this指代的也不是代理对象

我们在Controller使用@AutoWired的注解引入的MediaFileService对象是代理对象,怎么在调用的时候就不是了呢?

在MediaFileServiceImpl类中this指代的就是当前类,不是代理对象

代理对象其实是在原始对象上包了一层,然后调用原始对象上的方法而已

image-20231223225251724

为了解决这个问题,我们可以在uploadFile方法中使用代理的方式调用addMediaFilesToDb方法,使其能触发@Transactional注解

不同担心循环依赖的问题,spring有三级缓存

@Autowired
MediaFileService currentProxy;

将addMediaFilesToDb方法提成接口

/*** @description 将文件信息添加到文件表* @param companyId  机构id* @param fileMd5  文件md5值* @param uploadFileParamsDto  上传文件的信息* @param bucket  桶* @param objectName 对象名称* @return com.xuecheng.media.model.po.MediaFiles*/public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);

调用addMediaFilesToDb方法的代码处改为如下

//TODO 2.将文件信息保存到数据库
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_medialFiles, ObjectName);
if (mediaFiles == null){XueChengPlusException.cast("文件上传后保存信息失败");
}

此时就是通过CGLB生成的代理对象,挺完美的

image-20231223231025237

1.8 测试

测试上传文件

image-20231223210201133

image-20231223210009088

二、上传视频

上传视频的位置如下所示

image-20231223231511151

image-20231223232117836

2.0 响应结果类

/*** 通用结果类*/
@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;}/*** 错误信息的封装** @param msg* @param <T>* @return*/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;}
}

2.1 断点续传

假如说用户上传了一个大文件,但是用户的网不是很好,上传了一部分网断了,这个时候就需要一个断点续传的需求了

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。

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

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

image-20231224012021229

流程如下

1、前端对文件进行分块

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

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

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

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

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

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

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

2.2 测试Demo

2.2.1 分块测试

/*** 测试分块*/
@Test
public void testChunk() throws Exception {//TODO 1.获取源文件File sourceFile = new File("E:\\歌.mp4");//TODO 2.定义基本参数//2.1 分块文件存储路径String chunkFilePath = "E:\\chunk\\";//2.2 分块文件的大小 1024*1024*1 代表1M,5M的话乘5即可(也就是最小单位是字节byte 1024个byte是1k)int chunkSize = 1024 * 1024 * 1;//2.3 分块文件大小//Math.ceil表示向上取整//sourceFile.length()是获取文件的大小是多少byte字节int chunkNum = (int) Math.ceil((sourceFile.length() * 1.0) / chunkSize);//TODO 3.从源文件中读数据,向分块文件中写数据//RandomAccessFile流既可以读又可以写//参数一:File类型  参数二:是读(“r”)还是写"rw"RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");//缓存区,1kbyte[] bytes = new byte[1024];//TODO 3.1 创建分块文件for (int i = 0; i < chunkNum; i++) {File chunkFile = new File(chunkFilePath + i);//分块文件写入流RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");int len = -1;//将数据读取到缓冲区中raf_r.read(bytes)while ((len = raf_r.read(bytes)) != -1) {//向临时文件中进行写入raf_rw.write(bytes, 0, len);//如果分块文件chunkFile的大小大于等于我们规定的分块文件的chunkSize大小,就不要再继续了if (chunkFile.length() >= chunkSize) {break;}}raf_rw.close();}raf_r.close();
}

image-20231224003333071

2.2.2 合并测试

/*** 测试合并*/
@Test
public void testMerge() throws Exception {//TODO 1.基本参数//分块文件目录File chunkFolder = new File("E:\\chunk\\");//源文件File sourceFile = new File("E:\\歌.mp4");//合并后的文件在哪里File mergeFile = new File("E:\\chunk\\歌Copy.mp4");//TODO 2.取出所有分块文件,此时的顺序可能是无序的File[] files = chunkFolder.listFiles();//将数组转换成ListList<File> fileList = Arrays.asList(files);//利用Comparator进行排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {//升序return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());}});//TODO 3.合并分块文件//缓存区,1kbyte[] bytes = new byte[1024];//向合并分块的流RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");for (File file : fileList) {//向读取分块文件RandomAccessFile raf_r = new RandomAccessFile(file, "r");int len = -1;while ((len = raf_r.read(bytes)) != -1) {raf_rw.write(bytes, 0, len);}raf_r.close();}raf_rw.close();//TODO 校验是否合并成功//合并文件完成后比对合并的文件MD5值域源文件MD5值FileInputStream fileInputStream = new FileInputStream(sourceFile);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("合并文件失败");}
}

image-20231224010242972

image-20231224010056328

2.3 测试MinIo

image-20231224012021229

MinioClient minioClient =MinioClient.builder()//这个地方是运行minio后展示的地址.endpoint("http://192.168.101.65:9000")//账号和密码.credentials("minioadmin", "minioadmin").build();

2.3.1 上传分块

将分块文件上传至MinIo

/*** 将分块文件上传到minio*/
@Test
public void uploadChunk() throws Exception {//获取所有的分块文件File file = new File("E:\\chunk\\");File[] files = file.listFiles();for (File chunkFile : files) {minioClient.uploadObject(UploadObjectArgs.builder()//桶,也就是目录.bucket("testbucket")//指定本地文件的路径.filename(chunkFile.getAbsolutePath())//对象名.object("chunk/"+chunkFile.getName())//构建.build());System.out.println("上传分块"+chunkFile.getName()+"成功");}
}

image-20231224015333814

2.3.2 合并分块

分块已经在Minio了,我们调用Minio提供的SDK即可合并分块

/*** 调用minio接口合并分块*/
@Test
public void merge() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {//分块文件集合List<ComposeSource> sources = new ArrayList<>();sources.add(ComposeSource.builder()//分块文件所在桶.bucket("testbucket")//分块文件名称.object("chunk/0").build());sources.add(ComposeSource.builder()//分块文件所在桶.bucket("testbucket")//分块文件名称.object("chunk/1").build());ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()//指定合并后文件在哪个桶里.bucket("testbucket")//最终合并后的文件路径及名称.object("merge/merge01.mp4")//指定分块源文件.sources(sources).build();//合并分块minioClient.composeObject(composeObjectArgs);
}

出现这个文件说明我们分块的文件太小了,要大于5242880byte,也就是5M,我们上传的文件才1048576byte,也就是1M

Minio默认的分块文件大小是5M

image-20231224021335475

2.4 上传视频

  • 前端上传视频之前,需要先检查文件是否已经上传过,我们只需要拿着文件的MD5值去数据库查询即可,如果查到了就说明之前已经上传完成了

  • 假如说文件没有上传过,前端开始对视频进行分块然后将分块传输给后端,但是后端不会立即将分块上传到Minio系统,而是先检查此分块是否从前上传到在Minio系统

我们知道文件的MD5值及分块的序号,就可以知道这个分块在不在Minio系统

因为我们是根据文件的MD5值及分块的序号在Minio系统中存储的

  • 等所有的分块文件都上传完毕后,前端就可以请求后端接口进行合并分块

我们某个文件的分块都是在一个目录下的,也就是说一个分块是在一个目录,不用担心分块文件会重复

2.4.1 文件上传前校验

分为“文件上传前检查文件”与“分块文件上传前的检测”两个接口

2.4.1.1 BigFilesController
/*** 1.检查文件数据库有没有* 2.如果数据库有了再检查minio系统当用有没有(可能存在脏数据,数据库中有但是minio没有那也要传输)*/
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) {return mediaFileService.checkFile(fileMd5);
}/*** 分块在数据库中是不存储的,但是可以向minio中查询分块是否存在* minio中有了就不再传了,若没有的话再传*/
@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkChunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {return mediaFileService.checkChunk(fileMd5,chunk);
}
2.4.1.2 MediaFileServiceImpl
/*** 1.首先查询数据库,如果文件不在数据库中表明文件不在* 2.如果文件在数据库中再查询minio系统,** @param fileMd5 文件的md5* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @description 检查文件是否存在*/
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {//TODO 1.查询数据库MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);//TODO 2.如果数据库存在再查询minioif (mediaFiles != null) {GetObjectArgs getObjectArgs = GetObjectArgs.builder()//mediaFiles会有记录.bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();try {FilterInputStream inputStream = minioClient.getObject(getObjectArgs);if (inputStream != null) {//文件已经存在return RestResponse.success(true);}} catch (Exception e) {e.printStackTrace();}}//文件不存在return RestResponse.success(false);
}private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/chunk/";
}

检查分块是否存在

对于Service的checkChunk分块文件校验方法我觉得还有另外一个见解思路

课程中对分块文件的校验思路只是检查一下minio系统中有没有对应的minio文件,如果没有的话就上传分块

但是我觉得这个地方可以改一改

有化成根据fileMd5去数据库查一下是否存在这个文件,然后得到这个文件的getFilePath,查看一下是否有完整的视频文件,如果有的话直接不用检查Minio分块文件是否存在了,假如说Minio没有完整的文件,那再上传对应的分块文件

可能有的人会问,我们明明已经在checkFile中校验了,为什么还要校验?

因为我在学习这个课程的时候我发现一个bug,就是我无法重复上传同一个文件,显然是不合理的

我说的重复上传的意思是毫无征兆的上传不上,前端也不会给什么提示信息,一直卡在百分之九十多的位置不动,为了优化这个地方我才采用了我这个思路

而且我不明白课程中checkFile明明已经校验为true文件存在了,还要进行checkChunk检查分块、合并分块的逻辑

如果是我做前端的话,我就当checkFile返回true时也不进行checkchunk检查分块,更不会合并分块

image-20231224223747158

image-20231224223730483

下面的TODO 1便是我新添加上的,只需要加载Todo2之前即可

2.4.3 合并分块文件模块也要加

//TODO 1.判断fileMd5对应的文件已经合并成一个完整的文件了,如果有了的话,那也不需要检查分块了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){//文件已经存在return RestResponse.success(true);
}
/*** 分块不会存在于数据库,直接查询minio系统即可** @param fileMd5    文件的md5* @param chunkIndex 分块序号* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @description 检查分块是否存在*/
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {//TODO 2.根据MD5得到分块文件的目录路径//分块存储路径:md5前两位为两个目录,MD5值也是一层目录,chunk目录存储分块文件String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);GetObjectArgs getObjectArgs = GetObjectArgs.builder()//视频的桶.bucket(bucket_video)//文件名是目录路径+分块序号.object(chunkFileFolderPath+chunkIndex).build();try {FilterInputStream inputStream = minioClient.getObject(getObjectArgs);if (inputStream != null) {//文件已经存在return RestResponse.success(true);}} catch (Exception e) {e.printStackTrace();}return RestResponse.success(false);
}

2.4.2 上传分块文件

2.4.2.1 BigFilesController
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file,@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {//1.创建一个临时文件,前缀是"minio",后缀是“.temp”File tempFile = File.createTempFile("minio", ".temp");file.transferTo(tempFile);return mediaFileService.uploadChunk(fileMd5,chunk,tempFile.getAbsolutePath());
}
2.4.2.2 MediaFileServiceImpl
/*** @param fileMd5            文件md5* @param chunk              分块序号* @param localChunkFilePath 本地文件路径* @return com.xuecheng.base.model.RestResponse* @description 上传分块*/
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {//TODO 将分块文件上传到minio//传空默认返回类型MediaType.APPLICATION_OCTET_STREAM_VALUE application/octet-stream未知流类型String mimeType = getMimeType(null);//获取分块文件的目录String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);boolean b = this.addMediaFilesToMinIO(localChunkFilePath, bucket_video, chunkFileFolderPath + chunk, mimeType);if (!b) {//falsereturn RestResponse.validfail(false, "上传分块文件{" + fileMd5 + "/" + chunk + "}失败");}//上传分块文件成功return RestResponse.success(true);
}/*** 将文件上传到MinIo** @param bucket        桶* @param localFilePath 文件在本地的路径* @param objectName    上传到MinIo系统中时的文件名称* @param mimeType      上传的文件类型*/private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()//桶,也就是目录.bucket(bucket)//指定本地文件的路径.filename(localFilePath)//上传到minio中的对象名,上传的文件存储到哪个对象中.object(objectName).contentType(mimeType)//构建.build();minioClient.uploadObject(uploadObjectArgs);log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);return true;} catch (Exception e) {e.printStackTrace();log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());return false;}}

2.4.3 合并分块文件

  • 找到分块文件

  • 调用Minio的SDK,将分块文件进行合并

  • 校验合并后的文件与原文件是否一致

    合并后的文件来源于分块文件,如果上传分块的过程中有问题导致数据丢失,那合并后的文件肯定是不完整的

    合并后的文件与原文件一致,才可以说视频是上传成功的

  • 将文件信息入库

  • 最后清理Minio的分块文件

2.4.3.1 BigFilesController
/*** @param fileMd5    文件md5值* @param fileName   合并分块之后要入库,fileName原始文件名要写在数据库* @param chunkTotal 总共分块数*/
@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.setFilename(fileName);uploadFileParamsDto.setTags("视图文件");uploadFileParamsDto.setFileType("001002");//数据字典代码 - 001002代表视频return mediaFileService.mergeChunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}
2.4.3.2 MediaFileServiceImpl

其实和2.4.1模块同理

可以在mergeChunks放大TODO1之前下面添加这一段代码

假如说合并后的文件已经存在了,就不需要进行合并了

//TODO 0.如果已经有了合并分块后对应的文件的话,就不用再合并了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){//文件已经存在return RestResponse.success(true);
}
/*** 为什么又companyId 机构ID?* 分布式文件系统空间不是随便使用的,比如某个机构传输的课程很多很多,那我们就可以收费了(比如超过1Tb便开始收费)* 知道了companyId我们就知道是谁传的,也知道这些机构用了多少GB** @param companyId           机构id* @param fileMd5             文件md5* @param chunkTotal          分块总和* @param uploadFileParamsDto 文件信息(要入库)* @return com.xuecheng.base.model.RestResponse* @description 合并分块*/
@Override
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {//TODO 1.获取所有分块文件List<ComposeSource> sources = new ArrayList<>();//1.1 分块文件路径String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);for (int i = 0; i < chunkTotal; i++) {sources.add(ComposeSource.builder()//分块文件所在桶.bucket(bucket_video)//分块文件名称.object(chunkFileFolderPath + i).build());}//1.2 指定合并后文件存储在哪里String filename = uploadFileParamsDto.getFilename();String fileExt = filename.substring(filename.lastIndexOf("."));//1.3 获取对象存储名String filePathByMD5 = this.getFilePathByMD5(fileMd5, fileExt);ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()//指定合并后文件在哪个桶里.bucket(bucket_video)//最终合并后的文件路径及名称.object(filePathByMD5)//指定分块源文件.sources(sources).build();//TODO 2.合并分块try {minioClient.composeObject(composeObjectArgs);} catch (Exception e) {e.printStackTrace();log.error("合并文件出错:bucket:{},objectName:{},错误信息:{}", bucket_video, filePathByMD5, e.getMessage());return RestResponse.validfail(false, "合并文件出错");}//TODO 3.校验合并后的文件与原文件是否一致//3.1校验时先要把文件下载下来File tempFile = this.downloadFileFromMinIO(bucket_video, filePathByMD5);//3.2 比较原文件与临时文件的MD5值//将FileInputStream放在括号里,当try..catch执行结束后会自动关闭流,不用加finally了try (FileInputStream fis = new FileInputStream(tempFile)) {String mergeFile_md5 = DigestUtils.md5Hex(fis);if (!fileMd5.equals(mergeFile_md5)) {log.error("校验合并文件md5值不一致,原始文件{},合并文件{}", fileMd5, mergeFile_md5);return RestResponse.validfail(false, "文件校验失败");}//保存一下文件信息 - 文件大小uploadFileParamsDto.setFileSize(tempFile.length());} catch (IOException e) {e.printStackTrace();return RestResponse.validfail(false, "文件校验失败");}//TODO 4.文件信息MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, filePathByMD5);if (mediaFiles == null) {return RestResponse.validfail(false, "文件入库失败");}//TODO 5.清理分块文件//5.1获取分块文件路径//this.getChunkFileFolderPath(fileMd5);this.clearChunkFiles(chunkFileFolderPath, chunkTotal);return RestResponse.success(true);
}
       /*** 清除分块文件** @param chunkFileFolderPath 分块文件路径* @param chunkTotal          分块文件总数*/private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {//需要参数removeObjectsArgs//Iterable<DeleteObject> objects =List<DeleteObject> objects = Stream.iterate(0, i -> ++i).limit(chunkTotal)//String.concat函数用于拼接字符串.map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i)))).collect(Collectors.toList());RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()//指定要清理的分块文件的桶.bucket(bucket_video)//需要一个Iterable<DeleteObject>迭代器.objects(objects).build();//执行了这段方法并没有真正的删除,还需要遍历一下Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);results.forEach(f->{try {//get方法执行之后才是真正的删除了DeleteError deleteError = f.get();} catch (Exception e) {e.printStackTrace();}});
//        或者是下面这种遍历方式,都是可以的
//        for (Result<DeleteError> deleteError:results){
//            DeleteError error = deleteError.get();
//        }}

最终合并后的路径是在getFilePathByMD5中

/*** @param fileMd5 文件md5值* @param fileExt 文件扩展名*/
private String getFilePathByMD5(String fileMd5, String fileExt) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}

2.4.4 测试

  • 首先修改Springboot-web默认上传文件大小

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

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

    指定单个文件上传的最大大小限制,单个文件的大小如果超过了这个配置的值,将会导致文件上传失败

  • max-request-size

    指定整个 HTTP 请求的最大大小限制,包括所有上传文件和其他请求数据,请求的总大小超过了这个配置的值,将会导致整个请求失败

两个配置项的值可以使用标准的大小单位,比如 KB(千字节)、MB(兆字节)等。在你的例子中,50MB 表示最大文件大小和最大请求大小都被限制为 50 兆字节

  • 点击“上传视频”按钮

image-20231224165736643

  • 点击“添加文件”按钮

image-20231224165809867

  • 上传文件

image-20231224172907840

  • 查看Minio系统

    分块上传挺成功的

image-20231224172946156

  • 合并分块之后

    所有的分块都被清理了,然后合并成一个文件

  • 经过我的2.4.3与2.4.1优化之后,上传挺正常了

image-20231224230839650

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

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

相关文章

网络通信--深入理解网络和TCP / IP协议

计算机网络体系结构 TCP/IP协议族 TCP / IP 网络传输中的数据术语 网络通信中的地址和端口 window端查看IP地址和MAC地址&#xff1a;ipconfig -all MAC层地址是在数据链路层的&#xff1b;IP工作在网络层的 MAC是48个字节&#xff0c;IP是32个字节 在子网&#xff08;局域…

MFC 动态创建机制

目录 动态创建机制概述 代码测试分析 执行过程 总结 动态创建机制概述 MFC 动态创建机制是 MFC 中的一项重要功能&#xff0c;它允许开发者在运行时动态创建和管理窗口控件。通过动态创建机制&#xff0c;开发者可以根据需要在程序运行过程中创建、显示和销毁窗口&#xf…

微信小程序(uniapp)api讲解

Uniapp是一个基于Vue.js的跨平台开发框架&#xff0c;可以同时开发微信小程序、H5、App等多个平台的应用。下面是Uniapp常用的API讲解&#xff1a; Vue.js的API Uniapp采用了Vue.js框架&#xff0c;因此可以直接使用Vue.js的API。例如&#xff1a;v-show、v-if、v-for、comput…

java八股 spring + mybatis

Spring常用注解&#xff08;绝对经典&#xff09;_spring注解-CSDN博客 框架篇-02-Spring-单例bean是线程安全的吗_哔哩哔哩_bilibili 1.spring.bean 单例 线程不安全 2.AOP 项目里可以说记录用户登录日志&#xff0c;利用request去获取姓名、ip、、请求方式、url&#xff0…

基于HC-SR04传感器的避障机器人设计与实现

本文介绍了如何设计和实现一个基于HC-SR04超声波传感器的避障机器人。我们将详细讨论硬件和电路连接&#xff0c;并提供完整的Arduino代码。该机器人可以利用超声波传感器检测周围的障碍物&#xff0c;并采取相应的动作进行避障&#xff0c;实现自主导航。 引言&#xff1a; 避…

【数据库系统概论】第3章-关系数据库标准语言SQL(1)

文章目录 3.1 SQL概述3.2 学生-课程数据库3.3 数据定义3.3.1 数据库定义3.3.2 模式的定义3.3.3 基本表的定义3.3.4 索引的建立与删除3.3.5 数据字典 3.1 SQL概述 动词 分类 三级模式 3.2 学生-课程数据库 3.3 数据定义 3.3.1 数据库定义 创建数据库 tips&#xff1a;[ ]表…

案例163:基于微信小程序的校园二手交易平台系统设计与开发

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

Unity之DOTweenPath轨迹移动

Unity之DOTweenPath轨迹移动 一、介绍 DOTweenPath二、操作说明1、Scene View Commands2、INfo3、Tween Options4、Path Tween Options5、Path Editor Options&#xff1a;轨迹编辑参数&#xff0c;就不介绍了6、ResetPath&#xff1a;重置轨迹7、Events&#xff1a;8、WayPoin…

Flutter windows 环境配置

Flutter windows 环境配置 从零开始&#xff0c;演示flutter环境配置到启动项目&#xff0c;同时支持 vscode 和 android studio 目录 Flutter windows 环境配置一、环境配置1. Flutter SDK2. Android Studio3. JDK4. 拓展安装5. Visual Studio 2022二、项目创建和启动1. vsco…

Hadoop(2):常见的MapReduce[在Ubuntu中运行!]

1 以词频统计为例子介绍 mapreduce怎么写出来的 弄清楚MapReduce的各个过程&#xff1a; 将文件输入后&#xff0c;返回的<k1,v1>代表的含义是&#xff1a;k1表示偏移量&#xff0c;即v1的第一个字母在文件中的索引&#xff08;从0开始数的&#xff09;&#xff1b;v1表…

记pbcms网站被攻击,很多标题被篡改(1)

记得定期打开网站看看哦! 被攻击后的网站异常表现:网页内容缺失或变更,页面布局破坏,按钮点击无效,...... 接着查看HTML、CSS、JS文件,发现嵌入了未知代码! 攻击1:index.html 或其他html模板页面的标题、关键词、描述被篡改(俗称,被挂马...),如下: 攻击2:在ht…

leetcode——背包问题汇总

本章来汇总一下leetcode中做过的背包问题&#xff0c;包括0-1背包和完全背包。 背包问题的通常形式为&#xff1a;有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。求解将哪些物品装入背包里物品价值总和最大。0-1背包和…