Java使用MinIo

MinIo介绍

Minio 是个基于Apache License v2.0开源协议的对象存储服务,虽然轻量,却拥有着不错的性能。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据。

例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。

官网地址:minio

何为对象存储?我们来看下阿里云 OSS(Object Storage Service) 的介绍

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

对于中小型企业,如果不选择存储上云,那么 Minio 是个不错的选择,麻雀虽小,五脏俱全。当然 Minio 除了直接作为对象存储使用,还可以作为云上对象存储服务的网关层,无缝对接到 Amazon S3、MicroSoft Azure。

下载MinIO

既然知道了Minio是做什么的,就来看一下如何下载Minio

Minio官方下载文档

Docker安装minio

Docker如果想安装软件,必须先到 Docker 镜像仓库下载镜像

Docker官方镜像

  1. 寻找Minio镜像

    image-20240327193224316

  2. 下载Minio镜像

    命令 描述
    docker pull minio/minio 下载最新版Minio镜像 (其实此命令就等同于 : docker pull minio/minio:latest )
    docker pull minio/minio:RELEASE.2024-03-26T22-10-45Z.fips 下载指定版本的Minio镜像(xxx指具体版本号)

    检查当前Docker下载的镜像
    docker images

  3. 创建目录
    一个用来存放配置,一个用来存储上传文件的目录
    启动前需要先创建Minio外部挂载的配置文件(/home/minio/config)和存储上传文件的目录(/home/minio/data)

    1
    2
    mkdir -p /home/minio/config
    mkdir -p /home/minio/data
  4. 创建Minio容器并运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    docker run -p 9000:9000 -p 9090:9090 \
    --net=host \
    --name minio \
    -d --restart=always \
    -e "MINIO_ACCESS_KEY=minioadmin" \
    -e "MINIO_SECRET_KEY=minioadmin" \
    -v /home/minio/data:/data \
    -v /home/minio/config:/root/.minio \
    minio/minio server \
    /data --console-address ":9090" -address ":9000"

    9090端口指的是minio的客户端端口

    MINIO_ACCESS_KEY :账号

    MINIO_SECRET_KEY :密码(账号长度必须大于等于5,密码长度必须大于等于8位)

​ 通过docker ps查看是否正在运行

  1. 访问操作

​ 访问: http://你虚拟机的ip:9090/login

​ 用户名和密码填你自己的

​ 创建Bucket

点进去设置访问策略为Public

Windows安装minio

官网页面中有服务端、客户端以及不同语言SDK的依赖包

可以点击DOWNLOAD的下载服务端按钮,或者在Windows PowerShell中执行该命令:

1
Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-amd64/minio.exe" -OutFile "C:\minio.exe"

其中C:\minio.exe为下载的路径位置,可以自行更改

创建一个minio存放文件的目录

设置用户名和密码

1
2
3
4
5
6
7
# 用户环境变量
setx MINIO_ROOT_USER admin
setx MINIO_ROOT_PASSWORD password

#系统环境变量(需管理员)
setx MINIO_ROOT_USER admin /m
setx MINIO_ROOT_PASSWORD password /m

启动

启动需要在cmd里输入命令启动

D:\minio\data 为刚才创建的minio存放文件的目录

1
minin.exe server D:\minio\data

如果 9000 端口被别的服务占用了,可以在启动的时候指定服务的端口

例如指定 9100 和 9101,前提是这两个端口没有被占用

1
minio.exe server D:\minio\data --console-address ":9100" --address ":9101"

访问

在启动后的窗口的WebUI查看访问地址

访问后输入用户名和密码登录进入控制台页面

创建桶

点进去设置访问策略为Public

环境搭建

首先我们需要导入minio的依赖,来使用minio的api

1
2
3
4
5
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>

配置minio的各项属性

1
2
3
4
5
6
7
8
9
10
11
12
# minio的配置
minio:
# minio地址 写你自己的地址
endpoint: http://192.168.140.134:9000
# 用户名 写你自己的
accessKey: accessKey
# 密码 同样写你自己的
secretKey: secretKey
# 桶 根据自己创的来写
bucket:
# 视频桶
videofiles: video
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;

@Value("${minio.accessKey}")
private String accessKey;

@Value("${minio.secretKey}")
private String secretKey;

//注册MinioClient Bean
@Bean
public MinioClient minioClient() {

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

这样minio的各项配置就配置完成,我们就可以用MinioClient中的方法来进行操作了

Minio分片上传

minio上传方法

1
2
3
4
5
6
7
8
9
10
11
12
MinioClient minioClient = new MinioClient();
//上传文件
minioClient.uploadObject(
//构建上传文件的对象
UploadObjectArgs.builder()
//minio的桶相当于文件夹
.bucket("桶名")
.filename("文件在本地的地址")
.object("文件上传到minio后在minio中的地址")
.contentType("文件类型")
.build()
);

首先来分析上传需要的参数:

  • bucket
    这个我们指定上传文件在哪个桶就行了
  • fileName
    由于是本地地址,所以接收了前端传过来的文件后存储在临时的文件夹中就可以了
  • object
    我们用传过来的文件的md5作目录加上/chunk加上/分片的顺序也就是1,2,3,4…这样就不会重复了
  • contentType
    由于是分片所以类型是空也就是 “”

这样我们就知道上传分片的接口要有什么参数了

正常来说切片应该是由前端来切片文件,然后调用接口来上传分片文件,等所有分片文件上传完后请求合并接口,如果上传到一半暂停检查minio中的分片,如果有从最后的分片开始接着传后面的分片,等所有分片文件上传完后请求合并接口

接口大概就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@PostMapping("/upload/uploadchunk")
public R<Void> uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") int chunk){
try {
//创建临时文件
File tempFile = File.createTempFile("minio", "temp");
//上传的文件拷贝到临时文件
file.transferTo(tempFile);
//文件路径
String absolutePath = tempFile.getAbsolutePath();
//调用上传方法
minioService.uploadVideo(fileMd5,chunk,absolutePath);
boolean delete = tempFile.delete();
if (!delete) {
return R.fail(ReturnCode.RC500.getCode(),"上传分片失败");
}
return R.ok(i);
} catch (Exception e) {
return R.fail(ReturnCode.RC500.getCode(),"上传分片失败");
}
}

但是我是后端,在测试的时候需要查看分片的效果,所以接口就变成接受完整文件,分片在后端完成,返回分片数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 @PostMapping("/upload/uploadchunk")
public R<Integer> uploadChunk(@RequestParam("file") MultipartFile file) {
try {
//创建临时文件
File tempFile = File.createTempFile("minio", "temp");
//上传的文件拷贝到临时文件
file.transferTo(tempFile);
//调用上传方法
int i = minioService.uploadVideo(tempFile);
return R.success(i);
} catch (Exception e) {
return R.fail(ReturnCode.RC500.getCode(),"上传分片失败");
}
}

实现分片的上传方法

  1. 首先创建MinioService
    定义上传方法

    1
    2
    3
    4
    5
    6
    7
    8
    public interface MinioService {
    /**
    * 上传分片文件
    * @param videoFIle 完整视频文件
    * @return 上传分片的数量
    */
    public int uploadVideo(File videoFIle) ;
    }
  2. 创建MinioServiceImpl,注入MinioClient,定义好每个分片的大小,从yaml里获取存储视频文件的桶名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    @Slf4j
    public class MinioServiceImpl implements MinioService {
    private final MinioClient minioClient;

    //存储视频文件
    @Value("${minio.bucket.videofiles}")
    private String bucket_video;

    // 设置分片大小
    long partSize = 5 * 1024 * 1024; //5MB

    public MinioServiceImpl(MinioClient minioClient) {
    this.minioClient = minioClient;
    }
    }
  3. 创建根据扩展名获取mimeType的方法,也就是我们上传需要的contentType

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //根据扩展名获取mimeType
    private String getMimeType(String extension){
    if(extension == null){
    extension = "";
    }
    //根据扩展名取出mimeType
    ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
    String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    if(extensionMatch != null){
    mimeType = extensionMatch.getMimeType();
    }
    return mimeType;
    }
  4. 创建获取文件的值的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //获取文件的md5
    private String getFileMd5(File file) {
    try (FileInputStream fileInputStream = new FileInputStream(file)) {
    String fileMd5 = DigestUtils.md5Hex(fileInputStream);
    return fileMd5;
    } catch (Exception e) {
    log.error("获取文件md5失败",e);
    return null;
    }
    }
  5. 创建根据md5值来获取分块文件在minio里的目录的方法

    1
    2
    3
    4
     //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }
  6. 实现上传视频的方法

    1
    2
    3
    @Override
    public int uploadVideo(File videoFile) {
    }
    1. 首先调用方法获取文件的md5和定义一个目录来存放分片文件
      前端切片的话并不需要md5和定义目录存放分片文件

      1
      2
      3
      4
      //获取文件的md5值	
      String fileMd5 = getFileMd5(videoFile);
      //存放分片的目录
      String chunkPath = "D:/testchunk/";
    2. 计算文件总大小来计算需要分片的数量

      1
      2
      3
      4
      5
      // 计算文件总大小
      long fileSize = videoFile.length();

      // 计算需要分片的数量
      int partCount = (int) Math.ceil((double) fileSize / partSize);
    3. 利用for循环循环上传每一个分片

      1
      2
      for(int i = 0; i < partCount; i++){
      }
    4. for循环里计算当前分片的范围

      1
      2
      3
      4
      5
      6
      // 计算分片的范围

      // 已经读取的数据
      long offset = i * partSize;
      // 看5MB和剩下的数据哪个小,哪个小就用哪个
      long size = Math.min(partSize, fileSize - offset);
    5. 读取分片的数据将分片的数据写入文件中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 读取分片的数据
      byte[] data = new byte[(int) size];
      try (RandomAccessFile raf = new RandomAccessFile(videoFile, "r")) {
      //在这个位置添加指针,从这个位置读
      raf.seek(offset);
      raf.readFully(data);
      } catch (IOException e) {
      throw new RuntimeException(e);
      }

      // 将分片的数据写入到文件中
      String chunkFileName = chunkPath + fileMd5 + (i + 1);
      try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
      fos.write(data);
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
    6. 将分片文件上传到minio

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      //将分片上传到Minio
      try {
      minioClient.uploadObject(
      //构建上传文件的对象
      UploadObjectArgs.builder()
      .bucket(bucket_video)
      .filename(chunkFileName)
      .object(getChunkFileFolderPath(fileMd5) + (i + 1))
      .contentType(getMimeType(null))
      .build()
      );
      } catch (Exception e) {
      log.error("上传分块失败", e);
      throw new RuntimeException("上传分块失败");
      }
    7. 在for循环外返回分块数量

      1
      return partCount;

    上传方法完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    @Service
    @Slf4j
    public class MinioServiceImpl implements MinioService {
    private final MinioClient minioClient;

    //存储视频文件
    @Value("${minio.bucket.videofiles}")
    private String bucket_video;

    // 设置分片大小
    long partSize = 5 * 1024 * 1024; //5MB

    public MinioServiceImpl(MinioClient minioClient) {
    this.minioClient = minioClient;
    }

    //根据扩展名获取mimeType
    private String getMimeType(String extension){
    if(extension == null){
    extension = "";
    }
    //根据扩展名取出mimeType
    ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
    String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    if(extensionMatch != null){
    mimeType = extensionMatch.getMimeType();
    }
    return mimeType;
    }

    //获取文件的md5
    private String getFileMd5(File file) {
    try (FileInputStream fileInputStream = new FileInputStream(file)) {
    String fileMd5 = DigestUtils.md5Hex(fileInputStream);
    return fileMd5;
    } catch (Exception e) {
    log.error("获取文件md5失败",e);
    return null;
    }
    }

    //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    @Override
    public int uploadVideo(File videoFile) {
    //获取文件的md5值
    String fileMd5 = getFileMd5(videoFile);
    //存放分片的目录
    String chunkPath = "D:/testchunk/";

    // 计算文件总大小
    long fileSize = videoFile.length();

    // 计算需要分片的数量
    int partCount = (int) Math.ceil((double) fileSize / partSize);

    //上传每个分片
    for(int i = 0; i < partCount; i++){
    // 计算分片的范围
    long offset = i * partSize;
    long size = Math.min(partSize, fileSize - offset);
    // 读取分片的数据
    byte[] data = new byte[(int) size];
    try (RandomAccessFile raf = new RandomAccessFile(videoFile, "r")) {
    //在这个位置添加指针,从这个位置读
    raf.seek(offset);
    raf.readFully(data);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }

    // 将分片的数据写入到文件中
    String chunkFileName = chunkPath + fileMd5 + (i + 1);
    try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
    fos.write(data);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }

    //将分片上传到Minio
    try {
    minioClient.uploadObject(
    //构建上传文件的对象
    UploadObjectArgs.builder()
    .bucket(bucket_video)
    .filename(chunkFileName)
    .object(getChunkFileFolderPath(fileMd5) + (i + 1))
    .contentType(getMimeType(null))
    .build()
    );
    } catch (Exception e) {
    log.error("上传分块失败", e);
    throw new RuntimeException("上传分块失败");
    }
    }
    return partCount;
    }
    }

    测试结果:

    可以看到分片文件已上传成功

Minio合并分片并清除分片

minio合并方法

这个方法可以组合来自不同源对象的数据来创建对象,我们用来合并分片为一个文件

1
public ObjectWriteResponse composeObject(ComposeObjectArgs args)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//构建不同文件的对象放入集合中
List<ComposeSource> sources = new ArrayList<ComposeSource>();
sources.add(
ComposeSource.builder()
.bucket("my-bucketname-one")
.object("my-objectname-one")
.build());
sources.add(
ComposeSource.builder()
.bucket("my-bucketname-two")
.object("my-objectname-two")
.build());
//合并集合中的对象
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket("my-destination-bucket")
.object("my-destination-object")
.sources(sources)
.build());

我们来思考合并接口需要的参数

  • 首先需要fileMd5获取分块文件的目录
  • 其次需要fileExt来决定合成后文件的类型
  • 最后需要分片的数量来决定迭代构建多少次对象

合并完后我们返回文件的地址

接口

1
2
3
4
5
6
7
@PostMapping("/upload/mergechunks")
public R<String> mergeChunks(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileExt") String fileExt,
@RequestParam("chunkTotal") int chunkTotal){
String path = minioService.mergeChunksAndUpload(fileMd5, fileExt, chunkTotal);
return R.success(path);
}

实现合并分片的方法

  1. 首先在MinioService创建合并分片的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface MinioService {
    /**
    * 合并视频分片
    * @param fileMd5 文件的md5来决定合并那个视频
    * @param fileExt 合并后的文件名称
    * @param chunkTotal 分片数量
    * @return 合并后的视频地址
    */
    public String mergeChunksAndUpload(String fileMd5, String fileExt, int chunkTotal);
    }
  2. MinioServiceImpl实现合并分片的方法

    1
    2
    3
    @Override
    public String mergeChunksAndUpload(String fileMd5, String fileExt, int chunkTotal) {
    }
    1. 首先我们定义objectName作为最后返回的文件地址

      1
      String objectName;
    2. 创建根据md5fileExt获取合并文件的目录的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      /**
      * 得到合并后的文件的地址
      * @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;
      }
    3. 然后获取分片文件目录使用迭代构建分块文件的对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      //分块文件所在目录
      String chunkFolderPath = getChunkFileFolderPath(fileMd5);
      //找到分块文件
      List<ComposeSource> sources = Stream.iterate(1, i -> ++i)
      //总数为5 最大返回5个流
      .limit(chunkTotal)
      //映射i 构建每个分块文件的对象
      .map(i -> ComposeSource.builder()
      .bucket(bucket_video)
      .object(chunkFolderPath + i)
      .build()
      )
      //转为List
      .toList();
    4. 获取合并后的文件的目录

      1
      2
      //合并后的文件的objectName
      objectName = getFilePathByMd5(fileMd5,fileExt);
    5. 将分片合并为一个文件

      1
      2
      3
      4
      5
      6
      7
      //将分片合并为一个文件
      minioClient.composeObject(ComposeObjectArgs
      .builder()
      .bucket(bucket_video)
      .object(objectName)
      .sources(sources)
      .build());

如此合并的方法就已经完成,但是合并后还需要一个操作,那就是将分片全部清除

想要实现清除分片的方法,我们先来看minio的批量删除方法

minio批量删除方法

1
public Iterable<Result<DeleteError>> removeObjects(RemoveObjectsArgs args)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个列表来存储要删除的对象
List<DeleteObject> objects = new LinkedList<>();
// 向列表中添加要删除的对象
objects.add(new DeleteObject("aa.tmp"));
objects.add(new DeleteObject("my-objectname"));
objects.add(new DeleteObject("nacos-server-2.0.3.tar.gz"));

// 调用 minioClient 来删除指定的对象
Iterable<Result<DeleteError>> results =
minioClient.removeObjects(
RemoveObjectsArgs.builder().bucket("my-bucketname").objects(objects).build());

// 遍历结果以处理可能的删除错误
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
// 输出删除对象时的错误信息
System.out.println(
"删除对象时发生错误:" + error.objectName() + "; " + error.message());
}

我们来思考删除分片方法需要的参数

  • 分块文件的目录

    目录加上分片的顺序1,2,3,4,5…作为object

  • 分片总数
    作为需要构建多少个对象的参数

实现清除分块的方法

  1. MinioServiceImpl中创建清除分块的方法

    1
    2
    3
    4
    5
    6
    7
    /**
    * 清除分块文件
    * @param chunkFileFolderPath 分块文件路径
    * @param chunkTotal 分块文件总数
    */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal){
    }
  2. 迭代构建删除文件对象

    1
    2
    3
    4
    5
    6
    7
    //迭代构建文件对象放到集合中
    List<DeleteObject> deleteObjects = Stream.iterate(1, i -> ++i)
    //最多多少
    .limit(chunkTotal)
    //object是分片目录加上i
    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
    .toList();
  3. 删除分片文件

    1
    2
    3
    4
    5
    6
    //删除分块文件
    Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs
    .builder()
    .bucket(bucket_video)
    .objects(deleteObjects)
    .build());
  4. 遍历结果以处理可能的删除错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //遍历结果以处理可能的删除错误
    results.forEach(r -> {
    DeleteError deleteError = null;
    try {
    deleteError = r.get();
    } catch (Exception e) {
    log.error("清除分块文件失败,objectName:{}",deleteError.objectName(),e);
    }
    });
  5. 完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * 清除分块文件
    * @param chunkFileFolderPath 分块文件路径
    * @param chunkTotal 分块文件总数
    */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal){
    try {
    //迭代构建文件对象放到集合中
    List<DeleteObject> deleteObjects = Stream.iterate(1, i -> ++i)
    .limit(chunkTotal)
    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
    .toList();
    //删除分块文件
    Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs
    .builder()
    .bucket(bucket_video)
    .objects(deleteObjects)
    .build());
    //遍历结果以处理可能的删除错误
    results.forEach(r -> {
    DeleteError deleteError = null;
    try {
    deleteError = r.get();
    } catch (Exception e) {
    log.error("清除分块文件失败,objectName:{}",deleteError.objectName(),e);
    }
    });
    } catch (Exception e) {
    log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
    }
    }

这样清除分块的方法也完成了,在合并完文件后调用清除分块的方法合并分片的接口就完成了
合并分片方法完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 合并分片
* @param fileMd5 文件的md5来决定合并那个视频
* @param fileExt 合并后的文件名称
* @param chunkTotal 分片数量
* @return 合并后的视频地址
*/
@Override
public String mergeChunksAndUpload(String fileMd5, String fileExt, int chunkTotal) {
String objectName;
try {
//分块文件所在目录
String chunkFolderPath = getChunkFileFolderPath(fileMd5);
//找到分块文件
List<ComposeSource> sources = Stream.iterate(1, i -> ++i)
//总数为5 最大返回5个流
.limit(chunkTotal)
//映射i 构建每个分块文件的对象
.map(i -> ComposeSource.builder()
.bucket(bucket_video)
.object(chunkFolderPath + i)
.build()
)
//转为List
.toList();
//合并后的文件的objectName
objectName = getFilePathByMd5(fileMd5,fileExt);
log.info("合并后的文件路径: {}",objectName);
//将分片合并为一个文件
minioClient.composeObject(ComposeObjectArgs
.builder()
.bucket(bucket_video)
.object(objectName)
.sources(sources)
.build());
log.info("文件合并并上传成功");
clearChunkFiles(chunkFolderPath,chunkTotal);
} catch (Exception e) {
log.error("文件合并并上传失败", e);
throw new RuntimeException("文件合并并上传失败");
}
return objectName;
}

测试:

可以看到分块已经被合并成一个视频文件,分块也已经被删除。请求后也返回了文件的目录

接下来我们来实现根据文件的目录来下载文件的接口

Minio下载文件

minio下载方法

以流的方式下载一个对象

1
public InputStream getObject(GetObjectArgs args)

示例:

1
2
3
4
5
6
7
8
// 获取对象的InputStream,并保存为文件
InputStream stream =
minioClient.getObject(
GetObjectArgs.builder().bucket("my-bucketname").object("my-objectname").build());
// 读流
File targetFile = new File("D:\\deploy\\targetFile.tmp");
FileUtils.copyInputStreamToFile(stream, targetFile);
stream.close();

分析下载接口需要的参数

首先桶名我们指定了对应的桶名所以不需要

文件目录,这个就是合并后返回的,我们需要这个

接口

1
2
3
4
5
6
7
8
9
/**
* 下载文件
* @param objectName 文件目录
* @return 文件
*/
@GetMapping("/download/downloadvideo")
public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName){
return minioService.downloadFileFromMinIO(objectName);
}

实现下载文件的方法

  1. MinioService创建下载的方法

    1
    2
    3
    4
    5
    6
    /**
    * 下载指定文件
    * @param objectName 文件目录
    * @return 文件
    */
    public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName);
  2. MioioServiceImpl实现下载的方法

    1
    2
    3
    @Override
    public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName) {
    }
    1. 首先获取指定对象的数据

      1
      2
      3
      4
      5
      6
      //获取指定对象的数据
      InputStream inputStream = minioClient.getObject(GetObjectArgs
      .builder()
      .bucket(bucket_video)
      .object(objectName)
      .build());
    2. 将文件转为byte字节数组

      1
      2
      //将文件转为byte字节数组
      byte[] byteArray = IOUtils.toByteArray(inputStream);
    3. 设置响应头信息

      1
      2
      3
      4
      5
      6
      //设置响应头信息
      HttpHeaders headers = new HttpHeaders();
      //设置二进制流数据格式
      headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      //通知浏览器以attachment(下载的方式)打开图片
      headers.setContentDispositionFormData("attachment",objectName);
    4. 返回文件

      1
      2
       //返回文件
      return new ResponseEntity<>(byteArray,headers, HttpStatus.OK);

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 下载指定文件
* @param objectName 文件目录
* @return 文件
*/
@Override
public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName) {
try {
//获取指定对象的数据
InputStream inputStream = minioClient.getObject(GetObjectArgs
.builder()
.bucket(bucket_video)
.object(objectName)
.build());
//将文件转为byte字节数组
byte[] byteArray = IOUtils.toByteArray(inputStream);
//设置响应头信息
HttpHeaders headers = new HttpHeaders();
//设置二进制流数据格式
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//通知浏览器以attachment(下载的方式)打开图片
headers.setContentDispositionFormData("attachment",objectName);
//返回文件
return new ResponseEntity<>(byteArray,headers, HttpStatus.OK);
} catch (Exception e) {
log.error("下载文件失败",e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

测试:

可以看到下载了指定的文件,文件也并没有损坏可以打开播放