在Web开发中,大文件的上传是必不可少的功能之一。本文将介绍如何使用SpringBoot整合minio实现一个简单的大文件上传网站。
项目下载
gitee:https://gitee.com/wusupweilgy/springboot-vue.git
前端:vue2、element-ui组件、axios
后端:springboot、minio、mybatis-plus、redis
基本分成3步完成上传,检查是否上传、获取分片上传的url列表和分片上传合并,所以这里我把文件上传分成了三张流程图,就是为了详细点,其实是一个流程。
检查是否上传
获取上传url
redis主要作用是缓存上传任务,因为如果是断点续传,需要查询上次上传的信息去查询已上传的分片信息,只要上传没有上传的分片即可。
mysql主要作用是存储已完成上传的文件信息。如果下次上传的是同一个文件,数据库中存在就不用上传,直接返回数据库中的文件信息。
1.spark-md5
主要用来计算文件MD5,安装命令:npm install spark-md5 --S
上传示例
选择文件
上传
清空
文件名
文件大小
上传进度
状态
{{ item.name }}
{{ item.size | transformByte }}
等待上传
校验MD5
正在上传
上传完成
正在上传
/**
* @param: fileName - 文件名称
* @param: 数据返回 1) 无后缀匹配 - false
* @param: 数据返回 2) 匹配图片 - image
* @param: 数据返回 3) 匹配 txt - txt
* @param: 数据返回 4) 匹配 excel - excel
* @param: 数据返回 5) 匹配 word - word
* @param: 数据返回 6) 匹配 pdf - pdf
* @param: 数据返回 7) 匹配 ppt - ppt
* @param: 数据返回 8) 匹配 视频 - video
* @param: 数据返回 9) 匹配 音频 - radio
* @param: 数据返回 10) 其他匹配项 - other
* @author: ljw
**/
export function fileSuffixTypeUtil(fileName){
// 后缀获取
var suffix = "";
// 获取类型结果
var result = "";
try {
var flieArr = fileName.split(".");
suffix = flieArr[flieArr.length - 1];
} catch (err) {
suffix = "";
}
// fileName无后缀返回 false
if (!suffix) {
result = false;
return result;
}
// 图片格式
var imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
// 进行图片匹配
result = imglist.some(function (item) {
return item == suffix;
});
if (result) {
result = "image";
return result;
}
// 匹配txt
var txtlist = ["txt"];
result = txtlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "txt";
return result;
}
// 匹配 excel
var excelist = ["xls", "xlsx"];
result = excelist.some(function (item) {
return item == suffix;
});
if (result) {
result = "excel";
return result;
}
// 匹配 word
var wordlist = ["doc", "docx"];
result = wordlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "word";
return result;
}
// 匹配 pdf
var pdflist = ["pdf"];
result = pdflist.some(function (item) {
return item == suffix;
});
if (result) {
result = "pdf";
return result;
}
// 匹配 ppt
var pptlist = ["ppt"];
result = pptlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "ppt";
return result;
}
// 匹配 视频
var videolist = ["mp4", "m2v", "mkv","ogg", "flv", "avi", "wmv", "rmvb"];
result = videolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "video";
return result;
}
// 匹配 音频
var radiolist = ["mp3", "wav", "wmv"];
result = radiolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "radio";
return result;
}
// 其他 文件类型
result = "other";
return result;
};
import request from '@/utils/request'
//上传信息
export function uploadScreenshot(data){
return request({
url:'upload/multipart/uploadScreenshot',
method:'post',
data
})
}
//上传信息
export function uploadFileInfo(data){
return request({
url:'upload/multipart/uploadFileInfo',
method:'post',
data
})
}
// 上传校验
export function checkUpload(MD5) {
return request({
url: `upload/multipart/check?md5=${MD5}`,
method: 'get',
})
};
// 初始化上传
export function initUpload(data) {
return request({
url: `upload/multipart/init`,
method: 'post',
data
})
};
// 初始化上传
export function mergeUpload(data) {
return request({
url: `upload/multipart/merge`,
method: 'post',
data
})
};
import axios from 'axios'
import router from "@/router";
import ElementUI from "element-ui";
const request = axios.create({
baseURL: `http://localhost:9090`,
timeout: 30000
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
return config
}, error => {
return Promise.reject(error)
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.headers === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
console.log(res)
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default request
/*
Navicat Premium Data Transfer
Source Server : local
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : shiro_jwt_vue_file
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 01/05/2023 13:52:06
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for files
-- ----------------------------
DROP TABLE IF EXISTS `files`;
CREATE TABLE `files` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
`upload_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '分片上传uploadId',
`file_md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件md5',
`url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '下载链接',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名称',
`bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '桶名',
`file_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型',
`file_size` bigint(0) NULL DEFAULT NULL COMMENT '文件大小(byte)',
`chunk_size` bigint(0) NULL DEFAULT NULL COMMENT '每个分片的大小(byte)',
`chunk_num` int(0) NULL DEFAULT NULL COMMENT '分片数量',
`is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
`enable` tinyint(1) NULL DEFAULT 1 COMMENT '是否禁用链接',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 262 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
com.alibaba
fastjson
1.2.50
com.baomidou
mybatis-plus-boot-starter
3.5.2
mysql
mysql-connector-java
8.0.30
io.minio
minio
8.3.1
com.squareup.okhttp3
okhttp
4.9.2
org.springframework.boot
spring-boot-starter-data-redis
cn.hutool
hutool-all
5.7.20
org.springframework.boot
spring-boot-starter-validation
org.apache.commons
commons-lang3
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
server:
port: 9090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro_jwt_vue_file?serverTimezone=Asia/Shanghai&userUnicode=true&useSSL=false
username: root
password: mysql
redis:
# Redis服务器地址
host: 127.0.0.1
# Redis服务器端口号
port: 6379
# 使用的数据库索引,默认是0
database: 0
# 连接超时时间
timeout: 1800000
# 设置密码
#password: "123456"
lettuce:
pool:
# 最大阻塞等待时间,负数表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 5
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中最大连接数,负数表示没有限制
max-active: 20
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
minio:
endpoint: http://localhost:9000
accesskey: minioadmin
secretkey: minioadmin
expiry: 1 #分片对象过期时间 单位(天)
breakpoint-time: 1 #断点续传有效时间,在redis存储任务的时间 单位(天)
package com.wusuowei.miniouploadfile.controller;
import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
import com.wusuowei.miniouploadfile.service.UploadService;
import com.wusuowei.miniouploadfile.utils.MinioUtils;
import com.wusuowei.miniouploadfile.utils.R;
import com.wusuowei.miniouploadfile.utils.RedisUtil;
import com.wusuowei.miniouploadfile.utils.RespEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* minio上传流程
*
* 1.检查数据库中是否存在上传文件
*
* 2.根据文件信息初始化,获取分片预签名url地址,前端根据url地址上传文件
*
* 3.上传完成后,将分片上传的文件进行合并
*
* 4.保存文件信息到数据库
*/
@RestController
@Slf4j
@RequestMapping("upload")
public class FileMinioController {
@Resource
private UploadService uploadService;
@Resource
private RedisUtil redisUtil;
@Resource
private MinioUtils minioUtils;
/**
* @description 获取上传文件
* @param fileMD5 文件md5
* @return {@link R }
* @author LGY
* @date 2023/04/26 16:00
*/
@GetMapping("/getUploadingFile/{fileMD5}")
public R getUploadingFile(@PathVariable String fileMD5) {
if (StringUtils.isBlank(fileMD5)) {
return R.error();
}
FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(fileMD5);
if (fileUploadInfo != null) {
// 查询上传后的分片数据
fileUploadInfo.setChunkUploadedList(minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType()));
return R.ok().setData(fileUploadInfo);
}
return R.error();
}
/**
* 校验文件是否存在
*
* @param md5 String
* @return ResponseResult
package com.wusuowei.miniouploadfile.service;
import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
import com.wusuowei.miniouploadfile.utils.R;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface UploadService {
/**
* 分片上传初始化
*
* @param fileUploadInfo
* @return Map
*/
Map initMultiPartUpload(FileUploadInfo fileUploadInfo);
/**
* 完成分片上传
*
* @param fileUploadInfo
* @return String
*/
String mergeMultipartUpload(FileUploadInfo fileUploadInfo);
/**
* 通过 md5 获取已上传的数据
* @param md5 String
* @return Mono
package com.wusuowei.miniouploadfile.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wusuowei.miniouploadfile.mapper.FilesMapper;
import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
import com.wusuowei.miniouploadfile.model.po.Files;
import com.wusuowei.miniouploadfile.service.UploadService;
import com.wusuowei.miniouploadfile.utils.MinioUtils;
import com.wusuowei.miniouploadfile.utils.R;
import com.wusuowei.miniouploadfile.utils.RedisUtil;
import com.wusuowei.miniouploadfile.utils.RespEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class UploadServiceImpl implements UploadService {
@Resource
private FilesMapper filesMapper;
@Resource
private MinioUtils minioUtils;
@Resource
private RedisUtil redisUtil;
@Value("${minio.breakpoint-time}")
private Integer breakpointTime;
/**
* 通过 md5 获取已上传的数据(断点续传)
*
* @param md5 String
* @return Mono
package com.wusuowei.miniouploadfile.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wusuowei.miniouploadfile.model.po.Files;
/**
*
* Mapper 接口
*
*
* @author LGY
*/
public interface FilesMapper extends BaseMapper {
}
package com.wusuowei.miniouploadfile.model.po;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
*
*
*
*
* @author LGY
*/
@Data
@TableName("files")
public class Files implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField("upload_id")
private String uploadId;
@TableField("file_md5")
private String fileMd5;
@TableField("url")
private String url;
@TableField("file_name")
private String fileName;
@TableField("bucket_name")
private String bucketName;
@TableField("file_type")
private String fileType;
@TableField("file_size")
private Long fileSize;
@TableField("chunk_size")
private Long chunkSize;
@TableField("chunk_num")
private Integer chunkNum;
@TableField("is_delete")
@TableLogic(value = "0",delval = "1")
private Boolean isDelete;
@TableField("enable")
private Boolean enable;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
package com.wusuowei.miniouploadfile.model.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@Accessors(chain = true)
public class FileUploadInfo {
@NotBlank(message = "文件名不能为空")
private String fileName;
@NotNull(message = "文件大小不能为空")
private Long fileSize;
@NotBlank(message = "Content-Type不能为空")
private String contentType;
@NotNull(message = "分片数量不能为空")
private Integer chunkNum;
@NotBlank(message = "uploadId 不能为空")
private String uploadId;
private Long chunkSize;
// 桶名称
//private String bucketName;
//md5
private String fileMd5;
//文件类型
private String fileType;
//已上传的分片索引+1
private List chunkUploadedList;
}
package com.wusuowei.miniouploadfile.utils;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.HashMultimap;
import com.wusuowei.miniouploadfile.config.CustomMinioClient;
import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Component
public class MinioUtils {
@Value(value = "${minio.endpoint}")
private String endpoint;
@Value(value = "${minio.accesskey}")
private String accesskey;
@Value(value = "${minio.secretkey}")
private String secretkey;
@Value(value = "${minio.expiry}")
private Integer expiry;
private CustomMinioClient customMinioClient;
/**
* 用spring的自动注入会注入失败
*/
@PostConstruct
public void init() {
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accesskey, secretkey)
.build();
customMinioClient = new CustomMinioClient(minioClient);
}
/**
* 单文件签名上传
*
* @param objectName 文件全路径名称
* @param bucketName 桶名称
* @return /
*/
public Map getUploadObjectUrl(String objectName, String bucketName) {
try {
log.info("tip message: 通过 <{}-{}> 开始单文件上传", objectName, bucketName);
Map resMap = new HashMap();
List partList = new ArrayList<>();
String url = customMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(expiry, TimeUnit.DAYS)
.build());
log.info("tip message: 单个文件上传、成功");
partList.add(url);
resMap.put("uploadId", "SingleFileUpload");
resMap.put("urlList", partList);
return resMap;
} catch (Exception e) {
log.error("error message: 单个文件上传失败、原因:", e);
// 返回 文件上传失败
return null;
}
}
/**
* 初始化分片上传
*
* @param fileUploadInfo
* @param objectName 文件全路径名称
* @param chunkNum 分片数量
* @param contentType 类型,如果类型使用默认流会导致无法预览
* @param bucketName 桶名称
* @return Mono>
*/
public Map initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int chunkNum, String contentType, String bucketName) {
log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, chunkNum, contentType, bucketName);
Map resMap = new HashMap<>();
try {
if (CharSequenceUtil.isBlank(contentType)) {
contentType = "application/octet-stream";
}
HashMultimap headers = HashMultimap.create();
headers.put("Content-Type", contentType);
//获取uploadId
String uploadId = null;
if(StringUtils.isBlank(fileUploadInfo.getUploadId())){
uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
}else{
uploadId = fileUploadInfo.getUploadId();
}
resMap.put("uploadId", uploadId);
fileUploadInfo.setUploadId(uploadId);
fileUploadInfo.setChunkNum(chunkNum);
List partList = new ArrayList<>();
Map reqParams = new HashMap<>();
reqParams.put("uploadId", uploadId);
for (int i = 1; i <= chunkNum; i++) {
reqParams.put("partNumber", String.valueOf(i));
String uploadUrl = customMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(reqParams)
.build());
partList.add(uploadUrl);
}
log.info("tip message: 文件初始化<分片上传>、成功");
resMap.put("urlList", partList);
return resMap;
} catch (Exception e) {
log.error("error message: 初始化分片上传失败、原因:", e);
// 返回 文件上传失败
return R.error(RespEnum.UPLOAD_FILE_FAILED);
}
}
/**
* 分片上传完后合并
*
* @param objectName 文件全路径名称
* @param uploadId 返回的uploadId
* @param bucketName 桶名称
* @return boolean
*/
public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
try {
log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
//目前仅做了最大1000分片
Part[] parts = new Part[1000];
// 查询上传后的分片数据
ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
int partNumber = 1;
for (Part part : partResult.result().partList()) {
parts[partNumber - 1] = new Part(partNumber, part.etag());
partNumber++;
}
// 合并分片
customMinioClient.mergeMultipartUpload(bucketName, null,objectName, uploadId, parts, null, null);
} catch (Exception e) {
log.error("error message: 合并失败、原因:", e);
//TODO删除redis的数据
return false;
}
return true;
}
/**
* 通过 sha256 获取上传中的分片信息
*
* @param objectName 文件全路径名称
* @param uploadId 返回的uploadId
* @param bucketName 桶名称
* @return Mono>
*/
public List getChunkByFileMD5(String objectName, String uploadId, String bucketName) {
log.info("通过 <{}-{}-{}> 查询上传分片数据", objectName, uploadId, bucketName);
try {
// 查询上传后的分片数据
ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
} catch (Exception e) {
log.error("error message: 查询上传后的分片信息失败、原因:", e);
return null;
}
}
/**
* 获取文件下载地址
*
* @param bucketName 桶名称
* @param fileName 文件名
* @return
*/
public String getFliePath(String bucketName, String fileName) {
return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径
}
/**
* 创建一个桶
*
* @return
*/
public String createBucket(String bucketName) {
try {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
//如果桶存在
if (customMinioClient.bucketExists(bucketExistsArgs)) {
return bucketName;
}
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
customMinioClient.makeBucket(makeBucketArgs);
return bucketName;
} catch (Exception e) {
log.error("创建桶失败:{}", e.getMessage());
throw new RuntimeException(e);
}
}
/**
* 根据文件类型获取minio桶名称
*
* @param fileType
* @return
*/
public String getBucketName(String fileType) {
try {
//String bucketName = getProperty(fileType.toLowerCase());
if (fileType != null && !fileType.equals("")) {
//判断桶是否存在
String bucketName2 = createBucket(fileType.toLowerCase());
if (bucketName2 != null && !bucketName2.equals("")) {
return bucketName2;
}else{
return fileType;
}
}
} catch (Exception e) {
log.error("Error reading bucket name ");
}
return fileType;
}
/**
* 读取配置文件
*
* @param fileType
* @return
* @throws IOException
*/
private String getProperty(String fileType) throws IOException {
Properties SysLocalPropObject = new Properties();
//判断桶关系配置文件是否为空
if (SysLocalPropObject.isEmpty()) {
InputStream is = getClass().getResourceAsStream("/BucketRelation.properties");
SysLocalPropObject.load(is);
is.close();
}
return SysLocalPropObject.getProperty("bucket." + fileType);
}
/**
* 文件上传
*
* @param file 文件
* @return Boolean
*/
public String upload(MultipartFile file, String bucketName) {
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename)) {
throw new RuntimeException();
}
String objectName = file.getName();
try {
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName)
.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
//文件名称相同会覆盖
customMinioClient.putObject(objectArgs);
} catch (Exception e) {
e.printStackTrace();
return null;
}
// 查看文件地址
GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(bucketName).object(objectName).method(Method.GET).build();
String url = null;
try {
url = customMinioClient.getPresignedObjectUrl(build);
} catch (ErrorResponseException e) {
e.printStackTrace();
} catch (InsufficientDataException e) {
e.printStackTrace();
} catch (InternalException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidResponseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (XmlParserException e) {
e.printStackTrace();
} catch (ServerException e) {
e.printStackTrace();
}
return url;
}
// /**
// * 写入配置文件
// */
// public void setProperty(String bucketName) {
// String tempPath = Objects.requireNonNull(getClass().getResource("/BucketRelation.properties")).getPath();
// OutputStream os;
// try {
// os = new FileOutputStream(tempPath);
// SysLocalPropObject.setProperty(bucketName, bucketName);
// SysLocalPropObject.store(os, "Update " + bucketName + " " + bucketName);
// os.close();
// } catch (IOException e) {
// }
// }
}
// @Autowired
// private MinioProp minioProp;
//
//
// @Autowired
// private MinioClient minioClient;
//
//
//
// /**
// * 列出所有的桶
// */
// public List listBuckets() throws Exception {
// List list = minioClient.listBuckets();
// List names = new ArrayList<>();
// list.forEach(b -> {
// names.add(b.name());
// });
// return names;
// }
//
// /**
// * 列出一个桶中的所有文件和目录
// */
// public List listFiles(String bucket) throws Exception {
// Iterable> results = minioClient.listObjects(
// ListObjectsArgs.builder().bucket(bucket).recursive(true).build());
//
// List infos = new ArrayList<>();
// results.forEach(r->{
// Fileinfo info = new Fileinfo();
// try {
// Item item = r.get();
// info.setFilename(item.objectName());
// info.setDirectory(item.isDir());
// infos.add(info);
// } catch (Exception e) {
// e.printStackTrace();
// }
// });
// return infos;
// }
//
// /**
// * 下载一个文件
// */
// public InputStream download(String bucket, String objectName) throws Exception {
// InputStream stream = minioClient.getObject(
// GetObjectArgs.builder().bucket(bucket).object(objectName).build());
// return stream;
// }
//
// /**
// * 删除一个桶
// */
// public void deleteBucket(String bucket) throws Exception {
// minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
// }
//
// /**
// * 删除一个对象
// */
// public void deleteObject(String bucket, String objectName) throws Exception {
// minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
// }
//
//
// /**
// * 创建一个桶
// */
// public void createBucket(String bucketName) {
// BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
// MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
// try {
// if (minioClient.bucketExists(bucketExistsArgs))
// return;
// minioClient.makeBucket(makeBucketArgs);
// } catch (Exception e) {
// log.error("创建桶失败:{}", e.getMessage());
// throw new RuntimeException(e);
// }
// }
//
// /**
// * 上传一个文件
// * @param file 文件
// * @param bucketName 存储桶
// * @return
// */
// public JSONObject uploadFile(MultipartFile file, String bucketName) throws Exception {
// JSONObject res = new JSONObject();
// res.put("code", 0);
// // 判断上传文件是否为空
// if (null == file || 0 == file.getSize()) {
// res.put("msg", "上传文件不能为空");
// return res;
// }
// // 判断存储桶是否存在
// createBucket(bucketName);
// // 文件名
// String originalFilename = file.getOriginalFilename();
// // 新的文件名 = 存储桶名称_时间戳.后缀名
// String fileName = bucketName + "_" + System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
// // 开始上传
// InputStream inputStream = file.getInputStream();
// PutObjectArgs args = PutObjectArgs.builder().bucket(bucketName).object(fileName)
// .stream(inputStream,inputStream.available(),-1).build();
// minioClient.putObject(args);
// res.put("code", 1);
// res.put("msg", minioProp.getEndpoint() + "/" + bucketName + "/" + fileName);
// return res;
// }
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long hGetMapSize(String key) {
try {
return redisTemplate.opsForHash().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
import com.google.common.collect.Multimap;
import io.minio.CreateMultipartUploadResponse;
import io.minio.ListPartsResponse;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.errors.*;
import io.minio.messages.Part;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class CustomMinioClient extends MinioClient {
/**
* 继承父类
* @param client
*/
public CustomMinioClient(MinioClient client) {
super(client);
}
/**
* 初始化分片上传、获取 uploadId
*
* @param bucket String 存储桶名称
* @param region String
* @param object String 文件名称
* @param headers Multimap 请求头
* @param extraQueryParams Multimap
* @return String
*/
public String initMultiPartUpload(String bucket, String region, String object, Multimap headers, Multimap extraQueryParams) 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 String 桶名称
* @param region String
* @param objectName String 文件名称
* @param uploadId String 上传的 uploadId
* @param parts Part[] 分片集合
* @param extraHeaders Multimap
* @param extraQueryParams Multimap
* @return ObjectWriteResponse
*/
public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap extraHeaders, Multimap extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException {
return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
/**
* 查询当前上传后的分片信息
*
* @param bucketName String 桶名称
* @param region String
* @param objectName String 文件名称
* @param maxParts Integer 分片数量
* @param partNumberMarker Integer 分片起始值
* @param uploadId String 上传的 uploadId
* @param extraHeaders Multimap
* @param extraQueryParams Multimap
* @return ListPartsResponse
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap extraHeaders, Multimap extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
}
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @description Web应用程序配置
* @author LGY
* @date 2023/03/14 20:09
* @version 1.0.0
*/
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowedHeaders("*");
}
//实体类属性为空时不进行序列化返回给前端
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter converter = fastConverter;
return new HttpMessageConverters(converter);
}
}
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MybatisPlus配置类
*
*/
@Slf4j
@Configuration
@MapperScan("com.wusuowei.miniouploadfile.mapper")
public class MybatisPlusConfig implements MetaObjectHandler {
/**
* 新的分页插件
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
/* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
/* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
}
package com.wusuowei.miniouploadfile.utils;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark [email protected]
*/
public class R extends HashMap {
private static final long serialVersionUID = 1L;
public R setData(Object data) {
put("data",data);
return this;
}
//利用fastjson进行反序列化
public T getData(Class typeReference) {
Object data = get("data"); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public R() {
put("code", 200);
put("msg", "success");
}
public static R error() {
return error(5000, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(RespEnum respEnum) {
return error(respEnum.getCode(), respEnum.getMessage());
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(RespEnum respEnum) {
return ok(respEnum.getCode(), respEnum.getMessage());
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(Map map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
package com.wusuowei.miniouploadfile.utils;
public enum RespEnum {
UPLOADSUCCESSFUL(1, "上传成功"),
UPLOADING(2, "上传中"),
NOT_UPLOADED(3, "未上传"),
ACCESS_PARAMETER_INVALID(1001,"访问参数无效"),
UPLOAD_FILE_FAILED(1002,"文件上传失败"),
DATA_NOT_EXISTS(1003,"数据不存在"),
;
private final Integer code;
private final String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
RespEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
文章贴的代码有点多了,大家可以去我的gitee上下载下来运行。项目的功能我测试优化了很多次,如果代码有何异常或者不完整欢迎在评论区留言。如果这篇文章有幸帮助到你,希望读者大大们可以给作者点个赞呀️️️