前言
在Web开发中,大文件的上传是必不可少的功能之一。本文将介绍如何使用SpringBoot整合minio实现一个简单的大文件上传网站。
一、项目介绍
项目下载
gitee:https://gitee.com/wusupweilgy/springboot-vue.git
1.项目运行
2.技术栈
前端:vue2、element-ui组件、axios
后端:springboot、minio、mybatis-plus、redis
3.功能
- 断点续传
- 分片上传
- 前端显示上传进度条
4.流程图
基本分成3步完成上传,检查是否上传、获取分片上传的url列表和分片上传合并,所以这里我把文件上传分成了三张流程图,就是为了详细点,其实是一个流程。
检查是否上传
获取上传url
分片上传合并
redis主要作用是缓存上传任务,因为如果是断点续传,需要查询上次上传的信息去查询已上传的分片信息,只要上传没有上传的分片即可。
mysql主要作用是存储已完成上传的文件信息。如果下次上传的是同一个文件,数据库中存在就不用上传,直接返回数据库中的文件信息。
二、前端实现
1.spark-md5
主要用来计算文件MD5,安装命令:
npm install spark-md5 --S
2.FileView.vue
上传示例
选择文件
上传
清空
文件名
文件大小
上传进度
状态
{{ item.name }}
{{ item.size | transformByte }}
等待上传
校验MD5
正在上传
上传完成
正在上传
import {checkUpload, initUpload, mergeUpload, uploadFileInfo} from "@/api/upload";
import {fileSuffixTypeUtil} from "@/utils/FileUtil"
import axios from 'axios'
import SparkMD5 from 'spark-md5'
const FILE_UPLOAD_ID_KEY = 'file_upload_id'
const chunkSize = 10 * 1024 * 1024 //10mb
let currentFileIndex = 0
const FileStatus = {
wait: '等待上传',
getMd5: '校验MD5',
chip: '正在创建序列',
uploading: '正在上传',
success: '上传成功',
error: '上传错误'
}
export default {
data() {
return {
// 上传并发数
simultaneousUploads: 3,
uploadIdInfo: null,
uploadFileList: [],
}
},
methods: {
/**
* 开始上传文件
*/
handler() {
const self = this
//判断文件列表是否为空
if (this.uploadFileList.length === 0) {
this.$message.error('请先选择文件')
return
}
if (currentFileIndex >= this.uploadFileList.length) {
this.$message.success("文件上传成功")
return;
}
//当前操作文件
const currentFile = this.uploadFileList[currentFileIndex]
console.log("当前操作文件:", currentFile)
//debugger
//更新上传标签
currentFile.status = FileStatus.getMd5
currentFile.chunkUploadedList = []
//截取封面图片
//this.ScreenshotVideo(currentFile.raw);
// 1. 计算文件MD5
this.getFileMd5(currentFile.raw, async (md5, totalChunks) => {
console.log("md5值", md5)
// 2. 检查是否已上传
const checkResult = await self.checkFileUploadedByMd5(md5)
console.log("检查是否已上传-->",checkResult)
// debugger
if (checkResult.code === 1) {
//self.$message.success(`上传成功,文件地址:${checkResult.data.url}`)
console.log('上传成功文件访问地址:' + checkResult.data.url)
currentFile.status = FileStatus.success
currentFile.uploadProgress = 100
//如果此文件上传过,就跳到下一个文件
currentFileIndex++;
this.handler()
return
} else if (checkResult.code === 2) { // "上传中" 状态
// 获取已上传分片列表
console.log("上传中:", checkResult)
currentFile.status = FileStatus.uploading
let chunkUploadedList = checkResult.data.chunkUploadedList
console.log("chunkUploadedList",chunkUploadedList)
currentFile.chunkUploadedList = chunkUploadedList
console.log("成功上传的分片信息", chunkUploadedList)
} else { // 未上传
console.log('未上传')
}
// 3. 正在创建分片
currentFile.status = FileStatus.chip;
//创建分片
let fileChunks = self.createFileChunk(currentFile.raw, chunkSize);
//重命名文件
//let fileName = this.getNewFileName(currentFile)
// 获取文件类型
//let type = currentFile.name.substring(currentFile.name.lastIndexOf(".") + 1)
let type = fileSuffixTypeUtil(currentFile.name)
let param = {
fileName: currentFile.name,
fileSize: currentFile.size,
chunkSize: chunkSize,
chunkNum: totalChunks,
fileMd5: md5,
contentType: 'application/octet-stream',
fileType: type,
//uploadId:localStorage.getItem("file_upload_id"),
chunkUploadedList: currentFile.chunkUploadedList//已上传的分片索引+1
}
// debugger
// 4. 获取上传url
let uploadIdInfoResult = await self.getFileUploadUrls(param)
//debugger
let uploadIdInfo = uploadIdInfoResult.data
console.log("获取上传url-->", uploadIdInfo)
let uploadUrls = uploadIdInfo.urlList
self.$set(currentFile, 'chunkList', [])
if (uploadUrls !== undefined) {
if (fileChunks.length !== uploadUrls.length) {
self.$message.error('文件分片上传地址获取错误')
return
}
}
fileChunks.map((chunkItem, index) => {
if(currentFile.chunkUploadedList.indexOf(index+1)!==-1){
currentFile.chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 100,
progressStatus:'success',
status: '上传成功'
})
}else{
currentFile.chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 0,
status: '—'
})
}
})
console.log("所有分片信息:",currentFile.chunkList)
let tempFileChunks = []
currentFile.chunkList.forEach((item) => {
tempFileChunks.push(item)
})
//更新状态
currentFile.status = FileStatus.uploading
// 处理分片列表,删除已上传的分片
tempFileChunks = self.processUploadChunkList(tempFileChunks)
console.log("删除已上传的分片-->", tempFileChunks)
// 5. 上传
await self.uploadChunkBase(tempFileChunks)
console.log('---上传完成---')
//判断是否单文件上传或者分片上传
if (uploadIdInfo.uploadId === "SingleFileUpload") {
console.log("单文件上传");
//更新状态
currentFile.status = FileStatus.success
//文件下标偏移
currentFileIndex++;
//递归上传下一个文件
this.handler()
return
} else {
// 6. 合并文件
console.log("合并文件-->",currentFile)
const mergeResult =await self.mergeFile({
uploadId: uploadIdInfo.uploadId,
fileName: currentFile.name,
fileMd5: md5,
fileType: type,
chunkNum:uploadIdInfo.urlList.length,
chunkSize:chunkSize,
fileSize:currentFile.size
})
//合并文件状态
if (!mergeResult.data) {
currentFile.status = FileStatus.error
self.$message.error(mergeResult.error)
} else {
localStorage.removeItem(FILE_UPLOAD_ID_KEY)
currentFile.status = FileStatus.success
console.log('文件访问地址:' + mergeResult.data)
//文件下标偏移
currentFileIndex++;
//递归上传下一个文件
this.handler()
}
}
})
},
/**
* 保存文件信息到数据库
* @param {*} imgInfoUrl 上传图片封面
* @param {*} currentFile 上传文件
* @param {*} fileName 文件名
* @param {*} url 文件url地址
* @param {*} md5 md5校验
*/
saveFileInfoToDB(currentFile, fileName, url, md5) {
let userInfoCache = JSON.parse(localStorage.getItem('userInfo'))
let VideoFileInfo = {
userId: userInfoCache.id,
fileRealName: currentFile.name,
fileName: fileName,
fileSize: currentFile.size,
fileMd5: md5,
fileAddress: url,
// imgAddress: imgInfoUrl,
bucketName: 'video',
fileType: 'video',
}
console.log(VideoFileInfo);
uploadFileInfo(VideoFileInfo).then(res => {
console.log(res.data);
if (res.status == 200) {
this.$message.success("文件信息存储成功");
//递归上传文件
if (this.uploadFileList.length > currentFileIndex) {
this.handleUpload()
}
} else {
this.$message.error("文件信息存储失败");
}
})
},
/**
* 清空列表
*/
clearFileHandler() {
this.uploadFileList = []
this.uploadIdInfo = null
this.fileIndex = 0
currentFileIndex = 0
},
/**
* 上传文件列表
* @param {*} file
* @param {*} fileList
*/
handleFileChange(file, fileList) {
this.initFileProperties(file)
this.uploadFileList = fileList
},
//初始化文件属性
initFileProperties(file) {
file.chunkList = []
file.status = FileStatus.wait
file.progressStatus = 'warning'
file.uploadProgress = 0
},
/**
* 移除文件列表
* @param {*} file
* @param {*} fileList
*/
handleRemove(file, fileList) {
this.uploadFileList = fileList
},
/**
* 检查上传文件格式
* @param {*} file
*/
beforeUploadVideo(file) {
let type = file.name.substring(file.name.lastIndexOf(".") + 1);
if (
[
"mp4",
"ogg",
"flv",
"avi",
"wmv",
"rmvb"
].indexOf(type) == -1
) {
this.$message.error("请上传正确的视频格式");
return false;
}
},
getNewFileName(file, md5) {
return new Date().getTime() + file.name
//return md5+"-"+ file.name
},
/**
* 分片读取文件 获取文件的MD5
* @param file
* @param callback
*/
getFileMd5(file, callback) {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
const fileReader = new FileReader()
// 计算分片数
const totalChunks = Math.ceil(file.size / chunkSize)
console.log('总分片数:' + totalChunks)
let currentChunk = 0
const spark = new SparkMD5.ArrayBuffer()
loadNext()
fileReader.onload = function (e) {
try {
spark.append(e.target.result)
} catch (error) {
console.log('获取Md5错误:' + currentChunk)
}
if (currentChunk = file.size) ? file.size : start + chunkSize
// 注意这里的 fileRaw
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
},
/**
* 文件分片
*/
createFileChunk(file, size = chunkSize) {
const fileChunkList = []
let count = 0
while (count = 0; i--) {
const chunkItem = chunkList[i]
for (let j = 0; j < chunkUploadedList.length; j++) {
if (chunkItem.chunkNumber === chunkUploadedList[j]) {
chunkList.splice(i, 1)
break
}
}
}
return chunkList
},
/**
* 上传分片
* @param chunkList
* @returns {Promise}
*/
uploadChunkBase(chunkList) {
const self = this
let successCount = 0
let totalChunks = chunkList.length
return new Promise((resolve, reject) => {
const handler = () => {
if (chunkList.length) {
const chunkItem = chunkList.shift()
// 直接上传二进制,不需要构造 FormData,否则上传后文件损坏
axios.put(chunkItem.uploadUrl, chunkItem.chunk.file, {
// 上传进度处理
onUploadProgress: self.checkChunkUploadProgress(chunkItem),
headers: {
'Content-Type': 'application/octet-stream'
}
}).then(response => {
if (response.status === 200) {
console.log('分片:' + chunkItem.chunkNumber + ' 上传成功')
successCount++
// 继续上传下一个分片
// debugger
handler()
} else {
console.log('上传失败:' + response.status + ',' + response.statusText)
}
})
.catch(error => {
// 更新状态
console.log('分片:' + chunkItem.chunkNumber + ' 上传失败,' + error)
// 重新添加到队列中
chunkList.push(chunkItem)
handler()
})
}
if (successCount >= totalChunks) {
resolve()
}
}
// 并发
for (let i = 0; i }
*/
getFileUploadUrls(fileParam) {
return initUpload(fileParam)
},
/**
* 根据MD5查看文件是否上传过
* @param md5
* @returns {Promise}
*/
checkFileUploadedByMd5(md5) {
return new Promise((resolve, reject) => {
checkUpload(md5).then(response => {
console.log("md5-->:", response);
resolve(response)
}).catch(error => {
reject(error)
})
})
},
/**
* 合并文件
* uploadId: self.uploadIdInfo.uploadId,
fileName: currentFile.name,
//fileMd5: fileMd5,
bucketName: 'bucket'
*/
mergeFile(fileParam) {
const self = this;
return new Promise((resolve, reject) => {
mergeUpload(fileParam).then(response => {
console.log(response);
let data = response
console.log("@@@", data)
if (!data) {
console.log("222")
data.msg = FileStatus.error
resolve(data)
} else {
data.msg = FileStatus.success
resolve(data)
}
})
// .catch(error => {
// self.$message.error('合并文件失败:' + error)
// file.status = FileStatus.error
// reject()
// })
})
},
/**
* 检查分片上传进度
*/
checkChunkUploadProgress(item) {
return p => {
item.progress = parseInt(String((p.loaded / p.total) * 100))
console.log("进度:", this.uploadFileList[currentFileIndex].uploadProgress)
this.updateChunkUploadStatus(item)
}
},
updateChunkUploadStatus(item) {
let status = FileStatus.uploading
let progressStatus = 'normal'
if (item.progress >= 100) {
status = FileStatus.success
progressStatus = 'success'
}
let chunkIndex = item.chunkNumber - 1
let currentChunk = this.uploadFileList[currentFileIndex].chunkList[chunkIndex]
// 修改状态
currentChunk.status = status
currentChunk.progressStatus = progressStatus
// 更新状态
this.$set(this.uploadFileList[currentFileIndex].chunkList, chunkIndex, currentChunk)
// 获取文件上传进度
this.getCurrentFileProgress()
},
getCurrentFileProgress() {
const currentFile = this.uploadFileList[currentFileIndex]
if (!currentFile || !currentFile.chunkList) {
return
}
const chunkList = currentFile.chunkList
const uploadedSize = chunkList.map((item) => item.chunk.file.size * item.progress).reduce((acc, cur) => acc + cur)
// 计算方式:已上传大小 / 文件总大小
let progress = parseInt((uploadedSize / currentFile.size).toFixed(2))
// debugger
currentFile.uploadProgress = progress
this.$set(this.uploadFileList,currentFileIndex, currentFile)
},
},
filters: {
transformByte(size) {
if (!size) {
return '0B'
}
const unitSize = 1024
if (size < unitSize) {
return size + ' B'
}
// KB
if (size < Math.pow(unitSize, 2)) {
return (size / unitSize).toFixed(2) + ' K';
}
// MB
if (size < Math.pow(unitSize, 3)) {
return (size / Math.pow(unitSize, 2)).toFixed(2) + ' MB'
}
// GB
if (size < Math.pow(unitSize, 4)) {
return (size / Math.pow(unitSize, 3)).toFixed(2) + ' GB';
}
// TB
return (size / Math.pow(unitSize, 4)).toFixed(2) + ' TB';
}
}
}
.container {
width: 750px;
margin: 0 auto;
}
.file-list-wrapper {
margin-top: 20px;
}
h2 {
text-align: center;
}
.file-info-item {
margin: 0 10px;
}
.upload-file-item {
display: flex;
}
.file-progress {
display: flex;
align-items: center;
}
.file-progress-value {
width: 150px;
}
.file-name {
width: 250px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
width: 100px;
margin-left: 60px;
}
.uploader-example {
width: 880px;
padding: 15px;
margin: 40px auto 0;
font-size: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, .4);
}
.uploader-example .uploader-btn {
margin-right: 4px;
}
.uploader-example .uploader-list {
max-height: 440px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
3.FileUtil.js(判断文件类型)
/**
* @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;
};
4.upload.js(请求后端方法)
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
})
};
5.request.js
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
三、后端实现
1.数据库
/*
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;
2.依赖pom.xml
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
3.application.yml
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存储任务的时间 单位(天)
4.controller文件上传
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
5.UploadService
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
6.UploadServiceImpl
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
7.FilesMapper
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 {
}
8.Files、FileUploadInfo实体类
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;
}
9.MinioUtils
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 、成功");
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;
// }
10.RedisUtil
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 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) {
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;
}
}
}
11.CustomMinioClient
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);
}
}
12.WebAppConfigurer
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);
}
}
13.MybatisPlusConfig
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 有多个方法,详细查看源码) */
}
14.R统一结果返回
package com.wusuowei.miniouploadfile.utils;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark sunlightcs@gmail.com
*/
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");
}
}
15.RespEnum
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上下载下来运行。项目的功能我测试优化了很多次,如果代码有何异常或者不完整欢迎在评论区留言。如果这篇文章有幸帮助到你,希望读者大大们可以给作者点个赞呀😶🌫️😶🌫️😶🌫️