文章目录
一、minio后端
1.1 新增分片上传下载类
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Part;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @author zb
* @Description
* 扩展 MinioClient <很多protected 修饰符的分片方法,自定义类继承使用>
* minio 大文件分片上传思路:
* 1. 前端访问文件服务,请求上传文件,后端返回签名数据及uploadId
* 2. 前端分片文件,携带签名数据及uploadId并发上传分片数据
* 3. 分片上传完成后,访问合并文件接口,后台负责合并文件。
* <!-- minio依赖-->
* <dependency>
* <groupId>io.minio</groupId>
* <artifactId>minio</artifactId>
* <version>8.4.6</version>
* </dependency>\
* 从8.4.0开始支持sdk异步操作 MinioAsyncClient,但是内部使用的的是 CompletableFuture 而且使用的是 默认线程池,有一定的弊端
*
*/
public class ParallelMinioClient extends MinioClient{
public ParallelMinioClient(MinioClient client) {
super(client);
}
@Override
public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName,
Multimap<String, String> headers, Multimap<String, String> extraQueryParams)
throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException,
ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
}
@Override
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)
throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException,
ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return super.uploadPart(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
}
@Override
public ListPartsResponse listParts(String bucketName, String region, String objectName, Integer maxParts,
Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders,
Multimap<String, String> extraQueryParams)
throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException,
ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException{
return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
@Override
public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId,
Part[] parts, Multimap<String, String> extraHeaders,
Multimap<String, String> extraQueryParams)
throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException,
ServerException, XmlParserException, ErrorResponseException, InternalException,InvalidResponseException {
return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
@Override
public AbortMultipartUploadResponse abortMultipartUpload(String bucketName, String region, String objectName,
String uploadId, Multimap<String, String> extraHeaders,
Multimap<String, String> extraQueryParams)
throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException,
ServerException, XmlParserException, ErrorResponseException, InternalException,InvalidResponseException {
return super.abortMultipartUpload(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
}
} ParallelMinioClient
1.2 新增自动配置类和配置
import com.command.file.constant.DefaultMinioBucketConst;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* @author zb
*
* @Description
*/
@RefreshScope
@Data
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
/**
* secretKey
*/
private String secretKey;
/**
* accessKey
*/
private String accessKey;
/**
* minio地址,填写一个nginx地址或者vip地址,由nginx转发
*/
private String url;
/**
* 桶名称
*/
private String bucketName = DefaultMinioBucketConst.DEFAULT_STORE_BUCKET_NAME;
/**
* 链接有效时间,单位 秒,默认 7天
*/
private Integer linkExpiry = 7 * 24 * 60 * 60;
/**
* 是否创建缩略图
*/
private Boolean createThumbnail = Boolean.TRUE;
/**
* 是否计算音视频时长
*/
private Boolean calculationDuration = Boolean.TRUE;
///////////////////////////okHttp相关配置///////////////////////////
/**
* 总的最大链接数
*/
private Integer httpMaxRequest = 128;
/**
* 每台机器的最大请求数量
*/
private Integer httpMaxRequestsPerHost = 10;
/**
* 链接超时时间,单位 秒,默认 1 分钟
*/
private Integer connectTimeout = 60;
/**
* 写超时时间,单位 秒,默认 1 分钟
*/
private Integer writeTimeout = 60;
/**
* 读取超时时间,单位 秒,默认 1 分钟
*/
private Integer readTimeout = 60;
/**
* http链接最大空闲数
*/
private Integer maxIdleConnections = 10;
/**
* 保持keepAlive的时间,单位 分钟
*/
private Long keepAliveDuration = 5L;
} MinioProperties
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import io.minio.MinioClient;
import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
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;
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author zb
*
* @Description
*/
@EnableConfigurationProperties(MinioProperties.class)
@Configuration
public class MinioAutoConfiguration {
@Resource
private MinioProperties minioProperties;
/********************************minio相关配置*********************************/
@Bean
@ConditionalOnMissingBean({MinioClient.class})
public MinioClient minioClient() {
Dispatcher dispatcher = new Dispatcher();
// 设置最大请求数量
dispatcher.setMaxRequests(minioProperties.getHttpMaxRequest());
// 设置每台主机的最大请求数量
dispatcher.setMaxRequestsPerHost(minioProperties.getHttpMaxRequestsPerHost());
OkHttpClient okHttpClient = new OkHttpClient.Builder()
// 取消https验证
.sslSocketFactory(SSLUtils.getSSLSocketFactory(), SSLUtils.getX509TrustManager())
.hostnameVerifier(SSLUtils.getHostnameVerifier())
.connectTimeout(minioProperties.getConnectTimeout(), TimeUnit.SECONDS)
.writeTimeout(minioProperties.getWriteTimeout(), TimeUnit.SECONDS)
.readTimeout(minioProperties.getReadTimeout(), TimeUnit.SECONDS)
// 支持协议
.protocols(Collections.singletonList(Protocol.HTTP_1_1))
// 最大空闲连接,保持活动时间
.connectionPool(new ConnectionPool(minioProperties.getMaxIdleConnections(), minioProperties.getKeepAliveDuration(), TimeUnit.MINUTES))
.dispatcher(dispatcher).build();
String url = minioProperties.getUrl();
if (StrUtil.endWith(url, StringPool.SLASH)){
url = StrUtil.subBefore(url,StringPool.SLASH, true);
}
return MinioClient.builder().endpoint(url)
.credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
.httpClient(okHttpClient)
.build();
}
/**
* 用于分片上传
*/
@Bean("parallelMinioClient")
@ConditionalOnBean({MinioClient.class})
@ConditionalOnMissingBean(ParallelMinioClient.class)
public ParallelMinioClient parallelMinioClient(MinioClient minioClient){
return new ParallelMinioClient(minioClient);
}
// /**
// * 取消https 验证
// * @return
// */
// public OkHttpClient getUnsafeOkHttpClient() {
// try {
// final TrustManager[] trustAllCerts = new TrustManager[]{
// new X509TrustManager() {
// @Override
// public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//
// }
//
// @Override
// public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//
// }
//
// @Override
// public X509Certificate[] getAcceptedIssuers() {
// return new X509Certificate[]{};
// }
// }
// };
//
// final SSLContext sslContext = SSLContext.getInstance("SSL");
// sslContext.init(null, trustAllCerts, new SecureRandom());
// final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
// OkHttpClient.Builder builder = new OkHttpClient.Builder();
//
// Dispatcher dispatcher = new Dispatcher();
// // 设置最大请求数量
// dispatcher.setMaxRequests(minioProperties.getHttpMaxRequest());
// // 设置每台主机的最大请求数量
// dispatcher.setMaxRequestsPerHost(minioProperties.getHttpMaxRequestsPerHost());
// builder
// .connectTimeout(minioProperties.getConnectTimeout(), TimeUnit.SECONDS)
// .writeTimeout(minioProperties.getWriteTimeout(), TimeUnit.SECONDS)
// .readTimeout(minioProperties.getReadTimeout(), TimeUnit.SECONDS)
// // 支持协议
// .protocols(Collections.singletonList(Protocol.HTTP_1_1))
// // 最大空闲连接,保持活动时间
// .connectionPool(new ConnectionPool(minioProperties.getMaxIdleConnections(), minioProperties.getKeepAliveDuration() , TimeUnit.MINUTES))
// .dispatcher(dispatcher)
// .sslSocketFactory(sslSocketFactory);
//
// builder.hostnameVerifier(new HostnameVerifier() {
// @Override
// public boolean verify(String s, SSLSession sslSession) {
// return true;
// }
// });
// return builder.build();
//
// } catch (NoSuchAlgorithmException | KeyManagementException e) {
// e.printStackTrace();
// }
// return null;
// }
//
// /**
// * copied logic from
// * https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java
// */
// private OkHttpClient enableExternalCertificates(OkHttpClient httpClient, String filename)
// throws GeneralSecurityException, IOException {
// Collection<? extends Certificate> certificates = null;
// try (FileInputStream fis = new FileInputStream(filename)) {
// certificates = CertificateFactory.getInstance("X.509").generateCertificates(fis);
// }
//
// if (certificates == null || certificates.isEmpty()) {
// throw new IllegalArgumentException("expected non-empty set of trusted certificates");
// }
// // Any password will work.
// char[] password = "password".toCharArray();
//
// // Put the certificates a key store.
// KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
// // By convention, 'null' creates an empty key store.
// keyStore.load(null, password);
//
// int index = 0;
// for (Certificate certificate : certificates) {
// String certificateAlias = Integer.toString(index++);
// keyStore.setCertificateEntry(certificateAlias, certificate);
// }
//
// // Use it to build an X509 trust manager.
// KeyManagerFactory keyManagerFactory =
// KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// keyManagerFactory.init(keyStore, password);
// TrustManagerFactory trustManagerFactory =
// TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// trustManagerFactory.init(keyStore);
//
// final KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
// final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
//
// SSLContext sslContext = SSLContext.getInstance("TLS");
// sslContext.init(keyManagers, trustManagers, null);
// SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
//
// return httpClient
// .newBuilder()
// .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])
// .build();
// }
} MinioAutoConfiguration
1.3新增模板操作类
package cn.mesmile.storage.plus;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.mesmile.storage.plus.entity.ProgressInputStream;
import cn.mesmile.storage.plus.entity.UploadListener;
import cn.mesmile.storage.plus.exceptions.ObjectStorageException;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.*;
import kotlin.Pair;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Headers;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author zb
* @Description
*/
@Slf4j
public class MinioTemplate {
///////////////////////////////////////// 桶相关API start ////////////////////////////////////////////
/**
* 检查桶是否存在
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @return 返回桶是否存在
*/
public boolean existsBucket(MinioClient minioClient, String bucketName) {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
try {
return minioClient.bucketExists(bucketExistsArgs);
} catch (Exception e) {
log.error("检查桶是否存在异常,桶名称{}", bucketName, e);
throw new ObjectStorageException("检查存储桶异常");
}
}
/**
* 创建桶,创建桶之前请先检查桶是否存在
*
* @param minioClient minio客户端
* @param bucketName 桶名称
*/
public void createBucket(MinioClient minioClient, String bucketName) {
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder()
.bucket(bucketName).objectLock(false).build();
try {
minioClient.makeBucket(makeBucketArgs);
} catch (Exception e) {
log.error("创建桶异常,桶名称{}", bucketName, e);
throw new ObjectStorageException("创建存储桶异常");
}
}
/**
* 删除一个空的桶,当桶中存在文件的时候无法删除桶
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @return
*/
public boolean removeEmptyBucket(MinioClient minioClient, String bucketName) {
RemoveBucketArgs removeBucketArgs = RemoveBucketArgs.builder().bucket(bucketName).build();
try {
minioClient.removeBucket(removeBucketArgs);
} catch (Exception e) {
log.error("删除桶异常,桶名称{}", bucketName, e);
throw new ObjectStorageException("删除存储桶异常");
}
return true;
}
/**
* 当前minio服务端所有桶信息
*
* @param minioClient minio客户端
* @return 当前minio服务端所有桶信息
*/
public List<Bucket> listBuckets(MinioClient minioClient) {
try {
return minioClient.listBuckets();
} catch (Exception e) {
log.error("列举桶异常", e);
throw new ObjectStorageException("列举存储桶异常");
}
}
///////////////////////////////////////// 桶相关API end /////////////////////////////////////////////
///////////////////////////////////////// 文件相关API start ///////////////////////////////////////////
/**
* 普通上传文件
* @param minioClient minio客户端
* @param bucketName minio桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param multipartFile 文件封装体
* @param defaultPartSize 是否采用默认分片大小,sdk内部针对大于5M的文件进行分片上传默认分片大小为5M
* @return 返回上传结果
*/
public ObjectWriteResponse upload(MinioClient minioClient, String bucketName,
String path, String filename, MultipartFile multipartFile,boolean defaultPartSize){
InputStream inputStream = null;
int available = 0;
try {
inputStream = multipartFile.getInputStream();
available = inputStream.available();
} catch (Exception e) {
e.printStackTrace();
}
return upload(minioClient,bucketName,path,filename,inputStream,available,defaultPartSize);
}
/**
* 普通上传文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param inputStream 文件流
* @param objectSize 对象大小(单位: byte) 1MB = 1 *1024 * 1024 byte;
* @return 上传结果
*/
public ObjectWriteResponse upload(MinioClient minioClient, String bucketName, String path, String filename,
InputStream inputStream, long objectSize) {
// 默认采用sdk内部的自动分片大小(默认大小为5M),当文件大小大于5M的时候,sdk内部会将文件进行分片上传到minio
long partSize = getPartSize(objectSize, true);
return upload(minioClient, bucketName, path, filename, inputStream, objectSize, partSize);
}
/**
* 普通上传文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param inputStream 文件流
* @param objectSize 对象大小(单位: byte) 1MB = 1 *1024 * 1024 byte;
* @param defaultPartSize 是否使用默认的分片大小
* @return 上传结果
*/
public ObjectWriteResponse upload(MinioClient minioClient, String bucketName, String path, String filename,
InputStream inputStream, long objectSize, boolean defaultPartSize) {
long partSize = getPartSize(objectSize, defaultPartSize);
return upload(minioClient, bucketName, path, filename, inputStream, objectSize, partSize);
}
/**
* 普通上传文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param inputStream 文件流
* @param objectSize 对象大小(单位: byte) 1MB = 1 *1024 * 1024 byte;
* @param partSize 文件分片大小(单位: byte) 分片大小的范围为 5MB 到 5GB (前后都包含)
* @return 上传结果
*/
public ObjectWriteResponse upload(MinioClient minioClient, String bucketName, String path, String filename,
InputStream inputStream, long objectSize, long partSize) {
// 默认"application/octet-stream"
MediaType mediaType = MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
String contentType = mediaType.toString();
String objectName = getObjectName(path, filename);
String taskIdStr = IdUtil.simpleUUID();
ProgressInputStream progressInputStream = new ProgressInputStream(inputStream, taskIdStr, new UploadListener() {
@Override
public void onProgress(String taskId, int process) {
System.out.println("【" + taskId + "】 process = " + process);
}
});
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucketName)
.contentType(contentType)
.object(objectName)
.stream(progressInputStream, objectSize, partSize).build();
try {
return minioClient.putObject(putObjectArgs);
} catch (Exception e) {
log.error("上传文件异常, bucketName {},path {},filename {},objectSize {},partSize {}"
, bucketName, path, filename, objectSize, partSize, e);
throw new ObjectStorageException("列举存储桶异常");
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
progressInputStream.close();
} catch (IOException e) {
log.error("关闭流异常, bucketName {},path {},filename {},objectSize {},partSize {}"
, bucketName, path, filename, objectSize, partSize, e);
}
}
}
/**
* 获取object名称,为 path+filename
*
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @return 获取object名称
*/
private String getObjectName(String path, String filename) {
if (StrUtil.isEmpty(filename)) {
filename = IdUtil.simpleUUID();
}
String objectName = filename;
boolean endWith = StrUtil.endWith(path, StrPool.C_SLASH);
boolean pathNotEmpty = StrUtil.isNotEmpty(path);
if (!endWith && pathNotEmpty) {
objectName = path + "/" + filename;
}
return objectName;
}
/**
* 获取自定义的分片大小
*
* @param objectSize 文件大小
* @param defaultPartSize 使用默认的分片大小,minio-sdk会自动计算分片大小
* @return 分片大小(单位 : byte 分片大小范围在 5MB到5GB 范围前后都包含)
*/
public long getPartSize(long objectSize, boolean defaultPartSize) {
long partSize = -1;
if (defaultPartSize || objectSize <= 0) {
return partSize;
}
// todo ObjectWriteArgs.MIN_MULTIPART_SIZE * 4 可由配置项 20MB
long maxPartSize = ObjectWriteArgs.MIN_MULTIPART_SIZE * 4;
if (objectSize > ObjectWriteArgs.MIN_MULTIPART_SIZE && objectSize <= maxPartSize) {
partSize = objectSize;
} else if (objectSize > maxPartSize) {
partSize = maxPartSize;
}
return partSize;
}
/**
* 下载文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @return 文件
*/
public GetObjectResponse getObjectResponse(MinioClient minioClient, String bucketName, String path, String filename) {
try {
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucketName)
.object(getObjectName(path, filename)).build();
return minioClient.getObject(getObjectArgs);
} catch (Exception e) {
log.error("下载文件异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("下载文件异常");
}
}
/**
* 下载文件
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param response http响应
* @param originalFileName 文件原有名称
*/
public void download (MinioClient minioClient, String bucketName, String path,
String filename, HttpServletResponse response, String originalFileName) {
GetObjectResponse objectResponse = getObjectResponse(minioClient, bucketName, path, filename);
if (objectResponse != null) {
Headers headers = objectResponse.headers();
for (Pair<? extends String, ? extends String> header : headers) {
String first = header.getFirst();
String second = header.getSecond();
// 追加请求头
response.addHeader(first, second);
}
int length;
byte[] bytes = new byte[2048];
try (OutputStream os = new BufferedOutputStream(response.getOutputStream())) {
while ((length = objectResponse.read(bytes)) != -1) {
os.write(bytes, 0, length);
}
os.flush();
response.setCharacterEncoding("utf-8");
if (StrUtil.isEmpty(originalFileName)){
originalFileName = filename;
}
Optional<MediaType> mediaTypeOptional = MediaTypeFactory.getMediaType(originalFileName);
if (mediaTypeOptional.isPresent()) {
// 设置原本的类型
response.setContentType(mediaTypeOptional.get().toString());
}else {
//设置强制下载不打开
response.setContentType("application/force-download");
}
// 这里URLEncoder.encode可以防止中文乱码
originalFileName = URLEncoder.encode(originalFileName, "UTF-8").replaceAll("\\+", "%20");
// 设置附件名称
response.addHeader("Content-Disposition", "attachment;fileName=" + originalFileName);
//在此处开放Content-Disposition权限,前端代码才能获取到
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
} catch (Exception e) {
log.error("下载文件异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("下载文件异常");
} finally {
try {
objectResponse.close();
} catch (IOException e) {
log.error("关闭流异常",e);
}
}
}
}
/**
* 删除文件
*
* @param minioClient minio客户端
* @param bucketName 文件桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
*/
public void removeFile(MinioClient minioClient, String bucketName, String path, String filename) {
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucketName)
.object(getObjectName(path, filename)).build();
try {
minioClient.removeObject(removeObjectArgs);
} catch (Exception e) {
log.error("删除文件异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("删除文件异常");
}
}
/**
* 一次删除多个文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param objects 需要删除的对象
* @return 删除结果
*/
public Iterable<Result<DeleteError>> removeFiles(MinioClient minioClient, String bucketName, Iterable<DeleteObject> objects) {
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucketName)
.objects(objects).build();
return minioClient.removeObjects(removeObjectsArgs);
}
/**
* 列举某个桶下面的文件
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param prefix 前缀
* @param startAfter 从哪个文件之后
* @return 文件列表信息
*/
public Iterable<Result<Item>> listFile(MinioClient minioClient, String bucketName,
String prefix, String startAfter) {
// 最多返回 1000 个文件
ListObjectsArgs.Builder builder = ListObjectsArgs.builder().bucket(bucketName)
.maxKeys(1000);
if (StrUtil.isNotEmpty(prefix)) {
builder.prefix(prefix);
}
if (StrUtil.isNotEmpty(startAfter)) {
builder.startAfter(startAfter);
}
return minioClient.listObjects(builder.build());
}
/**
* 查询文件信息
*
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @return 文件信息
*/
public StatObjectResponse selectFile(MinioClient minioClient, String bucketName,
String path, String filename) {
StatObjectArgs.Builder objectBuilder = StatObjectArgs.builder().bucket(bucketName)
.object(getObjectName(path, filename));
try {
return minioClient.statObject(objectBuilder.build());
} catch (Exception e) {
log.error("查询文件异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("查询文件异常");
}
}
/**
* 获取⽂件分享链接,带有过期时间
*
* @param minioClient minio客户端
* @param bucketName bucket名称
* @param expirySeconds 过期时间,预签名URL的默认过期时间为7天(单位:秒)
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @return 分享链接
*/
public String getObjectShareUrl(MinioClient minioClient, String bucketName, int expirySeconds
, String path, String filename) {
if (expirySeconds <= 0) {
expirySeconds = GetPresignedObjectUrlArgs.DEFAULT_EXPIRY_TIME;
}
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(getObjectName(path, filename))
.expiry(expirySeconds, TimeUnit.SECONDS).build();
try {
return minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
} catch (Exception e) {
log.error("获取文件分享链接异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("获取文件分享链接异常");
}
}
/**
* 合并对象,将多个文件合并到一起
* 这个方法存在一个问题,文件大的时候合并比较慢(并且原来的文件也会保存),推荐用文件分片的形式来完成
* @param minioClient minio客户端
* @param bucketName 桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param sources 分片对象源
* @return 合并结果
*/
public ObjectWriteResponse composeObject(MinioClient minioClient, String bucketName, String path, String filename,
List<ComposeSource> sources) {
ComposeObjectArgs.Builder composeObjectBuilder = ComposeObjectArgs.builder()
.bucket(bucketName).object(getObjectName(path, filename))
.sources(sources);
try {
return minioClient.composeObject(composeObjectBuilder.build());
} catch (Exception e) {
log.error("合并文件发生异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("合并文件发生异常");
}
}
///////////////////////////////////////// 文件相关API end ///////////////////////////////////////////
///////////////////////////////////////// 分片上传相关API start /////////////////////////////////////
/*
通过以下 createMultipartUploadAsync,uploadPartAsync/getPreSignUploadUrl,completeMultipartUploadAsync 方法
上传并合并的分片速度会比composeObject方法快,而且以下方式的分片是隐藏的形式存储在服务器上(是minio认识的分片)
当存在废弃的分片的时候,minio会自己处理
*/
/**
* 创建分片上传任务
* @param parallelMinioClient minio 客户端
* @param bucketName 桶名称
* @param region 地区,一般不填使用默认的
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param headers 一般只需要设置“Content-Type”
* @param extraQueryParams 查询参数
* @return
*/
public CreateMultipartUploadResponse createMultipartUpload(ParallelMinioClient parallelMinioClient,String bucketName,
String region, String path,String filename,
Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
// 创建分片上传任务
try {
return parallelMinioClient.createMultipartUpload(bucketName, region, getObjectName(path, filename), headers, extraQueryParams);
} catch (Exception e) {
log.error("创建分片上传任务发生异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("创建分片上传任务异常");
}
}
/**
* 前端通过后端上传分片到minio 方式一:(前端 ---> 后端 ---> minio)
* @param parallelMinioClient minio客户端
* @param bucketName 桶名称
* @param region 地区,一般不填使用默认的
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param data 分片文件,只能接收RandomAccessFile、InputStream类型的,一般使用InputStream类型
* @param length 文件大小,从1开始
* @param uploadId 文件上传id
* @param partNumber 分片编号
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return 分片对象
*/
public UploadPartResponse uploadPart(ParallelMinioClient parallelMinioClient, String bucketName, String region,
String path, String filename, Object data, long length, String uploadId, int partNumber,
Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.uploadPart(bucketName, region, getObjectName(path,filename), data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
} catch (Exception e) {
log.error("创建分片上传任务发生异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("创建分片上传任务异常");
}
}
/**
* 获取分片上传地址,前端直接上传分片到minio 方式二:(前端 ---> minio)
* @param bucketName minio桶名称
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param queryParams 查询参数,一般只需要设置“uploadId”和“partNumber”
* @return 分片上传地址
**/
public String getPreSignUploadUrl(ParallelMinioClient parallelMinioClient, String bucketName, String path,String filename, Map<String, String> queryParams) {
try {
if (queryParams == null || queryParams.get("uploadId") == null || queryParams.get("partNumber") == null){
throw new ObjectStorageException("缺少必要参数,uploadId 和 partNumber");
}
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(getObjectName(path, filename))
.expiry(60 * 60 * 24, TimeUnit.SECONDS)
.extraQueryParams(queryParams)
.build();
return parallelMinioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
} catch (Exception e) {
log.error("获取分片上传地址发生异常, bucketName {},path {},filename {},queryParams {}", bucketName, path, filename,queryParams, e);
throw new ObjectStorageException("创建分片上传任务异常");
}
}
/**
* 获取已上传的所有分片列表,可以为前端同completeMultipartUpload方法一起使用
* @param bucketName minio桶名称
* @param region 一般填null就行
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param maxParts 最大分片数,设置分页时每一页中分片数量。默认列举1000个分片。
* @param partNumberMarker 直接填0即可,指定List的起始位置。只有分片号大于此参数值的分片会被列举。
* @param uploadId 文件上传uploadId
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return ListPartsResponse对象
**/
public ListPartsResponse listParts(ParallelMinioClient parallelMinioClient,String bucketName, String region,
String path,String filename, Integer maxParts, Integer partNumberMarker, String uploadId,
Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.listParts(bucketName, region, getObjectName(path,filename), maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
} catch (Exception e) {
log.error("获取已上传的所有分片列表发生异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("获取已上传的所有分片列表异常");
}
}
/**
* 合并分片
* @param parallelMinioClient minio客户度
* @param bucketName minio桶名称
* @param region 一般填null就行
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param uploadId 文件上传uploadId
* @param parts 分片信息
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return ObjectWriteResponse对象
**/
public ObjectWriteResponse completeMultipartUpload(ParallelMinioClient parallelMinioClient,String bucketName, String region, String path, String filename,
String uploadId, Part[] parts, Multimap<String, String> extraHeaders,
Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.completeMultipartUpload(bucketName, region, getObjectName(path,filename), uploadId, parts, extraHeaders, extraQueryParams);
} catch (Exception e) {
log.error("合并发生异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("合并发生异常");
}
}
/**
* 删除minio中已有分片 (终止上传/取消上传)
* @param bucketName MinIO桶名称
* @param region 一般填null就行
* @param path 文件在桶中的路径或目录(例:2024/03/08/)
* @param filename 文件名(包含后缀,例:test.jpg)
* @param uploadId 文件上传uploadId
* @param extraHeaders 一般填null就行
* @param extraQueryParams 一般填null就行
* @return AbortMultipartUploadResponse对象
**/
public AbortMultipartUploadResponse abortMultipartUpload(ParallelMinioClient parallelMinioClient,String bucketName, String region,
String path,String filename, String uploadId,
Multimap<String, String> extraHeaders,
Multimap<String, String> extraQueryParams) {
try {
return parallelMinioClient.abortMultipartUpload(bucketName, region, getObjectName(path, filename), uploadId, extraHeaders, extraQueryParams);
} catch (Exception e) {
log.error("取消分片上传异常, bucketName {},path {},filename {}", bucketName, path, filename, e);
throw new ObjectStorageException("取消分片上传发生异常");
}
}
//////////////////////////////////////// 分片上传相关API end ///////////////////////////////////////////
public static void main(String[] args) {
// https://blog.csdn.net/qq_42449963/article/details/128167444
try {
makeBucket(getMinioClient());
} catch (Exception e) {
e.printStackTrace();
}
}
public static MinioClient getMinioClient() {
MinioClient.Builder credentials = MinioClient.builder()
.endpoint("https://play.minio.io")
.credentials("minioadmin", "minioadmin");
return credentials.build();
}
public static void makeBucket(MinioClient minioClient) throws IOException, InvalidKeyException,
InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException,
ServerException, InternalException, XmlParserException, ErrorResponseException {
String bucketName = "aossci";
BucketExistsArgs build = BucketExistsArgs.builder().bucket(bucketName).build();
boolean exists = minioClient.bucketExists(build);
if (!exists) {
MakeBucketArgs.Builder aossci = MakeBucketArgs.builder().bucket(bucketName);
minioClient.makeBucket(aossci.build());
System.out.println("----------------------------------");
System.out.println("------------创建了桶 aossci---------------");
System.out.println("----------------------------------");
}
File file = FileUtil.file("C:\\Users\\CDLX\\Desktop\\BladeX开发手册-2.8.1.RELEASE\\images\\image-20210211121123276.png");
MediaType mediaType = MediaTypeFactory.getMediaType(file.getName()).orElse(MediaType.APPLICATION_OCTET_STREAM);
// 默认值
String contentType = mediaType.toString();
String format = DateUtil.format(new Date(), "yyyy/MM/dd");
BufferedInputStream inputStream = FileUtil.getInputStream(file);
String taskIdStr = IdUtil.simpleUUID();
ProgressInputStream progressInputStream = new ProgressInputStream(inputStream, taskIdStr, new UploadListener() {
@Override
public void onProgress(String taskId, int process) {
System.out.println("------------------------");
System.out.println("【" + taskId + "】 process = " + process);
System.out.println("------------------------");
}
});
try {
PutObjectArgs.Builder object = PutObjectArgs.builder()
.bucket(bucketName)
.stream(progressInputStream, file.length(), -1)
.contentType(contentType)
.object(format + "/" + file.getName());
ObjectWriteResponse objectWriteResponse = minioClient.putObject(object.build());
System.out.println(objectWriteResponse);
} finally {
progressInputStream.close();
}
}
}
MinioTemplate
二、前端计算md5
https://blog.51cto.com/u_15967457/6081726
根据业务需要,在上传文件前我们要读取文件的md5值,将md5值传给后端用作秒传和断点续传文件的唯一标识。那么前端就需要使用js获取文件的md5值,对于普通小文件可以很轻松的读取文件md5值,而超大文件的md5值是如何快速的获取到的呢?
超大文件如何计算md5值?
前面的文章我们了解了分片上传,解决了大文件和超大文件web上传的超时的问题。
这里我们说的超大文件一般值1G+的文件,对于超大文件,我们不应该一次性的读取文件,这样的话有可能浏览器直接爆了。我们借助分片上传的概念,一片一片的读取文件,即每次读取一个分片内容chunk,之后再进行下一个分片内容继续计算,也就是读一片算一片,这样文件读取完毕,md5的值也就计算好了,同时整个计算过程占有内存也比一次性读取文件要低低多。
使用spark-md5计算本地文件md5
spark-md5.js号称是最适合前端最快的算法,能快速计算文件的md5。
快速安装:
npm install --save spark-md5
在组件中使用spark-md5时先引入:
import SparkMD5 from 'spark-md5';
spark-md5提供了两个计算md5的方法。一种是用SparkMD5.hashBinary()
直接将整个文件的二进制码传入,直接返回文件的md5。这种方法对于小文件会比较有优势——简单而且速度超快。
另一种方法是利用js中File对象的slice()
方法(File.prototype.slice
)将文件分片后逐个传入spark.appendBinary()
方法来计算、最后通过spark.end()
方法输出md5。很显然,此方法就是我们前面讲到的分片计算md5。这种方法对于大文件和超大文件会非常有利,不容易出错,不占用大内存,并且能够提供计算的进度信息。
以上两种方法在 spark-md5.js项目主页都有实例代码,本文主要讲第二种方法,即对超大文件计算的md5值。
vue-simple-uploader中添加“计算md5”状态
在上传文件前,需要检查文件状态,计算文件md5值。在上传列表中,其实是暂停状态,而我们不希望用户看到是暂停状态,我们应该友好的告诉用户正在计算md5,或者正在预处理文件,准备上传的状态。
从前几篇文章中,我们已经了解vue-simple-uploader在上传时会返回几种状态,如:上传中…、暂停、上传成功等状态。但并没有对自定义状态提供很好的接口。人们想法设法将类似计算md5的状态显示在上传列表中,在github上也跟作者提过,但好像没有得到更好的解决,无奈我翻看了一下作者的源码,fork下来,稍微做了几处改动,得到以下效果:
并且对原列表样式和图标做了修改,如果你已经安装好了vue-simple-uploader,直接下载:https://github.com/lrfbeyond/vue-uploader/blob/master/dist/vue-uploader.js,替换你的项目下\node_modules\vue-simple-uploader\dist\vue-uploader.js,然后再重新npm run dev
即可。
我们在组件调用时可以定义状态,其中cmd5
表示的是正在计算md5。
statusTextMap: {
success: '上传成功',
error: '上传出错了',
uploading: '上传中...',
paused: '暂停',
waiting: '等待中...',
cmd5: '计算md5...'
},
fileStatusText: (status, response) => {
return this.statusTextMap[status];
},
计算文件md5
在选择文件准备上传时,触发onFileAdded()
,先暂停上传,把md5计算出来后再上传。
暂停上传需要在uploader组件中设置:autoStart="false"
即可。
methods: {
onFileAdded(file) {
// 计算MD5
this.computeMD5(file);
},
computeMD5(file) {
...
}
}
根据spark-md5.js官方的例子,我们设置分片计算,每个分片2MB,根据文件大小可以计算得出分片总数。
然后设置文件状态为计算md5,即file.cmd5 = true;
。
接着一片片的一次读取分片信息,最后由spark.end()
计算得出文件的md5值。
得到md5值后,我们要将此文件的md5值赋值给file.uniqueIdentifier = md5;
,目的是为了后续的秒传和断点续传作为文件的唯一标识传给后端。
最后取消计算md5状态,即file.cmd5 = false;
,并且立马开始上传文件:file.resume();
//计算MD5
computeMD5(file) {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunkSize = 2097152, //2MB
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
let time = new Date().getTime();
file.cmd5 = true; //文件状态为“计算md5...”
fileReader.onload = (e) => {
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
console.log(`第${currentChunk}分片解析完成, 开始第${currentChunk +1} / ${chunks}分片解析`);
loadNext();
} else {
console.log('finished loading');
let md5 = spark.end(); //得到md5
console.log(`MD5计算完成:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
spark.destroy(); //释放缓存
file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
file.cmd5 = false; //取消计算md5状态
file.resume(); //开始上传
}
};
fileReader.onerror = () => {
console.warn('oops, something went wrong.');
file.cancel();
};
let loadNext = () => {
let start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
};
loadNext();
},
选择文件后,先计算文件md5值:
我们看到1个约83MB的文件,计算md5用时1.3秒。
而继续测试发现,1个约2GB的大文件,用时约29秒,我的电脑上8G内存,这个计算文件md5的速度是相当给力的了。