最好网站开发公司电话,网页设计类型与风格,wordpress postgre,电脑基础培训班哪里有文章目录 一、上传图片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 MediaFilesMapper1.6 MediaFileServic… 文章目录 一、上传图片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 MediaFilesMapper1.6 MediaFileServiceImpl1.7 事物优化1.8 测试 二、上传视频2.0 响应结果类2.1 断点续传2.2 测试Demo2.2.1 分块测试2.2.2 合并测试 2.3 测试MinIo2.3.1 上传分块2.3.2 合并分块 2.4 上传视频2.4.1 文件上传前校验2.4.1.1 BigFilesController2.4.1.2 MediaFileServiceImpl 2.4.2 上传分块文件2.4.2.1 BigFilesController2.4.2.2 MediaFileServiceImpl 2.4.3 合并分块文件2.4.3.1 BigFilesController2.4.3.2 MediaFileServiceImpl 2.4.4 测试 一、上传图片 此模块涉及到修改/添加课程之前已经写过接口了在下面这篇文章中 3.2 内容管理模块 - 课程分类、新增课程、修改课程-CSDN博客 1.1 需求分析
我们的文件要存储在minio文件系统中
在新增课程界面上传课程图片也可以修改课程图片 这一步上传图片是请求的媒资服务接口并不是内容管理服务接口 在课程基本信息中有一个“pic”字段要存储图片的路径 pic字段值类似“/mediafiles/2022/09/18/a16da7a132559daf9e1193166b3e7f52.jpg”其中“mediafiles”就是桶后面就是文件具体的对象名 在我们前端功能.env环境变量配置文件定义了图片服务器地址 如果想要展示图片那就要将“http://192.168.101.65:9000”拼接上pic字段值 为什么不将“http://192.168.101.65:9000”直接写在数据库里 因为如果上线之后地址就不好修改了 为什么要单独设置一个媒资管理服务
①管理文件的信息我们使用media_files表记录文件信息
不管是图片、视频、文档全会记录在这个表中 ②把文件本身放在分布式文件系统(minio)中管理起来
方便管理媒资服务就是一个统一的文件管理功能 详细流程 1、前端进入上传图片界面
2、上传图片请求媒资管理服务。
3、媒资管理服务将图片文件存储在MinIO。
4、媒资管理记录文件信息到数据库。
5、前端请求内容管理服务保存课程信息在内容管理数据库保存图片地址。
1.2 数据模型
我们怎么区分文件是否已经上传
可以通过文件的MD5值同一个文件的MD5值相同
前端上传的时候就会把文件MD5值传给我们后台后台拿着MD5值去数据库查询如果有这个MD5值了说明文件已经上传成功了不需要再上传了
1.2.1 media_files 媒资信息表 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字段存储课程图片的路径 1.3 准备Minio环境
1.3.1 桶环境 mediafiles桶 视频以外的文件都存储在这个桶内 video 桶 存放视频
并且两个桶的Access Policy权限都要改成public 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?serverTimezoneUTCuserUnicodetrueuseSSLfalseusername: 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: dev1.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 接口定义
下面的内容其实是完成标红的地方 做这一部分的时候可能发现course_base课程基本信息表和media_files表没有关联那怎么将图片的url存放到course_base课程基本信息表中的pic字段呢 当上传完图片之后会点击保存这个时候就会对course_base表中的各种信息进行操作了 但是我感觉没有很好的方法让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; namefiledata; 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 BaseMapperMediaFiles {}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 文件信息*/TransactionalOverridepublic 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注解代理对象执行此方法前会开启事务 如果在uploadFile方法上没有Transactional注解代理对象执行此方法前不进行事务控制 断该方法是否可以事务控制必须保证是通过代理对象调用此方法且此方法上添加了Transactional注解 在addMediaFilesToDb方法上添加Transactional注解也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法
如下图所示在addMediaFilesToDb方法中的this指代的并不是代理对象uploadFile方法中this指代的也不是代理对象 我们在Controller使用AutoWired的注解引入的MediaFileService对象是代理对象怎么在调用的时候就不是了呢 在MediaFileServiceImpl类中this指代的就是当前类不是代理对象 代理对象其实是在原始对象上包了一层然后调用原始对象上的方法而已 为了解决这个问题我们可以在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生成的代理对象挺完美的 1.8 测试
测试上传文件 二、上传视频
上传视频的位置如下所示 2.0 响应结果类
/*** 通用结果类*/
Data
ToString
public class RestResponseT {/*** 响应编码,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 RestResponseT validfail(String msg) {RestResponseT response new RestResponseT();response.setCode(-1);response.setMsg(msg);return response;}public static T RestResponseT validfail(T result, String msg) {RestResponseT response new RestResponseT();response.setCode(-1);response.setResult(result);response.setMsg(msg);return response;}/*** 添加正常响应数据包含响应内容** return RestResponse Rest服务封装相应数据*/public static T RestResponseT success(T result) {RestResponseT response new RestResponseT();response.setResult(result);return response;}public static T RestResponseT success(T result, String msg) {RestResponseT response new RestResponseT();response.setResult(result);response.setMsg(msg);return response;}/*** 添加正常响应数据不包含响应内容** return RestResponse Rest服务封装相应数据*/public static T RestResponseT success() {return new RestResponseT();}public Boolean isSuccessful() {return this.code 0;}
}2.1 断点续传
假如说用户上传了一个大文件但是用户的网不是很好上传了一部分网断了这个时候就需要一个断点续传的需求了
通常视频文件都比较大所以对于媒资系统上传文件的需求要满足大文件的上传要求。
http协议本身对上传文件大小没有限制但是客户的网络环境质量、电脑硬件环境等参差不齐如果一个大文件快上传完了网断了没有上传完成需要客户重新上传用户体验非常差所以对于大文件上传的要求最基本的是断点续传
断点续传在下载或上传时将下载或上传任务一个文件或一个压缩包人为的划分为几个部分每一个部分采用一个线程进行上传或下载如果碰到网络故障可以从已经上传或下载的部分开始继续上传下载未完成的部分而没有必要从头开始上传下载断点续传可以提高节省操作时间提高用户体验性 流程如下
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 代表1M5M的话乘5即可也就是最小单位是字节byte 1024个byte是1kint 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”还是写rwRandomAccessFile 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();
}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();//将数组转换成ListListFile fileList Arrays.asList(files);//利用Comparator进行排序Collections.sort(fileList, new ComparatorFile() {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(合并文件失败);}
}2.3 测试MinIo 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()成功);}
}2.3.2 合并分块
分块已经在Minio了我们调用Minio提供的SDK即可合并分块
/*** 调用minio接口合并分块*/
Test
public void merge() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {//分块文件集合ListComposeSource 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 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 RestResponseBoolean checkfile(RequestParam(fileMd5) String fileMd5) {return mediaFileService.checkFile(fileMd5);
}/*** 分块在数据库中是不存储的但是可以向minio中查询分块是否存在* minio中有了就不再传了若没有的话再传*/
ApiOperation(value 分块文件上传前的检测)
PostMapping(/upload/checkchunk)
public RestResponseBoolean 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.RestResponsejava.lang.Boolean false不存在true存在* description 检查文件是否存在*/
Override
public RestResponseBoolean 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检查分块更不会合并分块 下面的TODO 1便是我新添加上的只需要加载Todo2之前即可 2.4.3 合并分块文件模块也要加 //TODO 1.判断fileMd5对应的文件已经合并成一个完整的文件了如果有了的话那也不需要检查分块了
//判断Minio系统中是否有已经有合并的文件了如果有的话没有分块所在路径也无所谓
RestResponseBoolean booleanRestResponse this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){//文件已经存在return RestResponse.success(true);
}/*** 分块不会存在于数据库直接查询minio系统即可** param fileMd5 文件的md5* param chunkIndex 分块序号* return com.xuecheng.base.model.RestResponsejava.lang.Boolean false不存在true存在* description 检查分块是否存在*/
Override
public RestResponseBoolean checkChunk(String fileMd5, int chunkIndex) {//TODO 2.根据MD5得到分块文件的目录路径//分块存储路径md5前两位为两个目录MD5值也是一层目录chunk目录存储分块文件String chunkFileFolderPath this.getChunkFileFolderPath(fileMd5);GetObjectArgs getObjectArgs GetObjectArgs.builder()//视频的桶.bucket(bucket_video)//文件名是目录路径分块序号.object(chunkFileFolderPathchunkIndex).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系统中是否有已经有合并的文件了如果有的话没有分块所在路径也无所谓
RestResponseBoolean 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.获取所有分块文件ListComposeSource 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//IterableDeleteObject objects ListDeleteObject 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)//需要一个IterableDeleteObject迭代器.objects(objects).build();//执行了这段方法并没有真正的删除还需要遍历一下IterableResultDeleteError results minioClient.removeObjects(removeObjectsArgs);results.forEach(f-{try {//get方法执行之后才是真正的删除了DeleteError deleteError f.get();} catch (Exception e) {e.printStackTrace();}});
// 或者是下面这种遍历方式都是可以的
// for (ResultDeleteError 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默认上传文件大小
前端对文件分块的大小为5MBSpringBoot web默认上传文件的大小限制为1MB这里需要在media-api工程修改配置如下
spring:servlet:multipart:max-file-size: 50MBmax-request-size: 50MBmax-file-size 指定单个文件上传的最大大小限制单个文件的大小如果超过了这个配置的值将会导致文件上传失败 max-request-size 指定整个 HTTP 请求的最大大小限制包括所有上传文件和其他请求数据请求的总大小超过了这个配置的值将会导致整个请求失败 两个配置项的值可以使用标准的大小单位比如 KB千字节、MB兆字节等。在你的例子中50MB 表示最大文件大小和最大请求大小都被限制为 50 兆字节 点击“上传视频”按钮 点击“添加文件”按钮 上传文件 查看Minio系统 分块上传挺成功的 合并分块之后 所有的分块都被清理了然后合并成一个文件 经过我的2.4.3与2.4.1优化之后上传挺正常了