├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── tuine │ │ └── minio │ │ ├── MinioApplication.java │ │ ├── configurer │ │ ├── MinIoUtils.java │ │ └── MinioProperties.java │ │ ├── controller │ │ └── IndexController.java │ │ ├── service │ │ ├── UploadService.java │ │ └── impl │ │ │ └── UploadServiceImpl.java │ │ └── util │ │ └── CustomMinioClient.java └── resources │ └── application.yml └── test └── java └── me └── tuine └── minio └── MinioApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 tuine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickstart Guide 2 | ## Introduction 3 | Multipart objects using presigned URLs, upload directly to the server without transiting through the business server. 4 | 5 | This is just a simple demo, please improve the code according to actual business. 6 | 7 | ## Integration example 8 | curl example: 9 | 1. Init multipart upload 10 | ```shell script 11 | $ curl --location --request POST '127.0.0.1:8006/multipart/init' \ 12 | --header 'Content-Type: application/json' \ 13 | --data-raw '{ 14 | "filename": "b.jpg", 15 | "partCount": 2, 16 | "contentType": "image/jpeg" 17 | }' 18 | ``` 19 | Response example: 20 | ```js 21 | { 22 | "uploadId": "b7dd9a60-7c11-43f1-acee-bffd4ef2fccb", 23 | "uploadUrls": [ 24 | "https://play.minio.io:9000/tuinetest/test/b.jpg?uploadId=b7dd9a60-7c11-43f1-acee-bffd4ef2fccb&partNumber=1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20210324%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210324T032112Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=e39c8e8c165add0daa50d2da44e51ca752b9213e497633bcfb3431b60383b5be", 25 | "https://play.minio.io:9000/tuinetest/test/b.jpg?uploadId=b7dd9a60-7c11-43f1-acee-bffd4ef2fccb&partNumber=2&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20210324%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210324T032112Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=99611a212d6b791a24df295cb3475a79780ed4c6314ee9ddb8df4179326b7723" 26 | ] 27 | } 28 | ``` 29 | 30 | 2. Uploading objects using presigned URLs 31 | 32 | Cut the picture into two parts 33 | ```shell script 34 | curl --location --request PUT 'https://play.minio.io:9000/tuinetest/test/b.jpg?uploadId=b7dd9a60-7c11-43f1-acee-bffd4ef2fccb&partNumber=1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20210324%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210324T032112Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=e39c8e8c165add0daa50d2da44e51ca752b9213e497633bcfb3431b60383b5be' \ 35 | --header 'Content-Type: application/octet-stream' \ 36 | --data-binary '@/D:/jpgone' 37 | curl --location --request PUT 'https://play.minio.io:9000/tuinetest/test/b.jpg?uploadId=b7dd9a60-7c11-43f1-acee-bffd4ef2fccb&partNumber=2&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20210324%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210324T032112Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=99611a212d6b791a24df295cb3475a79780ed4c6314ee9ddb8df4179326b7723' \ 38 | --header 'Content-Type: application/octet-stream' \ 39 | --data-binary '@/D:/jpgtwo' 40 | ``` 41 | 42 | 3. Complete 43 | ```shell script 44 | curl --location --request PUT '127.0.0.1:8006/multipart/complete' \ 45 | --header 'Content-Type: application/json' \ 46 | --data-raw '{ 47 | "objectName":"test/b.jpg", 48 | "uploadId":"b7dd9a60-7c11-43f1-acee-bffd4ef2fccb" 49 | }' 50 | ``` 51 | 52 | ## Verify upload 53 | 54 | Login Minio: [play MinIo](https://play.minio.io:9000/minio/tuinetest/) 55 | Username: Q3AM3UQ867SPQQA43P2F 56 | Password: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.6.RELEASE 9 | 10 | 11 | me.tuine 12 | miniodemo 13 | 0.0.1-SNAPSHOT 14 | 分片上传示例 15 | minio multipart upload demo 16 | 17 | 11 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-test 28 | test 29 | 30 | 31 | cn.hutool 32 | hutool-all 33 | 5.4.1 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | 1.18.4 39 | true 40 | 41 | 42 | io.minio 43 | minio 44 | 8.0.3 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/MinioApplication.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MinioApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MinioApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/configurer/MinIoUtils.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.configurer; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.google.common.collect.HashMultimap; 5 | import io.minio.http.Method; 6 | import io.minio.messages.Part; 7 | import me.tuine.minio.util.CustomMinioClient; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.stereotype.Component; 11 | import io.minio.*; 12 | 13 | import javax.annotation.PostConstruct; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | /** 21 | * @author tuine 22 | * @date 2021/3/23 23 | */ 24 | @Component 25 | @Configuration 26 | @EnableConfigurationProperties({MinioProperties.class}) 27 | public class MinIoUtils { 28 | private final MinioProperties minioProperties; 29 | private CustomMinioClient customMinioClient; 30 | 31 | public MinIoUtils(MinioProperties minioProperties) { 32 | this.minioProperties = minioProperties; 33 | } 34 | 35 | @PostConstruct 36 | public void init() { 37 | MinioClient minioClient = MinioClient.builder() 38 | .endpoint(minioProperties.getEndpoint()) 39 | .credentials(minioProperties.getAccesskey(), minioProperties.getSecretkey()) 40 | .build(); 41 | customMinioClient = new CustomMinioClient(minioClient); 42 | } 43 | 44 | /** 45 | * 单文件签名上传 46 | * 47 | * @param objectName 文件全路径名称 48 | * @return / 49 | */ 50 | public String getUploadObjectUrl(String objectName) { 51 | // 上传文件时携带content-type头即可 52 | /*if (StrUtil.isBlank(contentType)) { 53 | contentType = "application/octet-stream"; 54 | } 55 | HashMultimap headers = HashMultimap.create(); 56 | headers.put("Content-Type", contentType);*/ 57 | try { 58 | return customMinioClient.getPresignedObjectUrl( 59 | GetPresignedObjectUrlArgs.builder() 60 | .method(Method.PUT) 61 | .bucket(minioProperties.getBucket()) 62 | .object(objectName) 63 | .expiry(1, TimeUnit.DAYS) 64 | //.extraHeaders(headers) 65 | .build() 66 | ); 67 | } catch (Exception e) { 68 | e.printStackTrace(); 69 | return null; 70 | } 71 | } 72 | 73 | /** 74 | * 初始化分片上传 75 | * 76 | * @param objectName 文件全路径名称 77 | * @param partCount 分片数量 78 | * @param contentType 类型,如果类型使用默认流会导致无法预览 79 | * @return / 80 | */ 81 | public Map initMultiPartUpload(String objectName, int partCount, String contentType) { 82 | Map result = new HashMap<>(); 83 | try { 84 | if (StrUtil.isBlank(contentType)) { 85 | contentType = "application/octet-stream"; 86 | } 87 | HashMultimap headers = HashMultimap.create(); 88 | headers.put("Content-Type", contentType); 89 | String uploadId = customMinioClient.initMultiPartUpload(minioProperties.getBucket(), null, objectName, headers, null); 90 | 91 | result.put("uploadId", uploadId); 92 | List partList = new ArrayList<>(); 93 | 94 | Map reqParams = new HashMap<>(); 95 | //reqParams.put("response-content-type", "application/json"); 96 | reqParams.put("uploadId", uploadId); 97 | for (int i = 1; i <= partCount; i++) { 98 | reqParams.put("partNumber", String.valueOf(i)); 99 | String uploadUrl = customMinioClient.getPresignedObjectUrl( 100 | GetPresignedObjectUrlArgs.builder() 101 | .method(Method.PUT) 102 | .bucket(minioProperties.getBucket()) 103 | .object(objectName) 104 | .expiry(1, TimeUnit.DAYS) 105 | .extraQueryParams(reqParams) 106 | .build()); 107 | partList.add(uploadUrl); 108 | } 109 | result.put("uploadUrls", partList); 110 | } catch (Exception e) { 111 | e.printStackTrace(); 112 | return null; 113 | } 114 | 115 | return result; 116 | } 117 | 118 | /** 119 | * 分片上传完后合并 120 | * 121 | * @param objectName 文件全路径名称 122 | * @param uploadId 返回的uploadId 123 | * @return / 124 | */ 125 | public boolean mergeMultipartUpload(String objectName, String uploadId) { 126 | try { 127 | //TODO::目前仅做了最大1000分片 128 | Part[] parts = new Part[1000]; 129 | ListPartsResponse partResult = customMinioClient.listMultipart(minioProperties.getBucket(), null, objectName, 1000, 0, uploadId, null, null); 130 | int partNumber = 1; 131 | for (Part part : partResult.result().partList()) { 132 | parts[partNumber - 1] = new Part(partNumber, part.etag()); 133 | partNumber++; 134 | } 135 | customMinioClient.mergeMultipartUpload(minioProperties.getBucket(), null, objectName, uploadId, parts, null, null); 136 | } catch (Exception e) { 137 | e.printStackTrace(); 138 | return false; 139 | } 140 | 141 | return true; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/configurer/MinioProperties.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.configurer; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | /** 8 | * @author tuine 9 | * @date 2021/3/23 10 | */ 11 | @ConfigurationProperties(prefix = "minio") 12 | @Getter 13 | @Setter 14 | public class MinioProperties { 15 | 16 | private String endpoint; 17 | 18 | private String accesskey; 19 | 20 | private String secretkey; 21 | 22 | private String bucket; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.controller; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import cn.hutool.json.JSONObject; 5 | import com.google.common.collect.ImmutableMap; 6 | import lombok.RequiredArgsConstructor; 7 | import me.tuine.minio.service.UploadService; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.Map; 13 | 14 | /** 15 | * @author tuine 16 | * @date 2021/3/23 17 | */ 18 | @RestController 19 | @RequiredArgsConstructor 20 | public class IndexController { 21 | 22 | private final UploadService uploadService; 23 | 24 | /** 25 | * 分片初始化 26 | * 27 | * @param requestParam 请求参数-此处简单处理 28 | * @return / 29 | */ 30 | @PostMapping("/multipart/init") 31 | public ResponseEntity initMultiPartUpload(@RequestBody JSONObject requestParam) { 32 | // 路径 33 | String path = requestParam.getStr("path", "test"); 34 | // 文件名 35 | String filename = requestParam.getStr("filename", "test.obj"); 36 | // content-type 37 | String contentType = requestParam.getStr("contentType", "application/octet-stream"); 38 | // md5-可进行秒传判断 39 | String md5 = requestParam.getStr("md5", ""); 40 | // 分片数量 41 | Integer partCount = requestParam.getInt("partCount", 1); 42 | 43 | //TODO::业务判断+秒传判断 44 | 45 | Map result = uploadService.initMultiPartUpload(path, filename, partCount, contentType); 46 | 47 | return new ResponseEntity<>(result, HttpStatus.OK); 48 | } 49 | 50 | /** 51 | * 完成上传 52 | * 53 | * @param requestParam 用户参数 54 | * @return / 55 | */ 56 | @PutMapping("/multipart/complete") 57 | public ResponseEntity completeMultiPartUpload( 58 | @RequestBody JSONObject requestParam 59 | ) { 60 | // 文件名完整路径 61 | String objectName = requestParam.getStr("objectName"); 62 | String uploadId = requestParam.getStr("uploadId"); 63 | Assert.notNull(objectName, "objectName must not be null"); 64 | Assert.notNull(uploadId, "uploadId must not be null"); 65 | boolean result = uploadService.mergeMultipartUpload(objectName, uploadId); 66 | 67 | return new ResponseEntity<>(ImmutableMap.of("success", result), HttpStatus.OK); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/service/UploadService.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.service; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author tuine 7 | * @date 2021/3/23 8 | */ 9 | public interface UploadService { 10 | 11 | /** 12 | * 分片上传初始化 13 | * 14 | * @param path 路径 15 | * @param filename 文件名 16 | * @param partCount 分片数量 17 | * @param contentType / 18 | * @return / 19 | */ 20 | Map initMultiPartUpload(String path, String filename, Integer partCount, String contentType); 21 | 22 | /** 23 | * 完成分片上传 24 | * 25 | * @param objectName 文件名 26 | * @param uploadId 标识 27 | * @return / 28 | */ 29 | boolean mergeMultipartUpload(String objectName, String uploadId); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/service/impl/UploadServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.service.impl; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.ImmutableMap; 5 | import lombok.RequiredArgsConstructor; 6 | import me.tuine.minio.configurer.MinIoUtils; 7 | import me.tuine.minio.service.UploadService; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * @author tuine 15 | * @date 2021/3/23 16 | */ 17 | @Service 18 | @RequiredArgsConstructor 19 | public class UploadServiceImpl implements UploadService { 20 | 21 | private final MinIoUtils minIoUtils; 22 | 23 | @Override 24 | public Map initMultiPartUpload(String path, String filename, Integer partCount, String contentType) { 25 | path = path.replaceAll("/+", "/"); 26 | if (path.indexOf("/") == 0) { 27 | path = path.substring(1); 28 | } 29 | String filePath = path + "/" + filename; 30 | 31 | Map result; 32 | // TODO::单文件上传可拆分,这里只做演示,可直接上传完成 33 | if (partCount == 1) { 34 | String uploadObjectUrl = minIoUtils.getUploadObjectUrl(filePath); 35 | result = ImmutableMap.of("uploadUrls", ImmutableList.of(uploadObjectUrl)); 36 | } else { 37 | result = minIoUtils.initMultiPartUpload(filePath, partCount, contentType); 38 | } 39 | 40 | return result; 41 | } 42 | 43 | @Override 44 | public boolean mergeMultipartUpload(String objectName, String uploadId) { 45 | return minIoUtils.mergeMultipartUpload(objectName, uploadId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/tuine/minio/util/CustomMinioClient.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio.util; 2 | 3 | import com.google.common.collect.Multimap; 4 | import io.minio.CreateMultipartUploadResponse; 5 | import io.minio.ListPartsResponse; 6 | import io.minio.MinioClient; 7 | import io.minio.ObjectWriteResponse; 8 | import io.minio.errors.*; 9 | import io.minio.messages.Part; 10 | 11 | import java.io.IOException; 12 | import java.security.InvalidKeyException; 13 | import java.security.NoSuchAlgorithmException; 14 | 15 | /** 16 | * 自定义minio 17 | * @author tuine 18 | * @date 2021-03-23 19 | */ 20 | public class CustomMinioClient extends MinioClient { 21 | 22 | public CustomMinioClient(MinioClient client) { 23 | super(client); 24 | } 25 | 26 | public String initMultiPartUpload(String bucket, String region, String object, Multimap headers, Multimap extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException { 27 | CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams); 28 | 29 | return response.result().uploadId(); 30 | } 31 | 32 | public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap extraHeaders, Multimap extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException { 33 | 34 | return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams); 35 | } 36 | 37 | public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap extraHeaders, Multimap extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { 38 | return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: ${SERVER_PORT:8006} 3 | 4 | minio: 5 | endpoint: ${MINIO_ENDPOINT:https://play.minio.io:9000} 6 | accesskey: ${MINIO_ASSESSKEY:Q3AM3UQ867SPQQA43P2F} 7 | secretkey: ${MINIO_SECRETKEY:zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG} 8 | bucket: ${MINIO_BUCKET:tuinetest} -------------------------------------------------------------------------------- /src/test/java/me/tuine/minio/MinioApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.tuine.minio; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class MinioApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------