初步流程:
选择上传文件 -> 提取md5 -> 请求后端校验此md5的文件是否已经上传过 -> 如果有上传就返回信息告诉前端上传完成(秒传) -> 如果没有则根据此md5获取已上传的分片有哪些,未上传的分片有多少个就返回多少个上传url
如何获取已上传的分片有哪些呢?
minio api有一个生成上传url的api,这个api可以指定接下来要上传文件的文件名,也就是说,在上传步骤,我们只要保证上传的分片文件是有规则的,那么我们就可以很轻松的获取到
举个例子:上传文件的大小为10m,分片2个,md5为 asdfghjkl,则我们生成2个上传url,每个url指定的文件名为 md5/分片序号.chunk(分片文件要不要文件后缀都可以) 的格式,也就是 asdfghjkl/1.chunk;asdfghjkl/2.chunk
所有分片文件上传完成后,通过 listObjects 获取到 asdfghjkl前缀开头的所有文件,即是所有分片文件的path,最后合并文件即可
为什么不所有步骤都采用前端直连Minio?毕竟官方的sdk也提供了前端的示例
这样做会直接暴露有关于minio的敏感信息,并且不管怎样,始终也需要后端去记录最终文件上传完成后的信息,所以一些敏感的操作,还是由后端程序来协助
这个流程的好处:
省掉每个分片的md5计算
省掉每上传一个分片文件就请求后端记录一次分片信息
省掉每个分片的数据库存储,合并文件后只需要删除分片文件,不需要再删除数据库数据
可以并发上传
文件上传直连于minio服务,不经过后端程序
分片文件更方便管理,比如分片文件上传到一个B桶里,合并的文件合并到A桶去
不暴露敏感信息
后端程序在整个流程里,仅仅充当了md5校验、生成url、通知minio合并文件的角色
后端代码实现如下
1、配置文件
spring.servlet.multipart.max-file-size=1000MB
spring.servlet.multipart.max-request-size=1000MB
spring.servlet.multipart.file-size-threshold=1000MB
spring.jackson.default-property-inclusion=non_null
minio.server.url=http://192.168.5.202:9000
minio.server.accessKey=admin
minio.server.secretKey=admin123
minio.bucket.img=img-library
minio.bucket.chunk=chunk
2、工具类
package com.anx.minio.controller;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
@Component
public class MinIoUtils {
@Value("${minio.server.url}")
private String url;
@Value("${minio.server.accessKey}")
private String accessKey;
@Value("${minio.server.secretKey}")
private String secretKey;
/**
* 合并后存储的桶名称
*/
@Value("${minio.bucket.img}")
private String imgBucket;
/**
* 分片存储的桶名称
*/
@Value("${minio.bucket.chunk}")
private String chunkBucKet;
private static MinioClient minioClient;
/**
* 排序
*/
public final static boolean SORT = true;
/**
* 不排序
*/
public final static boolean NOT_SORT = false;
/**
* 默认过期时间(分钟)
*/
private final static Integer DEFAULT_EXPIRY = 60;
/**
* 初始化MinIo对象
* 非此工具类请勿使用该方法
*/
@PostConstruct
public void init() {
minioClient = MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
//方便管理分片文件,则单独创建一个分片文件的存储桶
if (!isBucketExist(chunkBucKet)) {
createBucket(chunkBucKet);
}
if (!isBucketExist(imgBucket)) {
createBucket(imgBucket);
}
}
/**
* 存储桶是否存在
*
* @param bucketName 存储桶名称
* @return true/false
*/
@SneakyThrows
public static boolean isBucketExist(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建存储桶
*
* @param bucketName 存储桶名称
* @return true/false
*/
@SneakyThrows
public static boolean createBucket(String bucketName) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
return true;
}
/**
* 获取访问对象的外链地址
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param expiry 过期时间(分钟) 最大为7天 超过7天则默认最大值
* @return viewUrl
*/
@SneakyThrows
public static String getObjectUrl(String bucketName, String objectName, Integer expiry) {
expiry = expiryHandle(expiry);
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expiry)
.build()
);
}
/**
* 创建上传文件对象的外链
*
* @param bucketName 存储桶名称
* @param objectName 欲上传文件对象的名称
* @param expiry 过期时间(分钟) 最大为7天 超过7天则默认最大值
* @return uploadUrl
*/
@SneakyThrows
public static String createUploadUrl(String bucketName, String objectName, Integer expiry) {
expiry = expiryHandle(expiry);
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(expiry)
.build()
);
}
/**
* 创建上传文件对象的外链
*
* @param bucketName 存储桶名称
* @param objectName 欲上传文件对象的名称
* @return uploadUrl
*/
public static String createUploadUrl(String bucketName, String objectName) {
return createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY);
}
/**
* 批量创建分片上传外链
*
* @param bucketName 存储桶名称
* @param objectMD5 欲上传分片文件主文件的MD5
* @param chunkCount 分片数量
* @return uploadChunkUrls
*/
public static List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) {
if (null == objectMD5) {
return null;
}
objectMD5 += "/";
if (null == chunkCount || 0 == chunkCount) {
return null;
}
List<String> urlList = new ArrayList<>(chunkCount);
for (int i = 1; i <= chunkCount; i++) {
String objectName = objectMD5 + i + ".chunk";
urlList.add(createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY));
}
return urlList;
}
/**
* 创建指定序号的分片文件上传外链
*
* @param bucketName 存储桶名称
* @param objectMD5 欲上传分片文件主文件的MD5
* @param partNumber 分片序号
* @return uploadChunkUrl
*/
public static String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) {
if (null == objectMD5) {
return null;
}
objectMD5 += "/" + partNumber + ".chunk";
return createUploadUrl(bucketName, objectMD5, DEFAULT_EXPIRY);
}
/**
* 获取对象文件名称列表
*
* @param bucketName 存储桶名称
* @param prefix 对象名称前缀
* @param sort 是否排序(升序)
* @return objectNames
*/
@SneakyThrows
public static List<String> listObjectNames(String bucketName, String prefix, Boolean sort) {
ListObjectsArgs listObjectsArgs;
if (null == prefix) {
listObjectsArgs = ListObjectsArgs.builder()
.bucket(bucketName)
.recursive(true)
.build();
} else {
listObjectsArgs = ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(true)
.build();
}
Iterable<Result<Item>> chunks = minioClient.listObjects(listObjectsArgs);
List<String> chunkPaths = new ArrayList<>();
for (Result<Item> item : chunks) {
chunkPaths.add(item.get().objectName());
}
if (sort) {
return chunkPaths.stream().distinct().collect(Collectors.toList());
}
return chunkPaths;
}
/**
* 获取对象文件名称列表
*
* @param bucketName 存储桶名称
* @param prefix 对象名称前缀
* @return objectNames
*/
public static List<String> listObjectNames(String bucketName, String prefix) {
return listObjectNames(bucketName, prefix, NOT_SORT);
}
/**
* 获取分片文件名称列表
*
* @param bucketName 存储桶名称
* @param objectMd5 对象Md5
* @return objectChunkNames
*/
public static List<String> listChunkObjectNames(String bucketName, String objectMd5) {
if (null == objectMd5) {
return null;
}
return listObjectNames(bucketName, objectMd5, SORT);
}
/**
* 获取分片名称地址HashMap key=分片序号 value=分片文件地址
*
* @param chunkBucKet 存储桶名称
* @param objectMd5 对象Md5
* @return objectChunkNameMap
*/
public static Map<Integer, String> mapChunkObjectNames(String chunkBucKet, String objectMd5) {
if (null == objectMd5) {
return null;
}
List<String> chunkPaths = listObjectNames(chunkBucKet, objectMd5);
if (null == chunkPaths || chunkPaths.size() == 0) {
return null;
}
Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
for (String chunkName : chunkPaths) {
Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
chunkMap.put(partNumber, chunkName);
}
return chunkMap;
}
/**
* 合并分片文件成对象文件
*
* @param chunkBucKetName 分片文件所在存储桶名称
* @param composeBucketName 合并后的对象文件存储的存储桶名称
* @param chunkNames 分片文件名称集合
* @param objectName 合并后的对象文件名称
* @return true/false
*/
@SneakyThrows
public static boolean composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) {
//合并文件
List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());