MinIO笔记2

3/24/2024 MinIO对象存储

[toc]

# MinIO 笔记 2

# SpringBoot 与 minio 搭配使用

  1. 先创建 springboot 工程。
  2. 导入 minio 客户端依赖
<!-- MinIO 客户端 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.2.2</version>
</dependency>
1
2
3
4
5
6
  1. minio 配置
minio:
  url: http://127.0.0.1:19000 # minio服务端地址
  accessKey: minioadmin # 用户名
  secretKey: minioadmin # 密码
1
2
3
4
  1. 自定义 minio 客户端类

如果想要实现分片文件上传。我们需要用到下面的方法。但是这几个方法,MinioClient类无法调用。我们必须自定义MinioClient类,让自定义MinioClient类去继承官方的MinioClient类之后,自定义MinioClient类才能主动调用这几个方法。

注意此处用到的minio依赖包是8.2.2版本的。

createMultipartUpload    //创建文件上传id
listParts                //获取分片上传列表
completeMultipartUpload  // 合并分片文件
1
2
3
@Component
public class MyMinioClient extends MinioClient {

    /**
     * 调用父类MinioClient的构造方法
     * @param client
     */
    public MyMinioClient(MinioClient client) {
        super(client);
    }

    /**
     * 创建分片上传请求
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param headers          消息头
     * @param extraQueryParams 额外查询参数
     */
    @Override
    public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
        return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
    }

    /**
     * 完成分片上传,执行合并文件
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param parts            分片
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     */
    @Override
    public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams)  {
        return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    /**
     * 查询分片文件的数据
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     */
    public ListPartsResponse listParts(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }

}

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
  1. minio 配置类

minio 配置类中注入我们自定义 MinioClient 类。

@Configuration
public class MinioConfig {

    /**
     * 访问地址
     */
    @Value("${minio.url}")
    private String endpoint;

    /**
     * accessKey类似于用户ID,用于唯一标识你的账户
     */
    @Value("${minio.accessKey}")
    private String accessKey;

    /**
     * secretKey是你账户的密码
     */
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MyMinioClient minioClient() {
        MinioClient minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
        return new MyMinioClient(minioClient);
    }

}
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
  1. minio 工具类

minio 工具类中使用我们自定义的 MinioClient 类。

/**
 * MinIO工具类
 */
@Slf4j
@Component
public class MinioUtils {
    @Autowired
    private MyMinioClient myMinioClient;

    /****************************** 操作 Bucket ******************************/

    /**
     * 创建存储bucket
     * @return Boolean
     */
    public void makeBucket(String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        myMinioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 判断Bucket是否存在,true:存在,false:不存在
     * @param bucketName
     * @return
     */
    public boolean bucketExists(String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 查询所有存储桶Bucket
     * @return Bucket列表集合
     */
    public List<Bucket> getBucketslist() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.listBuckets();
    }

    /**
     * 根据bucketName删除Bucket,true:删除成功; false:删除失败或桶中存在文件
     * @param bucketName
     * @throws Exception
     */
    public void removeBucket(String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        myMinioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /******************************  查询文件 ******************************/

    /**
     * 判断文件是否存在
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            myMinioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * 获取文件信息, 如果抛出异常则说明文件不存在
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @return
     */
    public String getFileInfo(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()).toString();
    }

    /**
     * 根据文件前缀来获取文件列表信息
     * @param bucketName 存储桶
     * @param prefix     文件名称前缀
     * @param recursive  是否递归查找,false:模拟文件夹结构查找
     * @return 二进制流
     */
    public Iterable<Result<Item>> getListObjects(String bucketName, String prefix, boolean recursive) {
        return myMinioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(recursive)
                        .build());
    }

    /**
     * 根据文件前缀,查询文件
     * @param bucketName 存储桶
     * @param prefix     前缀
     * @param recursive  是否使用递归查询
     * @return MinioItem 列表
     */
    public List<Item> getObjectsByPrefix(String bucketName,
                                            String prefix,
                                            boolean recursive) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = myMinioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
        if (objectsIterator != null) {
            for (Result<Item> o : objectsIterator) {
                Item item = o.get();
                list.add(item);
            }
        }
        return list;
    }

    /**
     * 获取文件流
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @return 二进制流
     */
    public InputStream getObject(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }


    /**
     * 获取文件外链,有时间限制
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @param expires    外链有效时间(单位:秒)
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
        return myMinioClient.getPresignedObjectUrl(args);
    }

    /**
     * 获得文件外链,长期有效
     * @param bucketName
     * @param objectName
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .method(Method.GET).build();
        return myMinioClient.getPresignedObjectUrl(args);
    }

    /**
     * 获取预签名链接、过期时间一天
     * @param bucketName  桶名
     * @param objectName  文件名称
     * @return
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String objectName,Map<String, String> queryParams) {
        return myMinioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(60 * 60 * 24)
                        .extraQueryParams(queryParams)
                        .build());
    }

    /******************************  操作文件 ******************************/

    /**
     * 拷贝文件
     * @param bucketName    存储桶
     * @param objectName    文件名
     * @param srcBucketName 目标存储桶
     * @param srcObjectName 目标文件名
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
        return myMinioClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                        .bucket(srcBucketName)
                        .object(srcObjectName)
                        .build());
    }

    /**
     * 删除文件
     * @param bucketName 存储桶
     * @param objectName 文件名称
     */
    @SneakyThrows(Exception.class)
    public void removeFile(String bucketName, String objectName) {
        myMinioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }

    /**
     * 批量删除文件
     * @param bucketName 存储桶
     * @param keys       需要删除的文件列表
     * @return
     */
    public void removeFiles(String bucketName, List<String> keys) {
        List<DeleteObject> objects = new LinkedList<>();
        keys.forEach(s -> {
            objects.add(new DeleteObject(s));
            try {
                removeFile(bucketName, s);
            } catch (Exception e) {
                log.error("[Minio工具类]>>>> 批量删除文件,异常:", e);
            }
        });
    }



    /******************************  下载文件  ******************************/

    /**
     * 断点下载
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @param offset     起始字节的位置
     * @param length     要读取的长度
     * @return 二进制流
     */
    public InputStream getObject(String bucketName, String objectName, long offset, long length) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build());
    }

    /******************************  上传文件  ******************************/

    /**
     * 通过 MultipartFile 进行文件上传
     * @param bucketName  存储桶
     * @param file        文件名
     * @param objectName  对象名
     * @param contentType 文件类型
     * @return
     */
    public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        //先判断对象桶是否存在
        boolean b = bucketExists(bucketName);
        if(!b){
            //若对象桶不存在,则创建
            makeBucket(bucketName);
        }
        //把文件转化为输入流
        InputStream inputStream = file.getInputStream();
        //上传文件
        ObjectWriteResponse objectWriteResponse = myMinioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(contentType)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
        //上传文件后,关闭流
        if(inputStream!=null){
            inputStream.close();
        }
        return objectWriteResponse;
    }


    /**
     * 通过文件完整路径 上传文件
     * @param bucketName 存储桶
     * @param objectName 对象名称
     * @param filePath   本地文件路径
     * @return
     */
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, String filePath) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(filePath)
                        .build());
    }

    /**
     * 通过输入流 上传文件
     * @param bucketName  存储桶
     * @param objectName  文件对象
     * @param inputStream 文件流
     * @return
     */
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        return myMinioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }

    /**
     *  创建分片上传
     */
    public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return myMinioClient.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
    }

    /**
     * 完成分片上传(分片文件合并)
     */
    public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return myMinioClient.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    /**
     * 查询分片文件
     */
    public ListPartsResponse listParts(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return myMinioClient.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }
}

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339

# 文件上传

前端代码

<!--element-plus的文件上传组件-->
<el-upload
  ref="uploadRef"
  action="#"
  :limit="1"
  :file-list="fileList"
  :on-change="onChange"
  :auto-upload="false"
>
  <template #trigger>
    <el-button type="primary">选取文件</el-button>
  </template>
  <el-button style="margin-left: 10px" type="success" @click="uploadlFile">
    上传到服务器
  </el-button>
</el-upload>
<el-progress
  v-if="isShowProgress"
  :text-inside="true"
  :stroke-width="20"
  :percentage="progressPercent"
/>

<script setup>
  import axios from "axios"; //引入axios
  import { ElMessage } from "element-plus";

  let uploadRef = ref(undefined);
  let isShowProgress = ref(false); //是否显示进度条
  let progressPercent = ref(0); //进度条进度
  let fileList = ref([]); //文件列表

  //文件改变时,将新添加的文件加入到文件列表中
  function onChange(file) {
    fileList.value.push(file);
  }

  //自定义文件上传请求
  function uploadlFile() {
    //判断文件列表中是否有文件再上传
    if (fileList.value.length === 0) {
      return ElMessage.error("请选择要上传的文件");
    }
    //遍历文件列表,把列表中的每个文件都上传
    fileList.value.forEach((file) => {
      let param = new FormData();
      param.append("file", file.raw);
      param.append("bucketName", "media-episodes-bucket");
      isShowProgress.value = true;
      //调接口
      axios
        .request({
          url: "/shuyx-minio/oss/upload",
          method: "POST",
          data: param,
          headers: { "Content-Type": "multipart/form-data" },
          //获取进度方法
          onUploadProgress: (e) => {
            progressPercent.value = Number(
              ((e.loaded / e.total) * 100).toFixed(0)
            );
          },
        })
        .then((res) => {
          //设置文件列表中的文件的状态,变为成功
          file.url = res.data.fileName;
          file.status = "success";
          //隐藏进度条,并把进度清空
          isShowProgress.value = false;
          progressPercent.value = 0;
        });
    });
  }
</script>
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

后端代码

@RestController
@RequestMapping("/shuyx-minio/oss")
public class OSSController {
    @Autowired
    private MinioUtils minioUtils;

    //用户头像文件对象桶
    private String USER_AVATAR_BUCKET = "user-avatar-bucket";
    //媒体剧集文件对象桶
    private String MEDIA_EPISODES_BUCKET = "media-episodes-bucket";

    /**
     * 上传文件接口
     * @param file
     */
    @PostMapping("/upload")
    public Object upload(@RequestParam("file") MultipartFile file,String bucketName) {
        log.info("/shuyx-minio/oss/upload, file,{}",file);
        double size = (double) (file.getSize() / 1024 / 1024) + 1;
        log.info(file.getOriginalFilename() +" 文件大小为 "+size+" MB之内。");

        try {
            //新文件名
            String newFileName = System.currentTimeMillis() + "." +  file.getOriginalFilename();
            //文件类型
            String contentType = file.getContentType();
            //上传文件
            minioUtils.uploadFile(bucketName, file, newFileName, contentType);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("fileName",newFileName);
            return ReturnUtil.success(jsonObject);
        } catch (Exception e) {
            log.error("上传文件失败。请查询日志。");
            e.printStackTrace();
            return ReturnUtil.fail(ResultCodeEnum.HTTP_REQUEST_ERROR);
        }
    }
}

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

# 分片文件上传

当要上传的文件太大的时候。我们可以在前端,将大文件进行分片。然后把多个分片文件都上传到 minio 中。然后再 minio 中将分片文件拼接成大文件。

假设一个大文件,大小为 444M。并且设置每个分片文件的大小为 100M。那么这个大文件会被分成 5 个分片文件。

分片文件上传逻辑:

  1. 前端通过后端向 minio 发送分片上传请求。参数是文件名称,文件分片数量,对象桶名称
    1. minio 会根据这些参数,创建一个 uploadId。
    2. 我们拿着 uploadId,再向 minio 获取预签名链接。链接个数与分片个数相同。
    3. 最后把 uploadId 和多个预签名链接。作为响应结果返回。
  2. 前端将大文件进行分片。每个分片对应一个预签名链接。
  3. 然后前端直接向 minio 发送 put 请求。参数就是分片文件和对应的预签名链接。
  4. 当所有分片文件都上传到 minio 的时候。前端通过后端向 minio 发送分片文件合并请求。把 minio 中的分片文件合并为一个大文件。

注意:当我们拿到预签名链接后,前端是直接向minio服务端发送put请求,来上传分片文件。此时的put请求是没有经过后端的。

前端代码:

<!--element-plus的文件上传组件-->
<el-upload
  ref="uploadRef"
  action="#"
  :limit="1"
  :file-list="fileList"
  :on-change="onChange"
  :auto-upload="false"
>
  <template #trigger>
    <el-button type="primary">选取文件</el-button>
  </template>
  <el-button style="margin-left: 10px" type="success" @click="uploadlPartFile">
    上传到服务器
  </el-button>
</el-upload>
<el-progress
  v-if="isShowProgress"
  :text-inside="true"
  :stroke-width="20"
  :percentage="progressPercent"
/>

<script setup>
import axios from "axios"; //引入axios
  import { ElMessage } from "element-plus";

  //自定义分片文件上传请求
  function uploadlPartFile() {
    //判断文件列表中是否有文件再上传
    if (fileList.value.length === 0) {
      return ElMessage.error("请选择要上传的文件");
    }
    //获取文件列表中的第一个文件
    let currentFile = fileList.value[0].raw;
    console.log("11", currentFile);

    //将文件分片,得到分片数组
    let fileChunkList = createFileChunk(currentFile);

    //请求参数
    let param = new FormData();
    param.append("fileName", currentFile.name);
    param.append("bucketName", "media-episodes-bucket");
    param.append("chunkNum", fileChunkList.length);

    //创建分片上传请求
    axios.request({
        url: "/shuyx-minio/oss/createMultipartUpload",
        method: "POST",
        data: param,
        headers: { "Content-Type": "multipart/form-data" },
      })
      .then(async (res) => {
        //uploadId
        let uploadId = res.data.uploadId;
        //预签名链接数组
        let chunkList = res.data.uploadUrlList;
        //依次上传分片文件
        for (let i = 0; i < fileChunkList.length; i++) {
          console.log("------------------------------------");
          console.log("开始上传第" + (i + 1) + "个分片");
          //此处用axios发送put请求,该请求用于上传分片文件
          //请求参数就是分片文件和对应的预签名链接
          //注意该请求是同步的。即一个分片文件上传完毕之后,再上传下一个分片文件
          await axios({
            method: "put",
            url: chunkList[i],
            data: fileChunkList[i].file,
          }).then(async function (res) {
            if (res.status == 200) {
              console.log("第" + (i + 1) + "个分片,上传成功");
            } else {
              console.log("第" + (i + 1) + "个分片,上传失败");
            }
          });
        }

        //如果最后一个分片文件上传完毕,那么就调用分片文件合并请求
        let obj = {
          bucketName: "media-episodes-bucket",
          fileName: currentFile.name,
          uploadId: uploadId,
        };
        console.log("开始合并分片");

        axios.request({
            url: "/shuyx-minio/oss/mergePartFile",
            method: "POST",
            data: obj,
            headers: { "Content-Type": "multipart/form-data" },
          })
          .then((r) => {
            console.log("mergePartFile", r);
            console.log("分片合并完成");
          });
      });
  }

  /**
   * 把文件分片
   */
  function createFileChunk(file) {
    let fileChunkList = [];
    let chunkSize = 100 * 1024 * 1024; //每个分片大小为100mb
    let count = 0;
    while (count < file.size) {
      fileChunkList.push({
        file: file.slice(count, count + chunkSize),
      });
      count += chunkSize;
    }
    return fileChunkList;
  }
</script>
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115

后端代码

@RestController
@RequestMapping("/shuyx-minio/oss")
public class OSSController {
    @Autowired
    private MinioUtils minioUtils;

    //用户头像文件对象桶
    private String USER_AVATAR_BUCKET = "user-avatar-bucket";
    //媒体剧集文件对象桶
    private String MEDIA_EPISODES_BUCKET = "media-episodes-bucket";
  
    /**
     * 创建分片文件请求等信息
     * 逻辑:
     * 1. minio接收到前端发来的创建分片文件请求。会返回对应的uploadId
     * 2. 然后通过uploadId以及分片数量,创建同等数量的预签名链接。并返回
     * @return
     */
    @PostMapping("/createMultipartUpload")
    public Object createMultipartUpload(String fileName,String bucketName,Integer chunkNum)  {
        log.info("/shuyx-minio/oss/createMultipartUpload, bucketName {} , fileName {},chunkNum {}",bucketName,fileName,chunkNum);
        log.info("文件名称为"+fileName);
        log.info("对象桶名称为"+bucketName);
        log.info("分片数量为"+chunkNum);

        // 1. 向minio创建分片上传请求,会返回一个uploadId
        CreateMultipartUploadResponse response = minioUtils.createMultipartUpload(bucketName, null, fileName, null, null);
        // 2. 获取uploadId
        String uploadId = response.result().uploadId();

        // 3. 返回结果信息
        Map<String, Object> result = new HashMap<>();
        result.put("uploadId",uploadId);

        ArrayList<String> uploadUrlList = new ArrayList<>();

        // 4. 请求Minio,获取每个分块,对应的签名上传URL
        Map<String, String> partMap = new HashMap<>();
        partMap.put("uploadId", uploadId);
        // 5. 根据分片数量,循环获取各个分片的预签名链接
        for (int i = 1; i <= chunkNum; i++) {
            partMap.put("partNumber", String.valueOf(i));
            String uploadUrl = minioUtils.getPresignedObjectUrl(bucketName, fileName, partMap);//获取每个分片文件的预签名链接
            uploadUrlList.add(uploadUrl);   //添加预签名链接
        }
        result.put("uploadUrlList",uploadUrlList);
        //返回
        return ReturnUtil.success(result);
    }

    /**
     * 合并分片文件
     */
    @PostMapping("/mergePartFile")
    public Object mergePartFile(String bucketName,String fileName,String uploadId) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
         //先查询minio中具有相同uploadId的分片文件
        ListPartsResponse listPartsResponse = minioUtils.listParts(bucketName,null,
                fileName,null,null, uploadId,null,null);
        List<Part> parts = listPartsResponse.result().partList();
        //找到这些分片文件之后,开始合并分片文件
        minioUtils.completeMultipartUpload(bucketName, null,
                fileName, uploadId, parts.toArray(new Part[]{}), null, null);
        return ReturnUtil.success();
    }

}
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
Last Updated: 3/28/2024, 8:52:23 AM