目录
- 一、安装
- 二、代码
- 1、pom.xml(说明:minio所用依赖)
- 2、application.yml(说明:放置minio连接信息、minio文件上传限制)
- 3、MinioProperties.java(说明:读取minio连接信息)
- 4、MinioConfig.java(说明:配置minio连接客户端类)
- 5、AmazonS3Config.java(说明:配置aws连接minio客户端类)
- 6、ObjectItem.java(说明:接收Minio返回内容)
- 7、PartInfo.java(说明:转换分片信息,返回给前端)
- 8、Task.java(说明:分片上传任务)
- 9、AjaxResult.java(说明:控制层统一返回值)
- 10、Constants.java(说明:常量类)
- 11、ReflectUtils.java(说明:反射工具类)
- 12、ParallelMinioClient.java(说明:将分片方法暴露出来)
- 13、MinioUtil.java(说明:Minio连接客户端)
- 14、SimpleController.java(说明:测试Minio的普通操作)
- 15、MinioShardingController.java(说明:测试原生Minio的分片上传操作)
- 16、AwsShardingController.java(说明:测试AWS分片上传操作)
- 三、测试
- 四、资料
- 五、拓展—分片上传
一、安装
1、在k8s中安装minio单机版
(1)创建minio名称空间
kubectl create ns minio
我们下面的yaml文件用到了minio
名称空间,所以需要先把该名称空间创建出来
(2)minio单机版安装yaml
在安装minio之前,以下几点需要先说明一下
yaml文件内容修改说明:大家只用把写注释的地方改成自己的,其他地方都不用变
安装命令:大家可以把下面yaml文件内容复制到minio.yaml
中,然后上传到linux中,之后通过kubectl apply -f minio.yaml
命令来安装minio
端口解释:对于端口部分解释一下,5000
端口是供浏览器访问UI页面的,而9000
端口是供客户端连接的
访问链接:安装完成之后,可以通过http://ip:port
来访问minio,其中ip就是虚拟机ip,而端口就是5000
端口对应的nodePort端口,比如下面yaml文件中的就是30427
登录信息:登录用户名和密码需要看yaml文件中的MINIO_ROOT_USER
和MINIO_ROOT_PASSWORD
的value值,比如我的就是admin
和admin123456
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: minio
# 名称空间
namespace: minio
spec:
# 副本数量,建议1个,集群版可能存在问题
replicas: 1
selector:
matchLabels:
app: minio
serviceName: minio
template:
metadata:
labels:
app: minio
spec:
containers:
- command:
- /bin/sh
- -c
- minio server /data --console-address ":5000"
env:
- name: MINIO_ROOT_USER
# 登录用户名,按照自己的来设置
value: "admin"
- name: MINIO_ROOT_PASSWORD
# 登录密码,按照自己的来设置
value: "admin123456"
image: minio/minio:RELEASE.2022-03-22T02-05-10Z
name: minio
ports:
- containerPort: 9000
name: data
protocol: TCP
- containerPort: 5000
name: console
protocol: TCP
volumeMounts:
- mountPath: /data
name: data
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteMany
resources:
requests:
# MioIO存储空间大小
storage: 5Gi
# nfs动态挂载存储类名称
storageClassName: "managed-nfs-storage-bakckup"
---
apiVersion: v1
kind: Service
metadata:
labels:
app: minio
name: minio
namespace: minio
spec:
ports:
- name: data
port: 9000
protocol: TCP
targetPort: 9000
# 暴露端口
nodePort: 30024
- name: console
port: 5000
protocol: TCP
targetPort: 5000
# 暴露端口
nodePort: 30427
selector:
app: minio
type: NodePort
登录之后效果如下图:
二、代码
1、pom.xml(说明:minio所用依赖)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>minio-study-springboot</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--minio依赖-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.0</version>
</dependency>
<!-- 解决报错:okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<!--aws 文件上传工具包-->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.263</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.yml(说明:放置minio连接信息、minio文件上传限制)
# minio文件上传大小限制
spring:
servlet:
multipart:
max-file-size: 1GB
max-request-size: 1GB
# minio连接配置
minio:
# minio通信地址
endpoint: http://192.168.139.128:30024
# minio登录用户名
userName: admin
# minio登录密码
password: admin123456
3、MinioProperties.java(说明:读取minio连接信息)
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties("minio")
public class MinioProperties {
/**
* 访问地址 http://localhost:9000
*/
private String endpoint;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
}
4、MinioConfig.java(说明:配置minio连接客户端类)
import com.atguigu.miniostudyspringboot.util.ParallelMinioClient;
import com.atguigu.miniostudyspringboot.util.ReflectUtils;
import io.minio.MinioAsyncClient;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* 传统连接MinIO方式
* @author 明快de玄米61
* @date 2022/12/15 14:22
**/
@Data
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {
@Resource
private MinioProperties minioProperties;
/**
* 注入minio 客户端
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(minioProperties.getEndpoint())
.credentials(minioProperties.getUserName(), minioProperties.getPassword())
.build();
}
@Bean
@ConditionalOnBean({MinioClient.class})
@ConditionalOnMissingBean(ParallelMinioClient.class)
public ParallelMinioClient parallelMinioClient(MinioClient minioClient) {
MinioAsyncClient asyncClient = ReflectUtils.getFieldValue(minioClient, "asyncClient");
return new ParallelMinioClient(asyncClient);
}
}
5、AmazonS3Config.java(说明:配置aws连接minio客户端类)
import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* AWS连接MioIO方式
* @author 明快de玄米61
* @date 2022/12/15 14:23
**/
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class AmazonS3Config {
@Resource
private MinioProperties minioProperties;
@Bean(name = "amazonS3")
public AmazonS3 amazonS3() {
//设置连接时的参数
ClientConfiguration config = new ClientConfiguration();
//设置连接方式,可选参数为HTTP和HTTPS
boolean httpsFlag = minioProperties.getEndpoint().toLowerCase().startsWith("https");
config.setProtocol(httpsFlag ? Protocol.HTTPS : Protocol.HTTP);
//设置网络访问超时时间
config.setConnectionTimeout(5000);
config.setUseExpectContinue(true);
AWSCredentials credentials = new BasicAWSCredentials(minioProperties.getUserName(), minioProperties.getPassword());
//设置Endpoint
AwsClientBuilder.EndpointConfiguration end_point = new AwsClientBuilder.EndpointConfiguration(minioProperties.getEndpoint(), Regions.US_EAST_1.name());
AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
.withClientConfiguration(config)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withEndpointConfiguration(end_point)
.withPathStyleAccessEnabled(true).build();
return amazonS3;
}
}
6、ObjectItem.java(说明:接收Minio返回内容)
import lombok.Data;
@Data
public class ObjectItem {
private String objectName;
private Long size;
}
7、PartInfo.java(说明:转换分片信息,返回给前端)
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 分片信息
*
* @author 明快de玄米61
* @date 2022/12/15 15:59
*/
@Data
@Accessors(chain = true)
public class PartInfo implements Serializable {
private int partNumber;
private String etag;
private String lastModified;
private Long size;
}
8、Task.java(说明:分片上传任务)
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 分片上传任务
*
* @author 明快de玄米61
* @date 2022/12/15 14:24
*/
@Data
@Accessors(chain = true)
public class Task implements Serializable {
// 任务id
private String id;
// 桶名称
private String bucketName;
// 文件名称
private String fileName;
// 存储服务器中的文件绝对路径
private String remoteFileUrl;
// 分片上传的uploadId
private String uploadId;
// 文件大小(byte)
private Long fileSize;
// 分片大小(byte)
private Long chunkSize;
// 分片数量
private Long chunkNum;
// 上传状态(正在上传:0;已暂停:1;上传成功:2;上传失败:3;终止上传:4)
// 说明:暂停、开始都是前端控制,后端只是进行状态记录
private String status;
}
9、AjaxResult.java(说明:控制层统一返回值)
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author ruoyi
*/
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 状态类型
*/
public enum Type
{
/** 成功 */
SUCCESS(0),
/** 警告 */
WARN(301),
/** 错误 */
ERROR(500);
private final int value;
Type(int value)
{
this.value = value;
}
public int value()
{
return this.value;
}
}
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult()
{
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param type 状态类型
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(Type type, String msg, Object data)
{
super.put(CODE_TAG, type.value);
super.put(MSG_TAG, msg);
if (data != null)
{
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success()
{
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data)
{
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg)
{
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data)
{
return new AjaxResult(Type.SUCCESS, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error()
{
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg)
{
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data)
{
return new AjaxResult(Type.ERROR, msg, data);
}
}
10、Constants.java(说明:常量类)
/**
* 常量类
*
* @author 明快de玄米61
* @date 2022/12/15 14:13
*/
public class Constants {
public static final String BUCKET_NAME = "knowledge";
}
11、ReflectUtils.java(说明:反射工具类)
import org.apache.commons.lang3.Validate;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
*
* @author ruoyi
*/
@SuppressWarnings("rawtypes")
public class ReflectUtils
{
/**
* 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
*/
@SuppressWarnings("unchecked")
public static <E> E getFieldValue(final Object obj, final String fieldName)
{
Field field = getAccessibleField(obj, fieldName);
if (field == null)
{
System.out.println("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
return null;
}
E result = null;
try
{
result = (E) field.get(obj);
}
catch (IllegalAccessException e)
{
System.out.printf("不可能抛出的异常%s\n", e.getMessage());
}
return result;
}
/**
* 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
* 如向上转型到Object仍无法找到, 返回null.
*/
public static Field getAccessibleField(final Object obj, final String fieldName)
{
// 为空不报错。直接返回 null
if (obj == null)
{
return null;
}
Validate.notBlank(fieldName, "fieldName can't be blank");
for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass())
{
try
{
Field field = superClass.getDeclaredField(fieldName);
makeAccessible(field);
return field;
}
catch (NoSuchFieldException e)
{
continue;
}
}
return null;
}
/**
* 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
*/
public static void makeAccessible(Field field)
{
if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
|| Modifier.isFinal(field.getModifiers())) && !field.isAccessible())
{
field.setAccessible(true);
}
}
}
12、ParallelMinioClient.java(说明:将分片方法暴露出来)
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.XmlParserException;
import io.minio.messages.Part;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
/**
* Created by TD on 2021/10/26
* 扩展 MinioClient <很多protected 修饰符的分片方法,MinioClient实例对象无法使用,只能自定义类继承使用>
* minio 大文件分片上传思路:
* 1. 前端访问文件服务,请求上传文件,后端返回签名数据及uploadId
* 2. 前端分片文件,携带签名数据及uploadId并发上传分片数据
* 3. 分片上传完成后,访问合并文件接口,后台负责合并文件。
*/
public class ParallelMinioClient extends MinioAsyncClient {
public ParallelMinioClient(MinioAsyncClient client) {
super(client);
}
@Override
public CompletableFuture<CreateMultipartUploadResponse> createMultipartUploadAsync(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
return super.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams);
}
@Override
public CompletableFuture<UploadPartResponse> uploadPartAsync(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
return super.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
}
@Override
public CompletableFuture<ListPartsResponse> listPartsAsync(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
return super.listPartsAsync(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
@Override
public CompletableFuture<ObjectWriteResponse> completeMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
return super.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
@Override
public CompletableFuture<AbortMultipartUploadResponse> abortMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
return super.abortMultipartUploadAsync(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
}
}
13、MinioUtil.java(说明:Minio连接客户端)
import com.atguigu.miniostudyspringboot.entity.ObjectItem;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
@Slf4j
public class MinioUtil {
@Resource
private MinioClient minioClient;
@Resource
private ParallelMinioClient parallelMinioClient;
/**
* 判断存储桶是否存在,不存在则创建
*
* @param bucketName 存储桶名称
*/
public void existBucket(String bucketName) {
try {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查看存储桶是否存在
*
* @param bucketName 存储桶名称
* @return 桶是否存在
*/
public boolean bucketExists(String bucketName) {
boolean found;
try {
found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
found = false;
e.printStackTrace();
}
return found;
}
/**
* 创建存储桶
*
* @param bucketName 存储桶名称
* @return 是否创建成功
*/
public Boolean makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 删除存储桶
*
* @param bucketName 存储桶名称
* @return 是否删除成功
*/
public Boolean removeBucket(String bucketName) {
try {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断对象是否存在
*
* @param bucketName 存储桶名称
* @param objectName MinIO中存储对象全路径
* @return 对象是否存在
*/
public boolean existObject(String bucketName, String objectName) {
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 文件上传
*
* @param bucketName 存储桶名称
* @param file 文件
* @return 桶中位置
*/
public String upload(String bucketName, MultipartFile file) {
MultipartFile[] fileArr = {file};
List<String> fileNames = upload(bucketName, fileArr);
return fileNames.size() == 0 ? null : fileNames.get(0);
}
/**
* 上传文件
*
* @param bucketName 存储桶名称
* @param fileList 文件列表
* @return 桶中位置列表
*/
public List<String> upload(String bucketName, List<MultipartFile> fileList) {
MultipartFile[] fileArr = fileList.toArray(new MultipartFile[0]);
return upload(bucketName, fileArr);
}
/**
* description: 上传文件
*
* @param bucketName 存储桶名称
* @param fileArr 文件列表
* @return 桶中位置列表
*/
public List<String> upload(String bucketName, MultipartFile[] fileArr) {
// 保证桶一定存在
existBucket(bucketName);
// 执行正常操作
List<String> bucketFileNames = new ArrayList<>(fileArr.length);
for (MultipartFile file : fileArr) {
// 获取桶中文件名称
// 获取原始文件名称
String originalFileName = file.getOriginalFilename();
// 获取当前日期,格式例如:2020/11/04
String datePath = new SimpleDateFormat("yyyy/MM-dd/HH").format(new Date());
// 文件名称
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
// 获取文件后缀
String type;
int index = originalFileName.lastIndexOf(46);
if (index != -1) {
type = originalFileName.substring(index + 1);
} else {
type = "unkown";
}
String bucketFileName = datePath + "/" + uuid + "." + type;
// 推送文件到MinIO
try (InputStream in = file.getInputStream()) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(bucketFileName)
.stream(in, in.available(), -1)
.contentType(file.getContentType())
.build()
);
} catch (Exception e) {
e.printStackTrace();
}
bucketFileNames.add(bucketFileName);
}
return bucketFileNames;
}
/**
* 文件下载
*
* @param bucketName 存储桶名称
* @param bucketFileName 桶中文件名称
* @param originalFileName 原始文件名称
* @param response response对象
*/
public void download(String bucketName, String bucketFileName, String originalFileName, HttpServletResponse response) {
GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
try (GetObjectResponse objResponse = minioClient.getObject(objectArgs)) {
byte[] buf = new byte[1024];
int len;
try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
while ((len = objResponse.read(buf)) != -1) {
os.write(buf, 0, len);
}
os.flush();
byte[] bytes = os.toByteArray();
response.setCharacterEncoding("utf-8");
//设置强制下载不打开
response.setContentType("application/force-download");
// 设置附件名称编码
originalFileName = new String(originalFileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
// 设置附件名称
response.addHeader("Content-Disposition", "attachment;fileName=" + originalFileName);
// 写入文件
try (ServletOutputStream stream = response.getOutputStream()) {
stream.write(bytes);
stream.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取已上传对象的文件流
*
* @param bucketName 存储桶名称
* @param bucketFileName 桶中文件名称
* @return 文件流
*/
public InputStream getFileStream(String bucketName, String bucketFileName) throws Exception {
GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
return minioClient.getObject(objectArgs);
}
/**
* 批量删除文件对象结果
*
* @param bucketName 存储桶名称
* @param bucketFileName 桶中文件名称
* @return 删除结果
*/
public DeleteError removeObjectsResult(String bucketName, String bucketFileName) {
List<DeleteError> results = removeObjectsResult(bucketName, Collections.singletonList(bucketFileName));
return results.size() > 0 ? results.get(0) : null;
}
/**
* 批量删除文件对象结果
*
* @param bucketName 存储桶名称
* @param bucketFileNames 桶中文件名称集合
* @return 删除结果
*/
public List<DeleteError> removeObjectsResult(String bucketName, List<String> bucketFileNames) {
Iterable<Result<DeleteError>> results = removeObjects(bucketName, bucketFileNames);
List<DeleteError> res = new ArrayList<>();
for (Result<DeleteError> result : results) {
try {
res.add(result.get());
} catch (Exception e) {
e.printStackTrace();
log.error("遍历删除结果出现错误:" + e.getMessage());
}
}
return res;
}
/**
* 批量删除文件对象
*
* @param bucketName 存储桶名称
* @param bucketFileNames 桶中文件名称集合
*/
private Iterable<Result<DeleteError>> removeObjects(String bucketName, List<String> bucketFileNames) {
List<DeleteObject> dos = bucketFileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
return minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build());
}
/**
* 查看文件对象
*
* @param bucketName 存储桶名称
* @return 文件对象集合
*/
public List<ObjectItem> listObjects(String bucketName) {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).build());
List<ObjectItem> objectItems = new ArrayList<>();
try {
for (Result<Item> result : results) {
Item item = result.get();
ObjectItem objectItem = new ObjectItem();
objectItem.setObjectName(item.objectName());
objectItem.setSize(item.size());
objectItems.add(objectItem);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return objectItems;
}
/**
* 获取桶(桶类型:public)中文件访问url
*
* @param bucketName 存储桶名称
* @param bucketFileName 桶中文件名称
* @return 访问url
*/
public String getUploadedObjectUrlForPublicBucket(String bucketName, String bucketFileName) {
return bucketName + "/" + bucketFileName;
}
/**
* 获取桶(不限制桶类型)中文件访问url
*
* @param bucketName 存储桶名称
* @param bucketFileName 桶中文件名称
* @param expiry 过期时间数量
* @param timeUnit 过期时间单位
* @return 访问url
*/
public String getUploadedObjectUrl(String bucketName, String bucketFileName, Integer expiry, TimeUnit timeUnit) {
GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(bucketFileName)
.expiry(expiry, timeUnit)
.build();
try {
return minioClient.getPresignedObjectUrl(urlArgs);
} catch (Exception e) {
log.error("获取已上传文件的 Url 失败:" + e.getMessage());
return "";
}
}
/**
* 创建分片上传请求
* @author 明快de玄米61
* @date 2022/12/15 12:47
* @param bucketName 桶名称
* @param region 一般填null就行
* @param objectName MinIO中文件全路径
* @param headers 一般只需要设置“Content-Type”
* @return CreateMultipartUploadResponse对象
**/
public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
// 保证桶一定存在
existBucket(bucketName);
// 创建分片上传任务
try {
return parallelMinioClient.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 前端通过后端上传分片到MinIO
* @author 明快de玄米61
* @date 2022/12/15 12:51
* @param bucketName MinIO桶名称
* @param region 一般填null就行
* @param objectName MinIO中文件全路径
* @param data 分片文件,只能接收RandomAccessFile、InputStream类型的,一般使用InputStream类型
* @param length 文件大小
* @param uploadId 文件上传uploadId
* @param partNumber 分片编号
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return UploadPartResponse对象
**/
public UploadPartResponse uploadPart(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取分片上传地址,前端直接上传分片到MinIO
* @author 明快de玄米61
* @date 2022/12/15 12:54
* @param bucketName MinIO桶名称
* @param ossFilePath MinIO中文件全路径
* @param queryParams 查询参数,一般只需要设置“uploadId”和“partNumber”
* @return 分片上传地址
**/
public String getPreSignUploadUrl(String bucketName, String ossFilePath, Map<String, String> queryParams) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(ossFilePath)
.expiry(60 * 60 * 24)
.extraQueryParams(queryParams)
.build());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取已上传的所有分片列表,可以为前端和completeMultipartUpload方法服务
* @author 明快de玄米61
* @date 2022/12/15 12:57
* @param bucketName MinIO桶名称
* @param region 一般填null就行
* @param ossFilePath MinIO中文件全路径
* @param maxParts 最大分片数,一般填写10000即可
* @param partNumberMarker 直接填0即可
* @param uploadId 文件上传uploadId
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return ListPartsResponse对象
**/
public ListPartsResponse listParts(String bucketName, String region, String ossFilePath, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.listPartsAsync(bucketName, region, ossFilePath, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 合并分片
* @author 明快de玄米61
* @date 2022/12/15 12:50
* @param bucketName MinIO桶名称
* @param region 一般填null就行
* @param ossFilePath MinIO中文件全路径
* @param uploadId 文件上传uploadId
* @param parts 分片信息
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return ObjectWriteResponse对象
**/
public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.completeMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, parts, extraHeaders, extraQueryParams).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 删除MinIO中已有分片
* @author 明快de玄米61
* @date 2022/12/15 13:04
* @param bucketName MinIO桶名称
* @param region 一般填null就行
* @param ossFilePath MinIO中文件全路径
* @param uploadId 文件上传uploadId
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return AbortMultipartUploadResponse对象
**/
public AbortMultipartUploadResponse abortMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.abortMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, extraHeaders, extraQueryParams).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
14、SimpleController.java(说明:测试Minio的普通操作)
import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import io.minio.messages.DeleteError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 普通操作
* @author 明快de玄米61
* @date 2022/12/15 14:16
* @param
* @return
**/
@RequestMapping("/simple")
@RestController
public class SimpleController {
@Resource
private MinioUtil minIoUtil;
// 上传文件
@PostMapping("/uploadFile")
public String uploadFile(@RequestParam("file") MultipartFile file) {
return minIoUtil.upload(Constants.BUCKET_NAME, file);
}
// 删除文件
@GetMapping("/deleteFile")
public DeleteError deleteFile(@RequestParam String bucketFileName) {
return minIoUtil.removeObjectsResult(Constants.BUCKET_NAME, bucketFileName);
}
// 下载文件
@GetMapping("/downloadFile")
public void downloadFile(@RequestParam String bucketFileName, @RequestParam String originalFilename, HttpServletResponse response) {
minIoUtil.download(Constants.BUCKET_NAME, bucketFileName, originalFilename, response);
}
// 获取文件临时分享地址
@GetMapping("/shareUrl")
public String shareUrl(@RequestParam String bucketFileName) {
return minIoUtil.getUploadedObjectUrl(Constants.BUCKET_NAME, bucketFileName, 7, TimeUnit.DAYS);
}
}
15、MinioShardingController.java(说明:测试原生Minio的分片上传操作)
import com.atguigu.miniostudyspringboot.entity.PartInfo;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.messages.Part;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* MioIO工具类分片上传
* 说明:
* 1、使用MinIO传统依赖即可,不用新增依赖
* 2、配置MinioConfig即可
*
* @author 明快de玄米61
* @date 2022/12/15 14:15
*/
@RequestMapping("/minio")
@RestController
public class MinioShardingController {
/** 分片上传任务集合 **/
private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();
@Resource
private MinioUtil minIoUtil;
/**
* 获取任务id
* @author 明快de玄米61
* @date 2022/12/15 14:31
* @param fileName 文件原始名称
* @param fileSize 文件大小(byte)
* @param chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
* @return 任务id
**/
@GetMapping("/initTask")
public AjaxResult initTask(
@RequestParam String fileName,
@RequestParam Long fileSize,
@RequestParam Long chunkSize
) {
// 获取uploadId
String suffix = FilenameUtils.getExtension(fileName);
String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
HashMultimap<String, String> headers = HashMultimap.create();
headers.put("Content-Type", contentType);
CreateMultipartUploadResponse response = minIoUtil.createMultipartUpload(
Constants.BUCKET_NAME,
null,
remoteFileUrl,
headers,
null
);
String uploadId = response.result().uploadId();
// 创建分片上传任务
long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
Task task = new Task();
task.setId(UUID.randomUUID().toString().replace("-", ""))
.setBucketName(Constants.BUCKET_NAME)
.setFileName(fileName)
.setRemoteFileUrl(remoteFileUrl)
.setUploadId(uploadId)
.setFileSize(fileSize)
.setChunkSize(chunkSize)
.setChunkNum(chunkNum)
.setStatus("0");
TASK_LIST.add(task);
return AjaxResult.success("操作成功" ,task.getId());
}
/**
* 上传分片
* 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
* @author 明快de玄米61
* @date 2022/12/15 14:56
* @param file 分片文件
* @param id 任务id
* @param partNumber 当前分片编号
**/
@PostMapping("/uploadPart")
public AjaxResult uploadPart(
@RequestPart MultipartFile file,
@RequestParam String id,
@RequestParam Integer partNumber
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 上传分片
try {
UploadPartResponse response = minIoUtil.uploadPart(
task.getBucketName(),
null,
task.getRemoteFileUrl(),
file.getInputStream(),
file.getSize(),
task.getUploadId(),
partNumber,
null,
null
);
return AjaxResult.success();
} catch (IOException e) {
e.printStackTrace();
}
return AjaxResult.error();
}
/**
* 获取分片上传地址
* 说明:前端直接上传分片到MinIO,对比:uploadPart
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @param partNumber 当前分片编号
* @return
**/
@GetMapping("/preSignUploadUrl")
public AjaxResult preSignUploadUrl(
@RequestParam String id,
@RequestParam Integer partNumber
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 获取分片上传地址
Map<String, String> reqParams = new HashMap<>();
reqParams.put("uploadId", task.getUploadId());
reqParams.put("partNumber", partNumber.toString());
String preSignUploadUrl = minIoUtil.getPreSignUploadUrl(
Constants.BUCKET_NAME,
task.getRemoteFileUrl(),
reqParams
);
return AjaxResult.success("操作成功", preSignUploadUrl);
}
/**
* 合并分片
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @return
**/
@GetMapping("/merge")
public AjaxResult merge(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 判断已上传分片数量
ListPartsResponse listPartsResponse = minIoUtil.listParts(
Constants.BUCKET_NAME,
null,
task.getRemoteFileUrl(),
10000,
0,
task.getUploadId(),
null,
null
);
List<Part> parts = listPartsResponse.result().partList();
if (parts.size() != task.getChunkNum()) {
return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
}
// 合并分片
ObjectWriteResponse objectWriteResponse = minIoUtil.completeMultipartUpload(
Constants.BUCKET_NAME,
null,
task.getRemoteFileUrl(),
task.getUploadId(),
parts.toArray(new Part[0]),
null,
null
);
// 更新任务状态
task.setStatus("1");
return AjaxResult.success();
}
/**
* 终止分片上传
* 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
* @author 明快de玄米61
* @date 2022/12/15 16:08
* @param id 任务id
* @return
**/
@GetMapping("/abort")
public AjaxResult abort(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 终止分片上传
AbortMultipartUploadResponse response = minIoUtil.abortMultipartUpload(
Constants.BUCKET_NAME,
null,
task.getRemoteFileUrl(),
task.getUploadId(),
null,
null
);
// 更新任务状态
task.setStatus("4");
return AjaxResult.success();
}
/**
* 获取任务中以上传的分片列表,用于断点续传
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @return
**/
@GetMapping("/taskInfo")
public AjaxResult taskInfo(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 判断文件是否合成
boolean exist = minIoUtil.existObject(Constants.BUCKET_NAME, task.getRemoteFileUrl());
if (exist) {
return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
}
// 查询分片集合
ListPartsResponse listPartsResponse = minIoUtil.listParts(
Constants.BUCKET_NAME,
null,
task.getRemoteFileUrl(),
10000,
0,
task.getUploadId(),
null,
null
);
// 返回结果格式转换
List<PartInfo> result = listPartsResponse.result().partList().stream().map(p -> {
String lastModified = p.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS"));
return new PartInfo().setSize(p.partSize()).setEtag(p.etag()).setLastModified(lastModified).setPartNumber(p.partNumber());
}).collect(Collectors.toList());
return AjaxResult.success(result);
}
/**
* 获取上传分片任务集合
* @author 明快de玄米61
* @date 2022/12/15 14:54
* @return 上传分片任务集合
**/
@GetMapping("/getTaskList")
public List<Task> getTaskList(){
return TASK_LIST;
}
/**
* 根据任务id获取上传分片任务
* @author 明快de玄米61
* @date 2022/12/15 14:55
* @param id 任务id
* @return 上传分片任务
**/
@GetMapping("/getTaskById")
public Task getTaskById(@RequestParam String id){
for (Task task : TASK_LIST) {
if (task.getId().equals(id)) {
return task;
}
}
return null;
}
}
16、AwsShardingController.java(说明:测试AWS分片上传操作)
import cn.hutool.core.date.DateUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* AWS分片上传
* 说明:
* 1、需要新增以下依赖
* <dependency>
* <groupId>com.amazonaws</groupId>
* <artifactId>aws-java-sdk-s3</artifactId>
* <version>1.12.263</version>
* </dependency>
* 2、新增AmazonS3Config配置类
*
* @author 明快de玄米61
* @date 2022/12/15 14:15
*/
@RequestMapping("/aws")
@RestController
public class AwsShardingController {
/** 分片上传任务集合 **/
private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();
@Resource
private AmazonS3 amazonS3;
/**
* 获取任务id
* @author 明快de玄米61
* @date 2022/12/15 14:31
* @param fileName 文件原始名称
* @param fileSize 文件大小(byte)
* @param chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
* @return 任务id
**/
@GetMapping("/initTask")
public AjaxResult initTask(
@RequestParam String fileName,
@RequestParam Long fileSize,
@RequestParam Long chunkSize
) {
// 获取uploadId
String suffix = FilenameUtils.getExtension(fileName);
String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);
InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3
.initiateMultipartUpload(new InitiateMultipartUploadRequest(Constants.BUCKET_NAME, remoteFileUrl).withObjectMetadata(objectMetadata));
String uploadId = initiateMultipartUploadResult.getUploadId();
// 创建分片上传任务
long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
Task task = new Task();
task.setId(UUID.randomUUID().toString().replace("-", ""))
.setBucketName(Constants.BUCKET_NAME)
.setFileName(fileName)
.setRemoteFileUrl(remoteFileUrl)
.setUploadId(uploadId)
.setFileSize(fileSize)
.setChunkSize(chunkSize)
.setChunkNum(chunkNum)
.setStatus("0");
TASK_LIST.add(task);
return AjaxResult.success("操作成功" ,task.getId());
}
/**
* 上传分片
* 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
* @author 明快de玄米61
* @date 2022/12/15 14:56
* @param file 分片文件
* @param id 任务id
* @param partNumber 当前分片编号
**/
@PostMapping("/uploadPart")
public AjaxResult uploadPart(
@RequestPart MultipartFile file,
@RequestParam String id,
@RequestParam Integer partNumber
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 上传分片
try {
UploadPartRequest request = new UploadPartRequest();
request.setInputStream(file.getInputStream());
request.setBucketName(task.getBucketName());
request.setKey(task.getRemoteFileUrl());
request.setUploadId(task.getUploadId());
request.setPartNumber(partNumber);
request.setPartSize(file.getInputStream().available());
UploadPartResult uploadPartResult = amazonS3.uploadPart(request);
return AjaxResult.success(uploadPartResult);
} catch (IOException e) {
e.printStackTrace();
}
return AjaxResult.error();
}
/**
* 获取分片上传地址
* 说明:前端直接上传分片到MinIO,对比:uploadPart
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @param partNumber 当前分片编号
* @return
**/
@GetMapping("/preSignUploadUrl")
public AjaxResult preSignUploadUrl(
@RequestParam String id,
@RequestParam Integer partNumber
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 获取分片上传地址
// 设置上传分片链接有效时间
Date expireDate = DateUtil.offsetMillisecond(new Date(), 60 * 60 * 1000);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(task.getBucketName(), task.getRemoteFileUrl())
.withExpiration(expireDate).withMethod(HttpMethod.PUT);
// 设置uploadId
request.addRequestParameter("uploadId", task.getUploadId());
// 设置分片数量
request.addRequestParameter("partNumber", partNumber.toString());
// 获取分片上传地址
URL preSignUploadUrl = amazonS3.generatePresignedUrl(request);
return AjaxResult.success("操作成功", preSignUploadUrl);
}
/**
* 合并分片
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @return
**/
@GetMapping("/merge")
public AjaxResult merge(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 判断已上传分片数量
ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
PartListing partListing = amazonS3.listParts(listPartsRequest);
List<PartSummary> parts = partListing.getParts();
if (parts.size() != task.getChunkNum()) {
return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
}
// 合并分片
CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
.withBucketName(task.getBucketName())
.withKey(task.getRemoteFileUrl())
.withUploadId(task.getUploadId())
.withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
CompleteMultipartUploadResult result = amazonS3.completeMultipartUpload(completeMultipartUploadRequest);
// 更新任务状态
task.setStatus("1");
return AjaxResult.success(result);
}
/**
* 终止分片上传
* 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
* @author 明快de玄米61
* @date 2022/12/15 16:08
* @param id 任务id
* @return
**/
@GetMapping("/abort")
public AjaxResult abort(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 终止分片上传
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
amazonS3.abortMultipartUpload(abortMultipartUploadRequest);
// 更新任务状态
task.setStatus("4");
return AjaxResult.success();
}
/**
* 获取任务中以上传的分片列表,用于断点续传
* @author 明快de玄米61
* @date 2022/12/15 15:13
* @param id 任务id
* @return
**/
@GetMapping("/taskInfo")
public AjaxResult taskInfo(
@RequestParam String id
) {
// 获取任务
Task task = getTaskById(id);
if (task == null) {
return AjaxResult.error("不存在该任务,任务id:" + id);
}
// 判断文件是否合成
boolean exist = amazonS3.doesObjectExist(task.getBucketName(), task.getRemoteFileUrl());
if (exist) {
return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
}
// 查询分片集合
ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
PartListing partListing = amazonS3.listParts(listPartsRequest);
return AjaxResult.success(partListing.getParts());
}
/**
* 获取上传分片任务集合
* @author 明快de玄米61
* @date 2022/12/15 14:54
* @return 上传分片任务集合
**/
@GetMapping("/getTaskList")
public List<Task> getTaskList(){
return TASK_LIST;
}
/**
* 根据任务id获取上传分片任务
* @author 明快de玄米61
* @date 2022/12/15 14:55
* @param id 任务id
* @return 上传分片任务
**/
@GetMapping("/getTaskById")
public Task getTaskById(@RequestParam String id){
for (Task task : TASK_LIST) {
if (task.getId().equals(id)) {
return task;
}
}
return null;
}
}
三、测试
1、普通操作(对应:SimpleController.java)
1.1、上传文件
1.2、删除文件
1.3、下载文件
1.4、获取文件临时分享地址
2、Minio分片上传(对应:MinioShardingController)
2.1、初始化分片上传任务
返回值中data里面是任务id
2.2、分片上传
有多少个分片就需要调用多少次该接口
2.3、查看任务中已上传分片列表
该接口可以帮助前端判断未上传分片,做到断点续传
2.4、合并分片
所有分片上传完毕之后,前端调用该接口进行分片合并
2.5、终止分片
假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口
3、AWS分片上传(对应:AwsShardingController)
3.1、初始化分片上传任务
返回值中data里面是任务id
3.2、分片上传
有多少个分片就需要调用多少次该接口
在这里插入图片描述
3.3、查看任务中已上传分片列表
该接口可以帮助前端判断未上传分片,做到断点续传
3.4、合并分片
所有分片上传完毕之后,前端调用该接口进行分片合并
3.5、终止分片
假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口
四、资料
五、拓展—分片上传
1、前端分片,后端组合分片(适用范围:本地存储)
推荐大家去看奇闻网盘
的 qiwen-file 项目,该项目中分片上传接口方法如下所示:
类全路径名称: com.qiwenshare.file.controller.FiletransferController
方法名: uploadFile
如果你去看源码,需要拉取这几个项目:
- 前端项目:https://gitee.com/qiwen-cloud/qiwen-file-web
- 后端主体项目:https://gitee.com/qiwen-cloud/qiwen-file
- 后端文件上传项目:https://gitee.com/qiwen-cloud/ufop-spring-boot-starter
- 后端公共项目:https://gitee.com/qiwen-cloud/qiwen-common
奇文网盘项目实现了前端分片之后,后端实现了以下几种存储方式:
- 本地存储:后端接收分片,并按照顺序在本地组合分片,组合完成后上传到本地存储位置
- 阿里云OSS存储:后端接收分片,并上传分片到阿里云,当所有分片接收完成后,调用阿里云组合分片接口进行分片组合
- FastDFS存储:后端接收分片,并上传分片到FastDFS,当所有分片接收完成后,那就上传完成了
- MinIO存储:后端接收分片,并按照顺序在本地组合分片,组合完成后在上传到MinIO;这种方式我是不推荐的,原因是MinIO支持分片上传,那我们可以直接把分片上传到MinIO,最终分片上传完成后调用MinIO进行分片合并就好了,可以极大解决本地存储空间
2、前端分片,后端把分片传递给文件存储服务器,调用文件存储服务器的接口组合分片(适用范围:Minio、阿里云OSS存储等)
推荐大家去看暴走的咖喱
的minio-upload项目,以及明珠
的spring-boot-minio项目,我们下面来详细介绍一下这两个项目的区别,最后我在补充一些我在项目中用到的其他内容
2.1、minio-upload
项目: https://gitee.com/Gary2016/minio-upload
博客: https://blog.csdn.net/weixin_44359036/article/details/126514643
说明: 这个项目后端没有使用传统连接Minio的方式,而是使用AWS SDK for Java
来操作MinIO Server,这种方式也是MinIO官方文档中推荐的方式之一。这个项目我是真正跑起来的,并且实现了分片上传功能,大家按照作者的README.md
中的说明文档操作即可,该项目中的一个出色点是获取contentType
,这可以让文件上传到文件存储服务器之后保证文件、文件格式都正确,目前没有看到其他项目做到这一点,具体代码如下:
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
// 作者在getMediaType方法中写的是key,而我写的是fileName,其实作用是类似的,只要参数中有后缀就好了
String contentType = MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);
另外它对流程的把控也是非常nice的,基本流程是创建上传任务、根据分片获取上传地址(上传地址有实效限制,但是这个时间点足够了)、上传分片、合并分片,另外还可以查询已上传分片列表,做到断点续传功能
2.2、spring-boot-minio
项目: https://gitee.com/pearl88/study-demo/tree/master/spring-boot-minio
博客: https://blog.csdn.net/qq_43437874/article/details/123429986
说明: 这个项目后端使用传统连接Minio的方式,虽然在新的minio依赖中,这种方式已经失效了,比如在我上面的minio版本中就已经失效了,但是这种思想依然没有改变的。不过该项目的不好的一点是对contentType
的处理,仅仅是简单使用application/octet-stream
来代表所有文件的类型,这种方式将会造成上传文本文件多处一些多余内容,所以建议使用上面介绍minio-upload
提到的contentType
获取方法;另外不好的一点是对上传地址的获取方式,在该项目中是直接在创建上传任务的时候返回所有分片的上传地址,不过这个上传地址有效时间是固定的,并且有最大值,但是用户可能将任务暂停很久很久,所以不建议使用这种方式,而建议使用上传哪个分片就先获取分片上传地址的方式
2.3、公司项目
我在项目中借鉴的minio-upload
的做法,也是使用AWS SDK for Java
来操作MinIO Server,主要补充两点
1、删除已上传分片
在实际使用中,大文件上传过程中肯定是允许用户中途删除文件的,这种情况下我们需要删除Minio中的分片,避免这些垃圾分片占用存储空间
// 参数解释:MinIO桶名称、文件在MinIO中存储的绝对路径、文件上传uploadId
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(record.getBucketName(), record.getRemoteFileUrl(), record.getUploadId());
amazonS3.abortMultipartUpload(abortMultipartUploadRequest);
上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#abortMultipartUploadAsync
2、前端通过后端上传分片到MinIO
我借鉴的是minio-upload
项目中的思路,所以我让前端通过Minio返回的分片上传地址上传分片到MinIO,但是前端那边报跨域,另外公司前辈说不建议把MinIO的连接信息暴露给前端,另外以后K8S上部署的MinIO甚至都不会以NodePort方式暴露出来,那这种方式就无法使用了,所以最好还是前端通过后端上传分片到Minio最好,我转念一想大佬说的也有道理,那就我后端中转一下吧,具体代码如下:
Controller:
@PostMapping("/uploadPart")
@ApiOperation(value = "上传分片", notes = "上传分片")
public CommonRes<UploadPartResult> uploadPart(
@ApiParam(value = "分片文件", required = true) @RequestPart("file") @Validated MultipartFile file,
@ApiParam(value = "主键id,用于获取上传信息", required = true) @RequestParam(value = "id") @Validated String id,
@ApiParam(value = "分片编号", required = true) @RequestParam(value = "partNumber") @Validated() Integer partNumber
) {
return manager.uploadPart(file, id, partNumber);
}
ServiceImpl:
@SneakyThrows
@Override
public CommonRes<UploadPartResult> uploadPart(MultipartFile file, String id, Integer partNumber) {
// 参数判断
UploadRecord record = service.getById(id);
if (record == null) {
// 避免删除接口先调用成功,然后才接收到前端调用上传分片的接口请求,之后导致报错,因此设置状态码是200
return CommonRes.error(HttpStatusConstants.SUCCESS, "上传记录不存在");
}
// 上传分片
UploadPartRequest request = new UploadPartRequest();
// 前端上传分片流
request.setInputStream(file.getInputStream());
// MioIO桶名称
request.setBucketName(record.getBucketName());
// MinIO中的最终合成文件全路径
request.setKey(record.getRemoteFileUrl());
// 文件分片上传uploadId
request.setUploadId(record.getUploadId());
// 分片编号
request.setPartNumber(partNumber);
// 分片大小
request.setPartSize(file.getInputStream().available());
// 上传分片到MinIO
UploadPartResult uploadPartResult = amazonS3.uploadPart(request);
return CommonRes.ok(uploadPartResult);
}
上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#uploadPartAsync