minio依赖
<!-- Minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
前端代码demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件</title>
</head>
<body>
<div>
<input type="file" multiple onchange="handleFileChange(event)" />
<button onclick="uploadFiles()">Upload</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 定义全局变量和函数
const files = [];
const chunkSize = 5 * 1024 * 1024; // 5MB
const uploadIdMap = {};
function handleFileChange(event) {
files.length = 0; // 清空数组
Array.from(event.target.files).forEach(file => files.push(file));
}
async function uploadFiles() {
if (files.length === 0) {
alert('Please select files to upload.');
return;
}
const promises = files.map(file => uploadFile(file));
try {
await Promise.all(promises);
alert('All files uploaded successfully.');
} catch (error) {
console.error('Error uploading files:', JSON.stringify(error, null, 2));
alert('Failed to upload files.');
}
}
async function uploadFile(file) {
const totalChunks = Math.ceil(file.size / chunkSize);
const fileName = file.name;
let uploadId = null;
try {
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const formData = new FormData();
formData.append('file', chunk);
formData.append('shardCount', totalChunks);
formData.append('shardIndex', i + 1);
formData.append('fileName', fileName);
if (!uploadId) {
formData.append('initialize', true);
} else {
formData.append('uploadId', uploadId);
}
const response = await axios.post('http://127.0.0.1:9204/shardingUpload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (!uploadId) {
uploadId = response.data.uploadId;
uploadIdMap[fileName] = uploadId;
}
}
await axios.post('http://127.0.0.1:9204/shardingComplete', {
uploadId,
fileName,
});
} catch (error) {
console.error(`Error uploading file ${fileName}:`, error);
throw error;
}
}
</script>
</body>
</html>
后端代码:
后端controller
package com.xxxx.file.controller;
import com.construction.common.core.domain.R;
import com.construction.file.entity.ShardingCompleteRequest;
import com.construction.file.entity.ShardingUploadResponse;
import com.construction.file.service.ISysFileService;
import com.construction.file.utils.FileUploadUtils;
import com.construction.system.api.domain.SysFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.text.DecimalFormat;
/**
* 文件请求处理
*
* @author construction
*/
@RestController
public class SysFileController {
private static final Logger log =LoggerFactory.getLogger(SysFileController.class);
@Autowired
@Qualifier(value ="minioService")
private ISysFileService sysFileService;
/**
* 分片上传
*
* @param file 文件
* @param fileName 文件名称
* @param shardIndex 当前切片序号,从1开始
* @param shardCount 切片总数量
*/
@PostMapping("shardingUpload")
public RshardingUpload(@RequestParam("file")MultipartFile file,
@RequestParam("shardIndex")int shardIndex,
@RequestParam("shardCount")int shardCount,
@RequestParam("fileName")String fileName) {
try {
//重命名文件
fileName =FileUploadUtils.extractFilenameForShardingUpload(fileName);
// 上传并返回访问地址
ShardingUploadResponse result =sysFileService.shardingUpload(file,shardIndex,shardCount,fileName);
SysFile sysFile =new SysFile();
sysFile.setFileSize(this.getSize(file.getSize()));
sysFile.setSize(file.getSize());
// sysFile.setName(FileUtils.getName(url));
//返回原始文件名
sysFile.setName(file.getOriginalFilename());
// sysFile.setUrl(url);
return R.ok(sysFile);
}catch (Exception e) {
log.error("上传文件失败",e);
return R.fail(e.getMessage());
}
}
/**
* 完成分片上传
*
* @param shardingCompleteRequest
*/
@PostMapping("shardingComplete")
public RshardingComplete(@RequestBody ShardingCompleteRequest shardingCompleteRequest) {
try {
//重命名文件
String fileName =FileUploadUtils.extractFilenameForShardingUpload(shardingCompleteRequest.getFileName());
SysFile sysFile =sysFileService.shardingComplete(fileName,shardingCompleteRequest.getUploadId());
sysFile.setFileSize(this.getSize(sysFile.getSize()));
sysFile.setName(fileName);
return R.ok(sysFile);
}catch (Exception e) {
log.error("上传文件失败",e);
return R.fail(e.getMessage());
}
}
public String getSize(long size) {
//获取到的size为:1705230
int GB =1024 *1024 *1024;//定义GB的计算常量
int MB =1024 *1024;//定义MB的计算常量
int KB =1024;//定义KB的计算常量
DecimalFormat df =new DecimalFormat("0.00");//格式化小数
String resultSize ="";
if (size /GB >=1) {
//如果当前Byte的值大于等于1GB
resultSize =df.format(size / (float)GB) +"GB";
}else if (size /MB >=1) {
//如果当前Byte的值大于等于1MB
resultSize =df.format(size / (float)MB) +"MB";
}else if (size /KB >=1) {
//如果当前Byte的值大于等于1KB
resultSize =df.format(size / (float)KB) +"KB";
}else {
resultSize =size +"B";
}
return resultSize;
}
}
后端代码:minioconfig
package com.construction.file.config;
import com.construction.file.client.CustomMinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;
/**
* Minio 配置信息
*
* @author construction
*/
@Configuration
@ConfigurationProperties(prefix ="minio")
public class MinioConfig
{
/**
* 内部调用服务地址
*/
private String url;
/**
* 对外开放服务地址
*/
private String httpsUrl;
/**
* 用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
public String getUrl()
{
return url;
}
public void setUrl(String url)
{
this.url =url;
}
public String getAccessKey()
{
return accessKey;
}
public void setAccessKey(String accessKey)
{
this.accessKey =accessKey;
}
public String getSecretKey()
{
return secretKey;
}
public void setSecretKey(String secretKey)
{
this.secretKey =secretKey;
}
public String getBucketName()
{
return bucketName;
}
public void setBucketName(String bucketName)
{
this.bucketName =bucketName;
}
public String getHttpsUrl() {
return httpsUrl;
}
public void setHttpsUrl(String httpsUrl) {
this.httpsUrl =httpsUrl;
}
@Bean(name ="minioClient")
public MinioClient getMinioClient()
{
return MinioClient.builder().endpoint(url).credentials(accessKey,secretKey).build();
}
/**
* 自定义分片上传的minio客户端
*
* @return
*/
@Bean(name ="customMinioClient")
public CustomMinioClient getCustomMinioClient() {
MinioClient build =MinioClient.builder().endpoint(url).credentials(accessKey,secretKey).build();
return new CustomMinioClient(build);
}
}
后端代码:自定义minioclient
package com.construction.file.client;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Part;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @ProjectName: construction
* @Package: com.construction.file.client
* @ClassName: CustomMinioClient
* @Author: luanzhiwei
* @Description: 自定义minioclient
* @Date: 2024/7/16 10:24
* @Version: 1.0
*/
public class CustomMinioClient extends MinioClient {
public CustomMinioClient(MinioClient client) {
super(client);
}
/**
* 初始化分片上传
*
* @param bucket
* @param region
* @param object
* @param headers
* @param extraQueryParams
* @return
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws InsufficientDataException
* @throws ServerException
* @throws InternalException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws ErrorResponseException
*/
public String initMultiPartUpload(String bucket,String region,String object,Multimapheaders,MultimapextraQueryParams)throws IOException,InvalidKeyException,NoSuchAlgorithmException,InsufficientDataException,ServerException,InternalException,XmlParserException,InvalidResponseException,ErrorResponseException {
CreateMultipartUploadResponse response =this.createMultipartUpload(bucket,region,object,headers,extraQueryParams);
return response.result().uploadId();
}
/**
* 合并分片文件
*
* @param bucketName
* @param region
* @param objectName
* @param uploadId
* @param parts
* @param extraHeaders
* @param extraQueryParams
* @return
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws InsufficientDataException
* @throws ServerException
* @throws InternalException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws ErrorResponseException
*/
public ObjectWriteResponse mergeMultipartUpload(String bucketName,String region,String objectName,String uploadId,Part[]parts,MultimapextraHeaders,MultimapextraQueryParams)throws IOException,InvalidKeyException,NoSuchAlgorithmException,InsufficientDataException,ServerException,InternalException,XmlParserException,InvalidResponseException,ErrorResponseException {
return this.completeMultipartUpload(bucketName,region,objectName,uploadId,parts,extraHeaders,extraQueryParams);
}
/**
* 列出详情
*
* @param bucketName
* @param region
* @param objectName
* @param maxParts
* @param partNumberMarker
* @param uploadId
* @param extraHeaders
* @param extraQueryParams
* @return
* @throws NoSuchAlgorithmException
* @throws InsufficientDataException
* @throws IOException
* @throws InvalidKeyException
* @throws ServerException
* @throws XmlParserException
* @throws ErrorResponseException
* @throws InternalException
* @throws InvalidResponseException
*/
public ListPartsResponse listMultipart(String bucketName,String region,String objectName,Integer maxParts,Integer partNumberMarker,String uploadId,MultimapextraHeaders,MultimapextraQueryParams)throws NoSuchAlgorithmException,InsufficientDataException,IOException,InvalidKeyException,ServerException,XmlParserException,ErrorResponseException,InternalException,InvalidResponseException {
return this.listParts(bucketName,region,objectName,maxParts,partNumberMarker,uploadId,extraHeaders,extraQueryParams);
}
/**
* 切片上传
*
* @param bucketName
* @param region
* @param objectName
* @param data
* @param length
* @param uploadId
* @param partNumber
* @param extraHeaders
* @param extraQueryParams
* @return
* @throws NoSuchAlgorithmException
* @throws InsufficientDataException
* @throws IOException
* @throws InvalidKeyException
* @throws ServerException
* @throws XmlParserException
* @throws ErrorResponseException
* @throws InternalException
* @throws InvalidResponseException
*/
public UploadPartResponse uploadParts(String bucketName,String region,String objectName,Object data,int length,String uploadId,int partNumber,MultimapextraHeaders,MultimapextraQueryParams)throws NoSuchAlgorithmException,InsufficientDataException,IOException,InvalidKeyException,ServerException,XmlParserException,ErrorResponseException,InternalException,InvalidResponseException {
if (data instanceof InputStream) {
InputStream inputStream = (InputStream)data;
if (!(inputStream instanceof BufferedInputStream)) {
inputStream =new BufferedInputStream(inputStream);
}
data =inputStream;
}
return this.uploadPart(bucketName,region,objectName,data,length,uploadId,partNumber,extraHeaders,extraQueryParams);
}
}
后端代码:service
package com.construction.file.service;
import com.construction.file.entity.ShardingUploadResponse;
import com.construction.system.api.domain.SysFile;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传接口
*
* @author construction
*/
public interface ISysFileService {
/**
* 文件上传接口
*
* @param file 上传的文件
* @return 访问地址
* @throws Exception
*/
public String uploadFile(MultipartFile file)throws Exception;
/**
* 分片文件上传接口
*
* @param file 上传的文件
* @param shardIndex
* @param shardCount
* @param fileName
* @return 访问地址
* @throws Exception
*/
public ShardingUploadResponse shardingUpload(MultipartFile file,int shardIndex,int shardCount,String fileName)throws Exception;
/**
* 完成分片上传
*
* @param fileName
* @param uploadId
* @return
*/
SysFile shardingComplete(String fileName,String uploadId);
}
后端代码:serviceImpl
package com.construction.file.service;
import com.alibaba.nacos.common.utils.IoUtils;
import com.construction.common.core.exception.base.BaseException;
import com.construction.file.client.CustomMinioClient;
import com.construction.file.config.MinioConfig;
import com.construction.file.entity.ShardingUploadResponse;
import com.construction.file.utils.FileUploadUtils;
import com.construction.system.api.domain.SysFile;
import io.minio.*;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* Minio 文件存储
*
* @author construction
*/
@Service(value ="minioService")
@Slf4j
public class MinioSysFileServiceImpl implements ISysFileService {
@Autowired
private MinioConfig minioConfig;
@Autowired
@Qualifier("minioClient")
private MinioClient client;
@Resource
@Qualifier("customMinioClient")
private CustomMinioClient customMinioClient;
/**
* 分片上传的part
*/
private ConcurrentHashMap>uploadPartsMap =new ConcurrentHashMap<>();
/**
* 文件上传保存的文件名和uploadId
*/
private ConcurrentHashMapuploadIds =new ConcurrentHashMap<>();
private final ReentrantLock lock =new ReentrantLock();
/**
* Minio文件上传接口
*
* @param file 上传的文件
* @return 访问地址
* @throws Exception
*/
@Override
public String uploadFile(MultipartFile file)throws Exception {
String fileName =FileUploadUtils.extractFilename(file);
InputStream inputStream =file.getInputStream();
PutObjectArgs args =PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.stream(inputStream,file.getSize(), -1)
.contentType(file.getContentType())
.build();
client.putObject(args);
IoUtils.closeQuietly(inputStream);
return minioConfig.getHttpsUrl() +"/" +minioConfig.getBucketName() +"/" +fileName;
}
/**
* 分片文件上传接口
*
* @param file 上传的文件
* @param shardIndex 切片序号
* @param shardCount 切片总数
* @param fileName
* @return 访问地址
* @throws Exception
*/
@Override
public ShardingUploadResponse shardingUpload(MultipartFile file,int shardIndex,int shardCount,String fileName)throws Exception {
log.info("分片上传,文件名称:{},总片数 {} ,当前片数:{}",fileName,shardCount,shardIndex);
ShardingUploadResponse uploadResponse =new ShardingUploadResponse();
String uploadId;
//加锁
this.lock.lock();
try {
if (!uploadIds.containsKey(fileName)) {
uploadId =customMinioClient.initMultiPartUpload(minioConfig.getBucketName(),null,fileName,null,null);
uploadIds.put(fileName,uploadId);
uploadPartsMap.put(fileName,new ArrayList<>());
}else {
uploadId =uploadIds.get(fileName);
}
}finally {
this.lock.unlock();
}
InputStream inputStream =file.getInputStream();
UploadPartResponse response =customMinioClient.uploadParts(minioConfig.getBucketName(),null,fileName,inputStream,inputStream.available(),uploadId,shardIndex,null,null);
uploadPartsMap.get(fileName).add(new Part(shardIndex,response.etag()));
inputStream.close();
uploadResponse.setUploadId(uploadId);
log.info("uploadId: {}, 分片上传结束,文件名称:{},总片数 {} ,当前片数:{}",uploadId,fileName,shardCount,shardIndex);
return uploadResponse;
}
/**
* 分片上传完后合并
*
* @param fileName 文件全路径名称
* @param uploadId
* @return /
*/
public SysFile shardingComplete(String fileName,String uploadId) {
log.info("分片上传完成,文件名称:{},uploadId {} ",fileName,uploadId);
SysFile sysFile =new SysFile();
try {
uploadId =uploadIds.get(fileName);
Listparts =uploadPartsMap.get(fileName);
// 假设 parts 是你已经获得的 Part 数组
Part[]partsArray =parts.toArray(new Part[0]);
Arrays.sort(partsArray,Comparator.comparingInt(Part::partNumber));
customMinioClient.mergeMultipartUpload(minioConfig.getBucketName(),null,fileName,uploadId,partsArray,null,null);
// 清理上传状态
uploadIds.remove(fileName);
uploadPartsMap.remove(fileName);
}catch (Exception e) {
log.error("minio分片上传合并失败",e);
throw new BaseException("合并分片失败!");
}
try {
StatObjectResponse statted =customMinioClient.statObject(
StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.build()
);
sysFile.setSize(statted.size());
}catch (Exception e) {
throw new RuntimeException(e);
}
sysFile.setName(fileName);
String url =minioConfig.getHttpsUrl() +"/" +minioConfig.getBucketName() +"/" +fileName;
sysFile.setUrl(url);
log.info("分片上传完成,文件名称:{},uploadId {} ,访问地址 {} ",fileName,uploadId,url);
return sysFile;
}
}
网友评论