Title
2、文件md5校验
@GetMapping(value = "/check")
public Map checkFileExists(String md5) {
Map resultMap = new HashMap<>();
if (ObjectUtils.isEmpty(md5)) {
resultMap.put("status", StatusCode.PARAM_ERROR.getCode());
return resultMap;
}
// 先从Redis中查询
String url = (String) jsonRedisTemplate.boundHashOps(MD5_KEY).get(md5);
// 文件不存在
if (ObjectUtils.isEmpty(url)) {
resultMap.put("status", StatusCode.NOT_FOUND.getCode());
return resultMap;
}
resultMap.put("status", StatusCode.SUCCESS.getCode());
resultMap.put("url", url);
// 文件已经存在了
return resultMap;
}
3、文件分片上传
@PostMapping(value = "/upload")
public Map upload(HttpServletRequest req) {
Map map = new HashMap<>();
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req;
// 获得文件分片数据
MultipartFile file = multipartRequest.getFile("data");
// 上传过程中出现异常,状态码设置为50000
if (file == null) {
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
// 分片第几片
int index = Integer.parseInt(multipartRequest.getParameter("index"));
// 总片数
int total = Integer.parseInt(multipartRequest.getParameter("total"));
// 获取文件名
String fileName = multipartRequest.getParameter("name");
String md5 = multipartRequest.getParameter("md5");
// 创建文件桶
minioTemplate.makeBucket(md5);
String objectName = String.valueOf(index);
log.info("index: {}, total:{}, fileName:{}, md5:{}, objectName:{}", index, total, fileName, md5, objectName);
// 当不是最后一片时,上传返回的状态码为20001
if (index < total) {
try {
// 上传文件
OssFile ossFile = minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
log.info("{} upload success {}", objectName, ossFile);
// 设置上传分片的状态
map.put("status", StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getCode());
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
} else {
// 为最后一片时状态码为20002
try {
// 上传文件
minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
// 设置上传分片的状态
map.put("status", StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getCode());
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
}
}
4、文件合并
@GetMapping(value = "/merge")
public Map merge(Integer shardCount, String fileName, String md5, String fileType,
Long fileSize) {
Map retMap = new HashMap<>();
try {
// 查询片数据
List objectNameList = minioTemplate.listObjectNames(md5);
if (shardCount != objectNameList.size()) {
// 失败
retMap.put("status", StatusCode.FAILURE.getCode());
} else {
// 开始合并请求
String targetBucketName = minioConfig.getBucketName();
String filenameExtension = StringUtils.getFilenameExtension(fileName);
String fileNameWithoutExtension = UUID.randomUUID().toString();
String objectName = fileNameWithoutExtension + "." + filenameExtension;
minioTemplate.composeObject(md5, targetBucketName, objectName);
log.info("桶:{} 中的分片文件,已经在桶:{},文件 {} 合并成功", md5, targetBucketName, objectName);
// 合并成功之后删除对应的临时桶
minioTemplate.removeBucket(md5, true);
log.info("删除桶 {} 成功", md5);
// 计算文件的md5
String fileMd5 = null;
try (InputStream inputStream = minioTemplate.getObject(targetBucketName, objectName)) {
fileMd5 = Md5Util.calculateMd5(inputStream);
} catch (IOException e) {
log.error("", e);
}
// 计算文件真实的类型
String type = null;
try (InputStream inputStreamCopy = minioTemplate.getObject(targetBucketName, objectName)) {
type = FileTypeUtil.getType(inputStreamCopy);
} catch (IOException e) {
log.error("", e);
}
// 并和前台的md5进行对比
if (!ObjectUtils.isEmpty(fileMd5) && !ObjectUtils.isEmpty(type) && fileMd5.equalsIgnoreCase(md5) && type.equalsIgnoreCase(fileType)) {
// 表示是同一个文件, 且文件后缀名没有被修改过
String url = minioTemplate.getPresignedObjectUrl(targetBucketName, objectName);
// 存入redis中
jsonRedisTemplate.boundHashOps(MD5_KEY).put(fileMd5, url);
// 成功
retMap.put("status", StatusCode.SUCCESS.getCode());
} else {
log.error("非法的文件信息: 分片数量:{}, 文件名称:{}, 文件md5:{}, 文件类型:{}, 文件大小:{}",
shardCount, fileName, md5, fileType, fileSize);
// 并需要删除对象
minioTemplate.deleteObject(targetBucketName, objectName);
retMap.put("status", StatusCode.FAILURE.getCode());
}
}
} catch (Exception e) {
log.error("", e);
// 失败
retMap.put("status", StatusCode.FAILURE.getCode());
}
return retMap;
}
5、测试
5.1、测试大文件分片上传
5.1.1、浏览器输入:http://localhost:18002/file/home/upload
5.1.2、去Minio客户端查看上传的文件
http://192.168.211.132:9090/buckets/minio-test/browse
5.2、测试秒传 6、完整的代码 6.1、pom.xml6.2、yaml4.0.0 org.springframework.boot spring-boot-starter-parent 2.6.7 cn.lyf springboot-minio-demo2 1.0.0-SNAPSHOT springboot-minio-demo2 springboot-minio-demo2 1.8 true 3.17.0 8.3.9 4.9.1 5.8.0.M4 2.11.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-configuration-processor true com.fasterxml.jackson.datatype jackson-datatype-jsr310 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core io.minio minio ${minio.version} com.squareup.okhttp3 okhttp ${okhttp.version} cn.hutool hutool-all ${hutool-all.version} org.redisson redisson ${redisson.version} redis.clients jedis org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf commons-io commons-io ${commons-io.version} org.apache.commons commons-lang3 org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
server:
port: 18002
spring:
application:
name: minio-application
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
redis:
database: 0
host: 192.168.211.132
port: 6379
jedis:
pool:
max-active: 200
max-wait: -1
max-idle: 10
min-idle: 0
timeout: 2000
thymeleaf:
#模板的模式,支持 HTML, XML TEXT JAVASCRIPT
mode: HTML5
#编码 可不用配置
encoding: UTF-8
#开发配置为false,避免修改模板还要重启服务器
cache: false
#配置模板路径,默认是templates,可以不用配置
prefix: classpath:/templates/
servlet:
content-type: text/html
minio:
endpoint: http://192.168.211.132:9000
accessKey: admin
secretKey: admin123456
bucketName: minio-test
6.3、注入Minio配置
6.3.1、MinioConfig.java
package cn.lyf.minio.config;
import cn.lyf.minio.core.MinioTemplate;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
@ConfigurationProperties(value = "minio")
public class MinioConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
@Bean(name = "minioClient")
public MinioClient initMinioClient() {
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
}
@Bean(name = "minioTemplate")
public MinioTemplate minioTemplate() {
return new MinioTemplate(initMinioClient(), this);
}
}
6.4、注入Redis配置
6.4.1、RedisConfig.java
package cn.lyf.minio.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Bean
public JedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory(new RedisStandaloneConfiguration(redisHost, Integer.parseInt(redisPort)));
}
@Bean(name = "jsonRedisTemplate")
public RedisTemplate redisTemplate(JedisConnectionFactory connectionFactory) {
return getRedisTemplate(connectionFactory, genericJackson2JsonRedisSerializer());
}
@Bean
@Primary // 当存在多个Bean时,此bean优先级最高
public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// 解决查询缓存转换异常的问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
// 支持 jdk 1.8 日期 ---- start ---
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule())
.registerModule(new ParameterNamesModule());
// --end --
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
@Bean
public RedissonClient redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort).setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "jdkRedisTemplate")
public RedisTemplate redisTemplateByJdkSerialization(JedisConnectionFactory connectionFactory) {
return getRedisTemplate(connectionFactory, new JdkSerializationRedisSerializer());
}
private RedisTemplate getRedisTemplate(JedisConnectionFactory connectionFactory,
RedisSerializer> redisSerializer) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(redisSerializer);
connectionFactory.afterPropertiesSet();
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
6.5、MinioTemplate.java
package cn.lyf.minio.core;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.lyf.minio.config.MinioConfig;
import cn.lyf.minio.entity.OssFile;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@AllArgsConstructor
public class MinioTemplate {
private MinioClient minioClient;
private MinioConfig minioConfig;
@SneakyThrows
public List listBuckets() {
return minioClient.listBuckets();
}
@SneakyThrows
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
@SneakyThrows
public void makeBucket(String bucketName) {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
@SneakyThrows
public void removeBucket(String bucketName) {
removeBucket(bucketName, false);
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
@SneakyThrows
public void removeBucket(String bucketName, boolean bucketNotNull) {
if (bucketNotNull) {
deleteBucketAllObject(bucketName);
}
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
@SneakyThrows
public OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {
String uuidFileName = generateFileInMinioName(originalFileName);
try {
if (ObjectUtils.isEmpty(bucketName)) {
bucketName = minioConfig.getBucketName();
}
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(uuidFileName)
.stream(inputStream, inputStream.available(), -1)
.build());
return new OssFile(uuidFileName, originalFileName);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
@SneakyThrows
public void deleteBucketAllObject(String bucketName) {
List list = listObjectNames(bucketName);
if (!list.isEmpty()) {
for (String objectName : list) {
deleteObject(bucketName, objectName);
}
}
}
@SneakyThrows
public List listObjectNames(String bucketName) {
List objectNameList = new ArrayList<>();
if (bucketExists(bucketName)) {
Iterable> results = listObjects(bucketName, true);
for (Result- result : results) {
String objectName = result.get().objectName();
objectNameList.add(objectName);
}
}
return objectNameList;
}
@SneakyThrows
public void deleteObject(String bucketName, String objectName) {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
@SneakyThrows
public OssFile putChunkObject(InputStream inputStream, String bucketName, String objectName) {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build());
return new OssFile(objectName, objectName);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
@SneakyThrows
public String getPresignedObjectUrl(String bucketName, String filePath) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(filePath)
.build());
}
@SneakyThrows
public String getPresignedObjectUrl(String bucketName, String filePath, Map
queryParams) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(filePath)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(queryParams)
.build());
}
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
return minioClient.getObject(
GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
@SneakyThrows
public Iterable> listObjects(String bucketName, boolean recursive) {
return minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
}
@SneakyThrows
public Map getPresignedPostFormData(String bucketName, String fileName) {
// 为存储桶创建一个上传策略,过期时间为7天
PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));
// 设置一个参数key,值为上传对象的名称
policy.addEqualsCondition("key", fileName);
// 添加Content-Type,例如以"image/"开头,表示只能上传照片,这里吃吃所有
policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);
// 设置上传文件的大小 64kiB to 10MiB.
//policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
return minioClient.getPresignedPostFormData(policy);
}
public String generateFileInMinioName(String originalFilename) {
return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;
}
@PostConstruct
public void initDefaultBucket() {
String defaultBucketName = minioConfig.getBucketName();
if (bucketExists(defaultBucketName)) {
log.info("默认存储桶:defaultBucketName已存在");
} else {
log.info("创建默认存储桶:defaultBucketName");
makeBucket(minioConfig.getBucketName());
}
;
}
@SneakyThrows
public OssFile composeObject(String bucketName, String fileName, List sourceObjectList) {
String filenameExtension = StringUtils.getFilenameExtension(fileName);
String objectName = UUID.randomUUID() + "." + filenameExtension;
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.sources(sourceObjectList)
.build());
String presignedObjectUrl = getPresignedObjectUrl(bucketName, fileName);
return new OssFile(presignedObjectUrl, fileName);
}
@SneakyThrows
public OssFile composeObject(List sourceObjectList, String bucketName, String objectName) {
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.sources(sourceObjectList)
.build());
String presignedObjectUrl = getPresignedObjectUrl(bucketName, objectName);
return new OssFile(presignedObjectUrl, objectName);
}
@SneakyThrows
public OssFile composeObject(String originBucketName, String targetBucketName, String objectName) {
Iterable> results = listObjects(originBucketName, true);
List objectNameList = new ArrayList<>();
for (Result- result : results) {
Item item = result.get();
objectNameList.add(item.objectName());
}
if (ObjectUtils.isEmpty(objectNameList)) {
throw new IllegalArgumentException(originBucketName + "桶中没有文件,请检查");
}
List
composeSourceList = new ArrayList<>(objectNameList.size());
// 对文件名集合进行升序排序
objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);
for (String object : objectNameList) {
composeSourceList.add(ComposeSource.builder()
.bucket(originBucketName)
.object(object)
.build());
}
return composeObject(composeSourceList, targetBucketName, objectName);
}
}
6.6、OssFile.java
package cn.lyf.minio.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OssFile {
private String ossFilePath;
private String originalFileName;
}
6.7、StatusCode.java
package cn.lyf.minio.entity;
import lombok.Getter;
public enum StatusCode {
SUCCESS(20000, "操作成功"),
PARAM_ERROR(40000, "参数异常"),
NOT_FOUND(40004, "资源不存在"),
FAILURE(50000, "系统异常"),
CUSTOM_FAILURE(50001, "自定义异常错误"),
ALONE_CHUNK_UPLOAD_SUCCESS(20001, "分片上传成功的标识"),
ALL_CHUNK_UPLOAD_SUCCESS(20002, "所有的分片均上传成功");
@Getter
private final Integer code;
@Getter
private final String message;
StatusCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
6.8、Md5Util.java
package cn.lyf.minio.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@Slf4j
public final class Md5Util {
private static final int BUFFER_SIZE = 8 * 1024;
private static final char[] HEX_CHARS =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private Md5Util() {
}
public static String calculateMd5(byte[] bytes) {
try {
MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
return encodeHex(md5MessageDigest.digest(bytes));
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("no md5 found");
}
}
public static String calculateMd5(InputStream inputStream) {
try {
MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
try (BufferedInputStream bis = new BufferedInputStream(inputStream);
DigestInputStream digestInputStream = new DigestInputStream(bis, md5MessageDigest)) {
final byte[] buffer = new byte[BUFFER_SIZE];
while (digestInputStream.read(buffer) > 0) {
// 获取最终的MessageDigest
md5MessageDigest = digestInputStream.getMessageDigest();
}
return encodeHex(md5MessageDigest.digest());
} catch (IOException ioException) {
log.error("", ioException);
throw new IllegalArgumentException(ioException.getMessage());
}
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("no md5 found");
}
}
public static String calculateMd5(String input) {
try {
// 拿到一个MD5转换器(如果想要SHA1参数,可以换成SHA1)
MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");
byte[] inputByteArray = input.getBytes(StandardCharsets.UTF_8);
md5MessageDigest.update(inputByteArray);
// 转换并返回结果,也是字节数组,包含16个元素
byte[] resultByteArray = md5MessageDigest.digest();
// 将字符数组转成字符串返回
return encodeHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("md5 not found");
}
}
private static String encodeHex(byte[] bytes) {
char[] chars = new char[32];
for (int i = 0; i < chars.length; i = i + 2) {
byte b = bytes[i / 2];
chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];
chars[i + 1] = HEX_CHARS[b & 0xf];
}
return new String(chars);
}
}
6.9、MinioFileController.java
package cn.lyf.minio.controller;
import cn.hutool.core.io.FileTypeUtil;
import cn.lyf.minio.config.MinioConfig;
import cn.lyf.minio.core.MinioTemplate;
import cn.lyf.minio.entity.OssFile;
import cn.lyf.minio.entity.StatusCode;
import cn.lyf.minio.utils.Md5Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping(value = "/file")
@Slf4j
@CrossOrigin // 允许跨域
public class MinioFileController {
private static final String MD5_KEY = "cn:lyf:minio:demo:file:md5List";
@Autowired
private MinioTemplate minioTemplate;
@Autowired
private MinioConfig minioConfig;
@Resource(name = "jsonRedisTemplate")
private RedisTemplate jsonRedisTemplate;
@RequestMapping(value = "/home/upload")
public ModelAndView homeUpload() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("upload");
return modelAndView;
}
@GetMapping(value = "/check")
public Map checkFileExists(String md5) {
Map resultMap = new HashMap<>();
if (ObjectUtils.isEmpty(md5)) {
resultMap.put("status", StatusCode.PARAM_ERROR.getCode());
return resultMap;
}
// 先从Redis中查询
String url = (String) jsonRedisTemplate.boundHashOps(MD5_KEY).get(md5);
// 文件不存在
if (ObjectUtils.isEmpty(url)) {
resultMap.put("status", StatusCode.NOT_FOUND.getCode());
return resultMap;
}
resultMap.put("status", StatusCode.SUCCESS.getCode());
resultMap.put("url", url);
// 文件已经存在了
return resultMap;
}
@PostMapping(value = "/upload")
public Map upload(HttpServletRequest req) {
Map map = new HashMap<>();
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req;
// 获得文件分片数据
MultipartFile file = multipartRequest.getFile("data");
// 上传过程中出现异常,状态码设置为50000
if (file == null) {
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
// 分片第几片
int index = Integer.parseInt(multipartRequest.getParameter("index"));
// 总片数
int total = Integer.parseInt(multipartRequest.getParameter("total"));
// 获取文件名
String fileName = multipartRequest.getParameter("name");
String md5 = multipartRequest.getParameter("md5");
// 创建文件桶
minioTemplate.makeBucket(md5);
String objectName = String.valueOf(index);
log.info("index: {}, total:{}, fileName:{}, md5:{}, objectName:{}", index, total, fileName, md5, objectName);
// 当不是最后一片时,上传返回的状态码为20001
if (index < total) {
try {
// 上传文件
OssFile ossFile = minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
log.info("{} upload success {}", objectName, ossFile);
// 设置上传分片的状态
map.put("status", StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getCode());
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
} else {
// 为最后一片时状态码为20002
try {
// 上传文件
minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
// 设置上传分片的状态
map.put("status", StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getCode());
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("status", StatusCode.FAILURE.getCode());
return map;
}
}
}
@GetMapping(value = "/merge")
public Map merge(Integer shardCount, String fileName, String md5, String fileType,
Long fileSize) {
Map retMap = new HashMap<>();
try {
// 查询片数据
List objectNameList = minioTemplate.listObjectNames(md5);
if (shardCount != objectNameList.size()) {
// 失败
retMap.put("status", StatusCode.FAILURE.getCode());
} else {
// 开始合并请求
String targetBucketName = minioConfig.getBucketName();
String filenameExtension = StringUtils.getFilenameExtension(fileName);
String fileNameWithoutExtension = UUID.randomUUID().toString();
String objectName = fileNameWithoutExtension + "." + filenameExtension;
minioTemplate.composeObject(md5, targetBucketName, objectName);
log.info("桶:{} 中的分片文件,已经在桶:{},文件 {} 合并成功", md5, targetBucketName, objectName);
// 合并成功之后删除对应的临时桶
minioTemplate.removeBucket(md5, true);
log.info("删除桶 {} 成功", md5);
// 计算文件的md5
String fileMd5 = null;
try (InputStream inputStream = minioTemplate.getObject(targetBucketName, objectName)) {
fileMd5 = Md5Util.calculateMd5(inputStream);
} catch (IOException e) {
log.error("", e);
}
// 计算文件真实的类型
String type = null;
try (InputStream inputStreamCopy = minioTemplate.getObject(targetBucketName, objectName)) {
type = FileTypeUtil.getType(inputStreamCopy);
} catch (IOException e) {
log.error("", e);
}
// 并和前台的md5进行对比
if (!ObjectUtils.isEmpty(fileMd5) && !ObjectUtils.isEmpty(type) && fileMd5.equalsIgnoreCase(md5) && type.equalsIgnoreCase(fileType)) {
// 表示是同一个文件, 且文件后缀名没有被修改过
String url = minioTemplate.getPresignedObjectUrl(targetBucketName, objectName);
// 存入redis中
jsonRedisTemplate.boundHashOps(MD5_KEY).put(fileMd5, url);
// 成功
retMap.put("status", StatusCode.SUCCESS.getCode());
} else {
log.error("非法的文件信息: 分片数量:{}, 文件名称:{}, 文件md5:{}, 文件类型:{}, 文件大小:{}",
shardCount, fileName, md5, fileType, fileSize);
// 并需要删除对象
minioTemplate.deleteObject(targetBucketName, objectName);
retMap.put("status", StatusCode.FAILURE.getCode());
}
}
} catch (Exception e) {
log.error("", e);
// 失败
retMap.put("status", StatusCode.FAILURE.getCode());
}
return retMap;
}
}
6.10、启动类
package cn.lyf.minio;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Minio2Application {
public static void main(String[] args) {
SpringApplication.run(Minio2Application.class, args);
}
}
7、分片上传的核心代码 io.minio.MinioClient#composeObject
public ObjectWriteResponse composeObject(ComposeObjectArgs args)
throws ErrorResponseException, InsufficientDataException, InternalException,
InvalidKeyException, InvalidResponseException, IOException, NoSuchAlgorithmException,
ServerException, XmlParserException {
checkArgs(args);
args.validateSse(this.baseUrl);
List sources = args.sources();
int partCount = calculatePartCount(sources);
if (partCount == 1
&& args.sources().get(0).offset() == null
&& args.sources().get(0).length() == null) {
return copyObject(new CopyObjectArgs(args));
}
Multimap headers = newMultimap(args.extraHeaders());
headers.putAll(args.genHeaders());
CreateMultipartUploadResponse createMultipartUploadResponse =
createMultipartUpload(
args.bucket(), args.region(), args.object(), headers, args.extraQueryParams());
String uploadId = createMultipartUploadResponse.result().uploadId();
Multimap ssecHeaders = HashMultimap.create();
if (args.sse() != null && args.sse() instanceof ServerSideEncryptionCustomerKey) {
ssecHeaders.putAll(newMultimap(args.sse().headers()));
}
try {
int partNumber = 0;
Part[] totalParts = new Part[partCount];
for (ComposeSource src : sources) {
long size = src.objectSize();
if (src.length() != null) {
size = src.length();
} else if (src.offset() != null) {
size -= src.offset();
}
long offset = 0;
if (src.offset() != null) {
offset = src.offset();
}
headers = newMultimap(src.headers());
headers.putAll(ssecHeaders);
if (size <= ObjectWriteArgs.MAX_PART_SIZE) {
partNumber++;
if (src.length() != null) {
headers.put(
"x-amz-copy-source-range", "bytes=" + offset + "-" + (offset + src.length() - 1));
} else if (src.offset() != null) {
headers.put("x-amz-copy-source-range", "bytes=" + offset + "-" + (offset + size - 1));
}
UploadPartCopyResponse response =
uploadPartCopy(
args.bucket(), args.region(), args.object(), uploadId, partNumber, headers, null);
String eTag = response.result().etag();
totalParts[partNumber - 1] = new Part(partNumber, eTag);
continue;
}
while (size > 0) {
partNumber++;
long startBytes = offset;
long endBytes = startBytes + ObjectWriteArgs.MAX_PART_SIZE;
if (size < ObjectWriteArgs.MAX_PART_SIZE) {
endBytes = startBytes + size;
}
Multimap headersCopy = newMultimap(headers);
headersCopy.put("x-amz-copy-source-range", "bytes=" + startBytes + "-" + endBytes);
UploadPartCopyResponse response =
uploadPartCopy(
args.bucket(),
args.region(),
args.object(),
uploadId,
partNumber,
headersCopy,
null);
String eTag = response.result().etag();
totalParts[partNumber - 1] = new Part(partNumber, eTag);
offset = startBytes;
size -= (endBytes - startBytes);
}
}
return completeMultipartUpload(
args.bucket(),
getRegion(args.bucket(), args.region()),
args.object(),
uploadId,
totalParts,
null,
null);
} catch (RuntimeException e) {
abortMultipartUpload(args.bucket(), args.region(), args.object(), uploadId, null, null);
throw e;
} catch (Exception e) {
abortMultipartUpload(args.bucket(), args.region(), args.object(), uploadId, null, null);
throw e;
}
}



