正能量erp软件下载网站,教育网站改版方案,浙江省建设厅网站证件,深圳网站建设文章目录 媒资管理5 上传视频5.1 需求分析5.2 断点续传技术5.2.1 什么是断点续传5.2.2 分块与合并测试5.2.3 视频上传流程5.2.4 minio合并文件测试 5.3 接口定义5.4 上传分块开发5.4.1 DAO开发5.4.2 Service开发5.4.2.1 检查文件和分块5.4.2.2 上传分块5.4.2.3 上传分块测试 5.… 文章目录 媒资管理5 上传视频5.1 需求分析5.2 断点续传技术5.2.1 什么是断点续传5.2.2 分块与合并测试5.2.3 视频上传流程5.2.4 minio合并文件测试 5.3 接口定义5.4 上传分块开发5.4.1 DAO开发5.4.2 Service开发5.4.2.1 检查文件和分块5.4.2.2 上传分块5.4.2.3 上传分块测试 5.5 合并分块开发5.5.1 service开发5.4.3 接口层完善5.5.2 合并分块测试 5.6其它问题5.6.1 分块文件清理问题 媒资管理
5 上传视频
5.1 需求分析
1、教学机构人员进入媒资管理列表查询自己上传的媒资文件。 点击“媒资管理”
进入媒资管理列表页面查询本机构上传的媒资文件。
2、教育机构用户在媒资管理页面中点击 “上传视频” 按钮。
点击“上传视频”打开上传页面
3、选择要上传的文件自动执行文件上传。
4、视频上传成功会自动处理处理完成可以预览视频。
5.2 断点续传技术
5.2.1 什么是断点续传
通常视频文件都比较大所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制但是客户的网络环境质量、电脑硬件环境等参差不齐如果一个大文件快上传完了网断了没有上传完成需要客户重新上传用户体验非常差所以对于大文件上传的要求最基本的是断点续传。 什么是断点续传 引用百度百科断点续传指的是在下载或上传时将下载或上传任务一个文件或一个压缩包人为的划分为几个部分每一个部分采用一个线程进行上传或下载如果碰到网络故障可以从已经上传或下载的部分开始继续上传下载未完成的部分而没有必要从头开始上传下载断点续传可以提高节省操作时间提高用户体验性。 断点续传流程如下图
流程如下 1、前端上传前先把文件分成块 2、一块一块的上传上传中断后重新上传已上传的分块则不用再上传 3、各分块上传完成最后在服务端合并文件
5.2.2 分块与合并测试
为了更好的理解文件分块上传的原理下边用java代码测试文件的分块与合并。 文件分块的流程如下 1、获取源文件长度 2、根据设定的分块文件的大小计算出块数 3、从源文件读数据依次向每一个块文件写数据。 测试代码如下
Java
package com.xuecheng.media;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.*;/*** author Mr.M* version 1.0* description 大文件处理测试* date 2022/9/13 9:21*/
public class BigFileTest {//测试文件分块方法Testpublic void testChunk() throws IOException {File sourceFile new File(d:/develop/bigfile_test/nacos.mp4);String chunkPath d:/develop/bigfile_test/chunk/;File chunkFolder new File(chunkPath);if (!chunkFolder.exists()) {chunkFolder.mkdirs();}//分块大小long 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 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、依次从合并的文件中读取数据向合并文件写入数 文件合并的测试代码
Java//测试文件合并方法Testpublic void testMerge() throws IOException {//块文件目录File chunkFolder new File(d:/develop/bigfile_test/chunk/);//原始文件File originalFile new File(d:/develop/bigfile_test/nacos.mp4);//合并文件File mergeFile new File(d:/develop/bigfile_test/nacos01.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();// 转成集合便于排序ListFile fileList Arrays.asList(fileArray);// 从小到大排序Collections.sort(fileList, new ComparatorFile() {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(合并文件失败);}}}5.2.3 视频上传流程
下图是上传视频的整体流程
1、前端对文件进行分块。 2、前端上传分块文件前请求媒资服务检查文件是否存在如果已经存在则不再上传。 3、如果分块文件不存在则前端开始上传 4、前端请求媒资服务上传分块。 5、媒资服务将分块上传至MinIO。 6、前端将分块上传完毕请求媒资服务合并分块。 7、媒资服务判断分块上传完成则请求MinIO合并文件。 8、合并完成校验合并后的文件是否完整如果不完整则删除文件。
5.2.4 minio合并文件测试
1、将分块文件上传至minio
Java
//将分块文件上传至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的合并文件
Java
//合并文件要求分块文件最小5M
Test
public void test_merge() throws Exception {ListComposeSource 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(){//合并分块完成将分块文件清除ListDeleteObject 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();IterableResultDeleteError 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再次测试。
5.3 接口定义
根据上传视频流程定义接口与前端的约定是操作成功返回{code:0}否则返回{code:-1} 从课程资料中拷贝RestResponse.java类到base工程下的model包下。 定义接口如下
Java
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;/*** author Mr.M* version 1.0* description 大文件上传接口* date 2022/9/6 11:29*/
Api(value 大文件上传接口, tags 大文件上传接口)
RestController
public class BigFilesController {ApiOperation(value 文件上传前检查文件)PostMapping(/upload/checkfile)public RestResponseBoolean checkfile(RequestParam(fileMd5) String fileMd5) throws Exception {return null;}ApiOperation(value 分块文件上传前的检测)PostMapping(/upload/checkchunk)public RestResponseBoolean checkchunk(RequestParam(fileMd5) String fileMd5,RequestParam(chunk) int chunk) throws Exception {return null;}ApiOperation(value 上传分块文件)PostMapping(/upload/uploadchunk)public RestResponse uploadchunk(RequestParam(file) MultipartFile file,RequestParam(fileMd5) String fileMd5,RequestParam(chunk) int chunk) throws Exception {return null;}ApiOperation(value 合并文件)PostMapping(/upload/mergechunks)public RestResponse mergechunks(RequestParam(fileMd5) String fileMd5,RequestParam(fileName) String fileName,RequestParam(chunkTotal) int chunkTotal) throws Exception {return null;}}5.4 上传分块开发
5.4.1 DAO开发
向媒资数据库的文件表插入记录使用自动生成的Mapper接口即可满足要求。
5.4.2 Service开发
5.4.2.1 检查文件和分块
接口完成进行接口实现首先实现检查文件方法和检查分块方法。 在MediaFileService中定义service接口如下
Java
/*** description 检查文件是否存在* param fileMd5 文件的md5* return com.xuecheng.base.model.RestResponsejava.lang.Boolean false不存在true存在* author Mr.M* date 2022/9/13 15:38
*/
public RestResponseBoolean checkFile(String fileMd5);/*** description 检查分块是否存在* param fileMd5 文件的md5* param chunkIndex 分块序号* return com.xuecheng.base.model.RestResponsejava.lang.Boolean false不存在true存在* author Mr.M* date 2022/9/13 15:39
*/
public RestResponseBoolean checkChunk(String fileMd5, int chunkIndex);service接口实现方法
Override
public RestResponseBoolean 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 RestResponseBoolean 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 /;
}5.4.2.2 上传分块
定义service接口
Java
/*** description 上传分块* param fileMd5 文件md5* param chunk 分块序号* param bytes 文件字节* return com.xuecheng.base.model.RestResponse* author Mr.M* date 2022/9/13 15:50
*/
public RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);接口实现
JavaOverride
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {//得到分块文件的目录路径String chunkFileFolderPath getChunkFileFolderPath(fileMd5);//得到分块文件的路径String chunkFilePath chunkFileFolderPath chunk;try {//将文件存储至minIOaddMediaFilesToMinIO(bytes, bucket_videoFiles,chunkFilePath);return RestResponse.success(true);} catch (Exception ex) {ex.printStackTrace();log.debug(上传分块文件:{},失败:{},chunkFilePath,e.getMessage());}return RestResponse.validfail(false,上传分块失败);
}5.4.2.3 上传分块测试
完善接口层
Java
ApiOperation(value 文件上传前检查文件)
PostMapping(/upload/checkfile)
public RestResponseBoolean checkfile(RequestParam(fileMd5) String fileMd5
) throws Exception {return mediaFileService.checkFile(fileMd5);
}ApiOperation(value 分块文件上传前的检测)
PostMapping(/upload/checkchunk)
public RestResponseBoolean 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);
}启动前端工程进入上传视频界面进行前后端联调测试。
前端对文件分块的大小为5MBSpringBoot web默认上传文件的大小限制为1MB这里需要在media-api工程修改配置如下
spring:servlet:multipart:max-file-size: 50MBmax-request-size: 50MBmax-file-size单个文件的大小限制 Max-request-size: 单次请求的大小限制
5.5 合并分块开发
5.5.1 service开发
定义service接口
/*** description 合并分块* param companyId 机构id* param fileMd5 文件md5* param chunkTotal 分块总和* param uploadFileParamsDto 文件信息* return com.xuecheng.base.model.RestResponse* author Mr.M* date 2022/9/13 15:56*/
public RestResponse mergechunks(Long companyId,String fileMd5,int chunkTotal,UploadFileParamsDto uploadFileParamsDto);service实现
Java
Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {//获取分块文件路径String chunkFileFolderPath getChunkFileFolderPath(fileMd5);//组成将分块文件路径组成 ListComposeSourceListComposeSource 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, 合并文件失败。);}// 验证md5File 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下载文件* param bucket 桶* param 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());//创建临时文件minioFileFile.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;
}
/*** 得到合并后的文件的地址* param fileMd5 文件id即md5值* param fileExt 文件扩展名* return*/
private String getFilePathByMd5(String fileMd5,String fileExt){return fileMd5.substring(0,1) / fileMd5.substring(1,2) / fileMd5 / fileMd5 fileExt;
}/*** 清除分块文件* param chunkFileFolderPath 分块文件路径* param chunkTotal 分块文件总数*/
private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){try {ListDeleteObject 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();IterableResultDeleteError 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);}
}5.4.3 接口层完善
下边完善接口层
Java
ApiOperation(value 文件上传前检查文件)
PostMapping(/upload/checkfile)
public RestResponseBoolean checkfile(RequestParam(fileMd5) String fileMd5
) throws Exception {return mediaFileService.checkFile(fileMd5);
}ApiOperation(value 分块文件上传前的检测)
PostMapping(/upload/checkchunk)
public RestResponseBoolean 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 {return mediaFileService.uploadChunk(fileMd5,chunk,file.getBytes());
}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);}5.5.2 合并分块测试
下边进行前后端联调 1、上传一个视频测试合并分块的执行逻辑 进入service方法逐行跟踪。 2、断点续传测试 上传一部分后停止刷新浏览器再重新上传通过浏览器日志发现已经上传过的分块不再重新上传
5.6其它问题
5.6.1 分块文件清理问题
上传一个文件进行分块上传上传一半不传了之前上传到minio的分块文件要清理吗怎么做的 1、在数据库中有一张文件表记录minio中存储的文件信息。 2、文件开始上传时会写入文件表状态为上传中上传完成会更新状态为上传完成。 3、当一个文件传了一半不再上传了说明该文件没有上传完成会有定时任务去查询文件表中的记录如果文件未上传完成则删除minio中没有上传成功的文件目录。