【MinIO】文件断点续传和分块合并

news2025/1/16 5:37:47

【MinIO】文件断点续传和分块合并

文章目录

  • 【MinIO】文件断点续传和分块合并
    • 0. 准备工作
    • 1. 检查文件是否存在
      • 1.1 定义接口
      • 1.2 编写实现方法
    • 2. 检查分块文件是否存在
      • 2.1 定义接口
      • 2.2 编写实现方法
    • 3. 上传分块文件接口
      • 3.1 定义接口
      • 3.2 编写实现方法
    • 4. 合并分块文件接口
      • 4.1 定义接口
      • 4.2 编写实现方法

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

image-20230228095107635

由图可知,我们需要在前端定义四个接口。

  1. 检查文件是否存在接口
  2. 检查分块文件是否存在接口
  3. 上传分块文件接口
  4. 合并分块文件接口

0. 准备工作

编写一个配置类,向Spring容器中注入一个minio客户端。

@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;

    @Bean
    public MinioClient minioClient() {

        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint(endpoint)
                        .credentials(accessKey, secretKey)
                        .build();
        return minioClient;
    }

}

1. 检查文件是否存在

1.1 定义接口

@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {
    return mediaFileService.checkFile(fileMd5);
}

1.2 编写实现方法

检查文件是否存在,必须同时满足两个条件:

  1. 在数据库的文件表中存在记录
  2. 在文件系统中存在文件

只有满足以上两个条件才表示文件存在,任何一项不满足都将返回false。

@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
    //在数据库中存在并且文件系统中也存在,才说明真的才存在
    MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
    if (mediaFiles == null) {
        return RestResponse.success(false);
    }
    //查看是否在文件系统存在
    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();
    try {
        InputStream inputStream = minioClient.getObject(getObjectArgs);
        if (inputStream == null) {
            //文件不存在
            return RestResponse.success(false);
        }
    } catch (Exception e) {
        e.printStackTrace();
        return RestResponse.success(false);
    }
    return RestResponse.success(true);
}

2. 检查分块文件是否存在

2.1 定义接口

@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.2 编写实现方法

想要查询分块文件是否在文件系统中,我们必须要指导分块文件所在的路径。

如果将所有的文件都存在同一个目录,将会导致IO效率低下。所以我们应该尽可能使文件分撒存储在不同目录(同一个文件的分块文件还得在同一目录下)。

我们指定一个规则:

假设一个文件的MD5值为:1374c8160ea2da8dd33208a9ad369641,那么我们就取第一个数为一级目录的名字,取第二个数为二级目录的名字。那么这个文件的分块文件都存在 video/1/3/1374c8160ea2da8dd33208a9ad369641/chunk 目录下,源文件存储在 video/1/3/1374c8160ea2da8dd33208a9ad369641 目录下。

根据这一个规则,我们可以编写一个获得分块文件所在目录的方法:

//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
    //将文件MD5值的第一位数作为一级目录,第二位数作为二级目录
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}

接着编写检查分块文件是否存在的方法:

如果存在则返回true,不存在都返回false。

@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
    //得到分块文件所在目录
    String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
    //分块文件的路径
    String chunkFilePath = chunkFileFolderPath + chunkIndex;

    //查看是否在文件系统中存在
    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();
    try {
        InputStream inputStream = minioClient.getObject(getObjectArgs);
        if (inputStream == null) {
            //文件不存在
            return RestResponse.success(false);
        }
    } catch (Exception e) {
        e.printStackTrace();
        return RestResponse.success(false);
    }
    return RestResponse.success(true);
}

3. 上传分块文件接口

3.1 定义接口

@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());
}

3.2 编写实现方法

实现方法涉及文件上传,但是文件上传不是只有在上传视频时才使用到,图片和文档的上传也同样使用得到。所以我们应该编写一个通用的上传文件方法。

通用上传文件代码:

private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
    //资源的媒体类型
    String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流
    if (objectName.indexOf(".") >= 0) {
        //取objectName中的扩展名
        String extension = objectName.substring(objectName.lastIndexOf("."));
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        if (extensionMatch != null) {
            contentType = extensionMatch.getMimeType();
        }
    }
    try {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket)
                .object(objectName)
                //InputStream stream, long objectSize 对象大小,long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000分片)
                .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                .contentType(contentType)
                .build();
        //上传到minio
        minioClient.putObject(putObjectArgs);
    } catch (Exception e) {
        e.printStackTrace();
        log.debug("上传文件到文件系统出错:{}", e.getMessage());
        XueChengPlusException.cast("上传文件出错!");
    }
}

上传分块文件代码:

@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
    //得到分块文件的目录路径
    String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
    //得到分块文件的路径
    String chunkFilePath = chunkFileFolderPath + chunk;

    try {
        //将文件存储至minIO
        addMediaFilesToMinIO(bytes, bucket_videofiles, chunkFilePath);
        return RestResponse.success(true);
    } catch (Exception ex) {
        ex.printStackTrace();
        log.debug("上传分块文件:{},失败:{}", chunkFilePath, ex.getMessage());
    }
    return RestResponse.validfail(false, "上传分块失败");
}

4. 合并分块文件接口

4.1 定义接口

@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.setFileType("001002");//视频
    uploadFileParamsDto.setTags("课程视频");
    return mediaFileService.mergechunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}

4.2 编写实现方法

合并分块文件流程:

  1. 下载所有分块
  2. 按顺序合并所有分块
  3. 将合并完的文件上传至文件系统
  4. 将文件信息存入数据库
  5. 关闭流,删除分块文件和临时文件

我们先给实现方法写一个大致的框架:

@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
    //1.下载所有分块
    //2.按顺序合并所有分块
    //3.将合并完的文件上传至文件系统
    //4.将文件信息存入数据库
}

1)下载所有分块

想要将分块文件合并成新的完整文件必须先将所有的分块文件下载下来,所以我们需要先编写一个下载分块文件的方法:

/***
 * @description 下载分块
 * @param fileMd5
 * @param chunkTotal 分块总数
 * @return java.io.File[] 分块文件数组
 */
private File[] checkChunkStatus(String fileMd5, int chunkTotal) {
    //得到分块文件所在目录
    String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
    File[] chunkFiles = new File[chunkTotal];
    for (int i = 0; i < chunkTotal; i++) {
        //分块文件的路径
        String chunkFilePath = chunkFileFolderPath + i;
        //分块临时文件
        File chunkFile = null;
        try {
            //创建分块临时文件
            chunkFile = File.createTempFile("chunk", null);

        } catch (IOException e) {
            e.printStackTrace();
            XueChengPlusException.cast("创建分块临时文件出错" + e.getMessage());
        }
        //下载分块文件
        chunkFile = downloadFileFromMinIO(chunkFile, bucket_videofiles, chunkFilePath);
        chunkFiles[i] = chunkFile;
    }
    return chunkFiles;
}

下载分块文件的代码中调用了一个通用下载方法:

/***
 * @description //根据桶和文件路径从minio下载文件
 * @param file 
 * @param bucket 桶名字
 * @param objectName 文件名路径
*/
public File downloadFileFromMinIO(File file, String bucket, String objectName) {
    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
    try (
            InputStream inputStream = minioClient.getObject(getObjectArgs);
            FileOutputStream outputStream = new FileOutputStream(file);
    ) {
        IOUtils.copy(inputStream, outputStream);
        return file;
    } catch (Exception e) {
        XueChengPlusException.cast("查询分块文件出错");
    }
    return null;
}

2)按顺序合并所有分块

创建一个临时文件,循环将分块文件数组写入这个临时文件,合并完成后验证这个合并后的文件和源文件是否相同?

我们只需要比对这两个文件的MD5值就可以判断是否相同。


3)将合并完的文件上传至文件系统

验证通过之后,我们就需要将文件上传至文件系统了,我们不能使用之前参数包含byte数组的上传方法,这样会导致内存被大量占用。

所以我们就需要编写一个不靠字节数组上传的方法,不靠字节数组靠什么呢?靠文件路径,因为我们创建的临时合并文件就存在文件系统中。

首先还是得定义一个规则,合并文件存储在哪?上面我们已经定义好了分块文件的路径,所以我们合并文件决定放在分块文件的上一级目录。

编写一个得到合并文件路径的方法:

/***
 * @description 得到保存文件的目录
 * @param fileMd5 文件MD5
 * @param fileExt 文件后缀名
*/
private String getFilePathByMd5(String fileMd5, String fileExt) {
    //将文件MD5值的第一位数作为一级目录,第二位数作为二级目录
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}

如上面所说,我们还需要编写一个按照文件路径上传的方法:

//将文件上传到分布式文件系统
private void addMediaFilesToMinIO(String filePath, String bucket, String objectName) {
    try {
        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                .bucket(bucket)
                .object(objectName)//同一个桶内对象名不能重复
                .filename(filePath)
                .build();
        //上传
        minioClient.uploadObject(uploadObjectArgs);
        log.debug("文件上传成功:{}", filePath);
    } catch (Exception e) {
        log.debug("文件上传失败");
        XueChengPlusException.cast("文件上传到文件系统失败");
    }
}

6)完整实现方法

步骤四和步骤五直接跳过,上完整实现方法:

@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
    //下载分块
    File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);

    //得到合并后文件的扩展名
    String filename = uploadFileParamsDto.getFilename();
    //扩展名
    String extension = filename.substring(filename.lastIndexOf("."));
    //创建一个临时文件作为合并文件
    File tempMergeFile = null;
    try {
        try {
            tempMergeFile = File.createTempFile("merge", extension);
        } catch (IOException e) {
            e.printStackTrace();
            XueChengPlusException.cast("创建临时合并文件出错");
        }

        //合并分块
        //创建临时合并文件的流对象
        try (RandomAccessFile raf_write = new RandomAccessFile(tempMergeFile, "rw");) {

            byte[] b = new byte[1024];
            for (File file : chunkFiles) {
                //读取分块文件的流对象
                try (RandomAccessFile raf_read = new RandomAccessFile(file, "r");) {
                    int len = -1;
                    while ((len = raf_read.read(b)) != -1) {
                        //向合并文件写数据
                        raf_write.write(b, 0, len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            XueChengPlusException.cast("合并文件过程出错");
        }

        //校验合并后的文件是否正确
        try {
            FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);
            String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);
            if (!fileMd5.equals(mergeMd5Hex)) {
                log.debug("合并文件校验不通过,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);
                XueChengPlusException.cast("合并文件校验不通过");
            }
        } catch (IOException e) {
            e.printStackTrace();
            XueChengPlusException.cast("合并文件校验出错");
        }

        //得到合并文件在minio的存储路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extension);
        //将合并后的文件上传至文件系统
        addMediaFilesToMinIO(tempMergeFile.getAbsolutePath(), bucket_videofiles, mergeFilePath);

        //将合并后的文件信息上传至数据库
        //合并文件的大小
        uploadFileParamsDto.setFileSize(tempMergeFile.length());
        addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, mergeFilePath);
        return RestResponse.success(true);
    } finally {
        //删除临时分块文件
        if (chunkFiles != null) {
            for (File chunkFile : chunkFiles) {
                if (chunkFile.exists()) {
                    chunkFile.delete();
                }
            }
        }

        //删除合并的临时文件
        if (tempMergeFile != null) {
            tempMergeFile.delete();
        }
    }
}

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

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

相关文章

如何判断一个客户是大客户?

米茂搜对主要外贸销售客户的识别方法整理如下&#xff1a;1. 确定研究目标。通过对客户数据的收集和分析&#xff0c;找出大客户&#xff0c;对大客户实施个性化管理&#xff0c;并对其服务进行跟踪&#xff0c;以及。不时地改善服务&#xff0c;以保持他们的忠诚度。2. 扩大信…

ChatGPT 引爆全网热议,如果当它是“聊天机器人”,那你可就错了

近日来&#xff0c;智能聊天机器人ChatGPT的出现引发众多网友讨论&#xff0c;那它到底是什么呢&#xff1f; 2022年11月&#xff0c;人工智能公司OpenAI推出了一款聊天机器人&#xff1a;ChatGPT。它能够通过学习和理解人类语言来进行对话&#xff0c;还能与聊天对象进行有逻…

骨传导耳机对骨头有影响吗?骨传导耳机好不好

首先很明确的告诉你&#xff0c;骨传导耳机对骨头是没有影响的&#xff0c;骨传导耳机反倒可以起到保护听力的作用。 骨传导是一种声音传导方式&#xff0c;即将声音转化为不同频率的机械振动&#xff0c;通过人的颅骨、骨迷路、内耳淋巴液传递&#xff0c;螺旋器、听神经、听觉…

【转载】bootstrap自定义样式-bootstrap侧边导航栏的实现

bootstrap自带的响应式导航栏是向下滑动的&#xff0c;但是有时满足不了个性化的需求: 侧滑栏使用定位fixed 使用bootstrap响应式使用工具类 visible-sm visible-xs hidden-xs hidden-sm等对不同屏幕适配 侧滑栏的侧滑效果不使用jquery方法来实现&#xff0c;使用的是css3 tr…

02-27 周一 图解机器学习SVM-人脸识别之PCA降维

02-27 周一 图解机器学习SVM分类时间版本修改人描述2023年2月27日09:48:38V0.1宋全恒新建文档 简介 本文主要是在试图代码分析图解机器学习这本书中5.5人脸识别分类&#xff08;p60&#xff09;&#xff0c;主要的过程是使用PCA技术和SVM技术进行人脸的分类工作。 准备 数据集…

JavaScript中单例模式这样用

如果希望自己的代码更优雅、可维护性更高以及更简洁&#xff0c;往往离不开设计模式这一解决方案。 在JS设计模式中&#xff0c;最核心的思想&#xff1a;封装变化&#xff08;将变与不变分离&#xff0c;确保变化的部分灵活&#xff0c;不变的部分稳定&#xff09;。 单例模式…

Spring Batch 综合案例实战中

目录 需求一 需求二 转视频版 需求一 需求&#xff1a;先动态生成50w条员工数据&#xff0c;存放再employee.csv文件中 步骤1&#xff1a;定义&#xff1a;DataInitController RestController public class DataInitController {Autowiredprivate IEmployeeService emplo…

arduino-sentry2之卡片篇

欧克,今天在学生的强烈要求下 我又重启arduino的sentry2调试篇 目前实验结果,可以检测到10张交通卡片 也就是如图所示十张 具体视频如下: https://live.csdn.net/v/279170 具体代码如下: #include <Arduino.h> #include <

什么是千年虫?计算机如何开始处理日期?都有哪些时间日期格式化?

目录 “千年虫”漏洞&#xff08;Year 2000 Problem&#xff0c;简称“Y2K”&#xff09; 计算机是怎么开始处理日期的么&#xff1f; 举例1&#xff1a;时间格式化举例( 过滤器) 举例2&#xff1a;时间格式化 自定义私有过滤器(日期格式化) 高性能计数器演示 OLE时间对象…

Vue的组件(注册、局部、组件复用、props、emit、生命周期)全解

文章目录前言知识点组件注册局部组件组件复用组件间通信props 类型检测子父组件通信之 emit动态组件生命周期函数前言 Vue 支持模块化和组件化开发&#xff0c;可以将整个页面进行模块化分割&#xff0c;低耦合高内聚&#xff0c;使得代码可以在各个地方使用。 知识点 组件注册…

python自学之《21天学通Python》(15)——第18章 数据结构基础

数据结构是用来描述一种或多种数据元素之间的特定关系&#xff0c;算法是程序设计中对数据操作的描述&#xff0c;数据结构和算法组成了程序。对于简单的任务&#xff0c;只要使用编程语言提供的基本数据类型就足够了。而对于较复杂的任务&#xff0c;就需要使用比基本的数据类…

华三OSPF 综合实验

OSPF 实验 实验拓扑 实验需求 按照图示配置 IP 地址按照图示分区域配置 OSPF &#xff0c;实现全网互通为了路由结构稳定&#xff0c;要求路由器使用环回口作为 Router-id&#xff0c;ABR 的环回口宣告进骨干区域 实验解法 1.配置 IP 地址部分 2.按照图示分区域配置 OS…

FFmpeg从入门到入魔(1):初探FFmpeg框架

1. FFmpeg介绍与裁剪1.1 FFmpeg简介FFmpeg&#xff08;Fast forword mpeg&#xff0c;音视频转换器&#xff09;是一个开源免费跨平台的视频和音频流方案&#xff0c;它提供了录制/音视频编解码、转换以及流化音视频的完整解决方案。ffmpeg4.0.2源码目录结构如下&#xff1a;目…

为什么IBDP的文凭更受美国大学的青睐?

家长们可以看到&#xff0c;不管是AP还是A-LEVEL这样的课程&#xff0c;都只是单科的课程&#xff08;A-LEVEL也是英国发展出来&#xff0c;AP是针对美国大学设计的&#xff09;&#xff0c;学生是可以针对他们的强项去做选修&#xff0c;比如我的化学很强&#xff0c;那我可以…

第十节 集合

集合 什么是集合 集合就是能储存一批元素的容器。 特征&#xff1a; 集合类型可以不固定&#xff0c;大小也是可变的。 ArrayList集合 ArrayList是集合中的一种&#xff0c;它支持索引。 ArrayList集合的对象获取 public ArrayList()创建一个空的集合对象 ArrayList集合的添加…

Hive 一文读懂

Hive 简介1.1 什么是Hive1&#xff09;hive简介Hive&#xff1a;由Facebook开源用于解决海量结构化日志的数据统计。Hive是基于Hadoop的一个数据仓库工具&#xff0c;可以将结构化的数据文件映射为一张表&#xff0c;并提供类SQL查询功能。2&#xff09;Hive本质&#xff1a;将…

Goframe快速创建项目,并使用Cli工具创建dao、service、logic

GoFrame项目创建与Cli工具创建1.项目创建2.Mysql数据库配置3.Cli工具dao自动生成4.业务模型须知5.Cli工具service/logic自动生成 - 接口6.Controller/Api创建1.项目创建 官网 - 项目创建-init 开发文档 - 目录介绍 官网 - 示例项目 1.gf init 项目名 &#xff08;创建项目…

无法定位程序输入点kernel32.dll,如何修复kernel32.dll

kernel32.dll是Windows操作系统中非常重要的一个系统文件&#xff0c;如果它丢失或损坏可能会导致许多应用程序无法正常运行。今天小编就来给大家详细的讲解一下无法定位程序输入点kernel32.dll&#xff0c;我们要怎么修复这个kernel32.dll缺失的问题。 一.kernel32.dll时候什么…

前端开发环境配置,浏览器跨域配置,代码提交配置git等

这是我目前公司的开发配置文档大家可以参考&#xff1a; 前端文档 1 搭建前端环境 1.1 安装nodejs 1.1.1 nodejs下载地址 https://nodejs.org/dist/v10.15.3/node-v10.15.3-x64.msi&#xff08;win64&#xff09; https://nodejs.org/dist/v10.15.3/node-v10.15.3.pkg&…

查询性能较 Trino/Presto 3-10 倍提升!Apache Doris 极速数据湖分析深度解读

从上世纪 90 年代初 Bill Inmon 在《building the Data Warehouse》一书中正式提出数据仓库这一概念&#xff0c;至今已有超过三十年的时间。在最初的概念里&#xff0c;数据仓库被定义为「一个面向主题的、集成的、相对稳定的、反映历史变化的数据集合&#xff0c;用于支持管理…