网上关于minio分片上传的资料不太详细,缺斤少两,所以我基于他们的代码做了一些修改,demo能够正常运行起来,但是偶尔也会发生一些小bug,不过这些都无伤大雅,最终目的是理解代码背后的逻辑和流程
流程:
- 前端获取生成文件MD5,发送至后台判断是否有该文件缓存,有信息终止上传,无则开始进行文件分片 。这里,我为了简单方便实现便没有使用数据库,直接用redis存储文件信息;
- 前端后端返回的结果进行分片,然后将文件分片的信息传输给后端,后端调用 minio 初始化,返回分片上传地址和 uploadId;
- 前端则根据获取的分片上传地址直接通过axios上传分片文件,不走后端;
- 上传完成后,前端发送请求至后端,后端调用 minio 合并文件;
流程图:
效果图
1.vue前端
2. minio文件桶
一.前端vue代码(代码较多,我就分开贴)
项目中使用到的类库:spark-md5
、axios
、element-ui
;
spark-md5
主要用来计算文件MD5,安装命令:
npm install spark-md5 --S
1.template
<template>
<div class="container">
<div style="display:none;">
<video width="320" height="240" controls id="upvideo">
</video>
</div>
<h2>上传示例</h2>
<el-upload class="upload-demo" ref="upload" action="https://jsonplaceholder.typicode.com/posts/"
:on-remove="handleRemove" :on-change="handleFileChange" :file-list="uploadFileList" :show-file-list="false"
:auto-upload="false" multiple>
<el-button slot="trigger" type="primary" plain>选择文件</el-button>
<el-button style="margin-left: 5px;" type="success" @click="handler" plain>上传</el-button>
<el-button type="danger" @click="clearFileHandler" plain>清空</el-button>
</el-upload>
<img :src="imgDataUrl" v-show="imgDataUrl" />
<!-- 文件列表 -->
<div class="file-list-wrapper">
<el-collapse>
<el-collapse-item v-for="(item, index) in uploadFileList" :key="index">
<template slot="title">
<div class="upload-file-item">
<div class="file-info-item file-name" :title="item.name">{{ item.name }}</div>
<div class="file-info-item file-size">{{ item.size | transformByte }}</div>
<div class="file-info-item file-progress">
<span class="file-progress-label"></span>
<el-progress :percentage="item.uploadProgress" class="file-progress-value" />
</div>
<div class="file-info-item file-size"><span></span>
<el-tag v-if="item.status === '等待上传'" size="medium" type="info">等待上传</el-tag>
<el-tag v-else-if="item.status === '校验MD5'" size="medium" type="warning">校验MD5</el-tag>
<el-tag v-else-if="item.status === '正在上传'" size="medium">正在上传</el-tag>
<el-tag v-else-if="item.status === '上传成功'" size="medium" type="success">上传完成</el-tag>
<el-tag v-else size="medium" type="danger">上传错误</el-tag>
</div>
</div>
</template>
<div class="file-chunk-list-wrapper">
<!-- 分片列表 -->
<el-table :data="item.chunkList" max-height="400" style="width: 100%">
<el-table-column prop="chunkNumber" label="分片序号" width="180">
</el-table-column>
<el-table-column prop="progress" label="上传进度">
<template v-slot="{ row }">
<el-progress v-if="!row.status || row.progressStatus === 'normal'"
:percentage="row.progress" />
<el-progress v-else :percentage="row.progress" :status="row.progressStatus"
:text-inside="true" :stroke-width="16" />
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="180">
</el-table-column>
</el-table>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
2.scirpt
<script>
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'
import { checkUpload, initUpload, mergeUpload, uploadFileInfo } from "@/api/upload";
import {fileSuffixTypeUtil} from "@/utils/util"
import SparkMD5 from 'spark-md5'
const FILE_UPLOAD_ID_KEY = 'file_upload_id'
const chunkSize = 10 * 1024 * 1024
let currentFileIndex = 0
const FileStatus = {
wait: '等待上传',
getMd5: '校验MD5',
chip: '正在创建序列',
uploading: '正在上传',
success: '上传成功',
error: '上传错误'
}
export default {
data() {
return {
changeDisabled: false,
uploadDisabled: false,
// 上传并发数
simultaneousUploads: 3,
uploadIdInfo: null,
uploadFileList: [],
retryList: [],
videoPoster: null,
ffmpeg: createFFmpeg({ log: true }), //截图工具ffmpeg
imgDataUrl: '',
}
},
methods: {
/**
* 开始上传文件
*/
handler() {
const self = this
//判断文件列表是否为空
if (this.uploadFileList.length === 0) {
this.$message.error('请先选择文件')
return
}
//当前操作文件
const currentFile = this.uploadFileList[currentFileIndex]
//更新上传标签
currentFile.status = FileStatus.getMd5
//截取封面图片
//this.ScreenshotVideo(currentFile.raw);
// 1. 计算文件MD5
this.getFileMd5(currentFile.raw, async (md5, totalChunks) => {
// 2. 检查是否已上传
const checkResult = await self.checkFileUploadedByMd5(md5)
// 确认上传状态
if (checkResult.code === 1) {
self.$message.success(`上传成功,文件地址:${checkResult.data.url}`)
console.log('文件访问地址:' + checkResult.data.url)
currentFile.status = FileStatus.success
currentFile.uploadProgress = 100
return
} else if (checkResult.code === 2) { // "上传中" 状态
// 获取已上传分片列表
let chunkUploadedList = checkResult.data
currentFile.chunkUploadedList = 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: fileName,
fileSize: currentFile.size,
chunkSize: chunkSize,
partCount: totalChunks,
fileMd5: md5,
contentType: 'application/octet-stream',
fileType: type,
}
// 4. 获取上传url
let uploadIdInfoResult = await self.getFileUploadUrls(param)
debugger
let uploadIdInfo = uploadIdInfoResult.data.data
self.saveFileUploadId(uploadIdInfo.uploadId)
let uploadUrls = uploadIdInfo.urlList
self.$set(currentFile, 'chunkList', [])
if (uploadUrls !== undefined) {
if (fileChunks.length !== uploadUrls.length) {
self.$message.error('文件分片上传地址获取错误')
return
}
}
// else if (uploadUrls.length === 1) {
// currentFileIndex++;
// //文件上传成功
// //this.saveFileInfoToDB(currentFile, fileName, uploadIdInfoResult.data.data, md5);
// currentFile.uploadProgress = 100
// currentFile.status = FileStatus.success
// //return;
// }
fileChunks.map((chunkItem, index) => {
currentFile.chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 0,
status: '—'
})
})
let tempFileChunks = []
currentFile.chunkList.forEach((item) => {
tempFileChunks.push(item)
})
//更新状态
currentFile.status = FileStatus.uploading
// 5. 上传
await self.uploadChunkBase(tempFileChunks)
// let imgParam = {
// fileName: screenImg.name,
// fileSize: screenImg.size,
// partCount: 1,
// contentType: 'application/octet-stream',
// fileType: 'image',
// }
// //上传封面图
// let screenImgUrl = await self.getFileUploadUrls(imgParam)
// 处理分片列表,删除已上传的分片
tempFileChunks = self.processUploadChunkList(tempFileChunks)
console.log('上传完成')
//判断是否单文件上传或者分片上传
if (uploadIdInfo.uploadId === "SingleFileUpload") {
console.log("单文件上传");
//更新状态
currentFile.status = FileStatus.success
}
else {
// 6. 合并文件
const mergeResult = await self.mergeFile({
uploadId: uploadIdInfo.uploadId,
fileName: fileName,
fileMd5: md5,
fileType: type,
})
//合并文件状态
if (!mergeResult.data) {
currentFile.status = FileStatus.error
self.$message.error(mergeResult.error)
} else {
currentFile.status = FileStatus.success
console.log('文件访问地址:' + mergeResult.data)
self.$message.success(`上传成功,文件地址:${mergeResult.data}`)
//文件下标偏移
currentFileIndex++;
//递归上传下一个文件
this.handler()
}
}
//this.saveFileInfoToDB( currentFile, fileName, mergeResult.url, md5);
})
},
/**
* 保存文件信息到数据库
* @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
currentFileIndex = 0
},
/**
* 上传文件列表
* @param {*} file
* @param {*} fileList
*/
handleFileChange(file, fileList) {
//if (!this.beforeUploadVideo(file)) return
this.uploadFileList = fileList
this.uploadFileList.forEach((item) => {
// 初始化自定义属性
this.initFileProperties(item)
})
},
//初始化文件属性
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
*/
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 < totalChunks) {
currentChunk++
loadNext()
} else {
callback(spark.end(), totalChunks)
}
}
fileReader.onerror = function () {
console.warn('读取Md5失败,文件读取错误')
}
function loadNext() {
const start = currentChunk * chunkSize
const end = ((start + chunkSize) >= 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 < file.size) {
fileChunkList.push({
file: file.slice(count, count + size),
})
count += size
}
return fileChunkList
},
/**
* 处理即将上传的分片列表,判断是否有已上传的分片,有则从列表中删除
*/
processUploadChunkList(chunkList) {
const currentFile = this.uploadFileList[currentFileIndex]
let chunkUploadedList = currentFile.chunkUploadedList
if (chunkUploadedList === undefined || chunkUploadedList === null || chunkUploadedList.length === 0) {
return chunkList
}
//
for (let i = chunkList.length - 1; i >= 0; i--) {
const chunkItem = chunkList[currentFileIndex]
for (let j = 0; j < chunkUploadedList.length; j++) {
if (chunkItem.chunkNumber === chunkUploadedList[j]) {
chunkList.splice(i, 1)
break
}
}
}
return chunkList
},
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 + ' 上传成功')
//如果长度为1,说明是单文件,直接退出
// if (chunkList.length === 1) {
// return;
// }
successCount++
// 继续上传下一个分片
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 < this.simultaneousUploads; i++) {
handler()
}
})
},
getFileUploadUrls(fileParam) {
return initUpload(fileParam)
},
saveFileUploadId(data) {
localStorage.setItem(FILE_UPLOAD_ID_KEY, data)
},
checkFileUploadedByMd5(md5) {
return new Promise((resolve, reject) => {
checkUpload(md5).then(response => {
console.log(response.data);
resolve(response.data)
}).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.data);
let data = response.data
if (!data.data) {
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))
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))
currentFile.uploadProgress = progress
this.$set(this.uploadFileList, 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';
}
}
}
</script>
3.css
<style scoped lang="less">
.container {
width: 600px;
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: 190px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
width: 100px;
}
.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;
}
</style>
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 { getToken } from '@/utils/CookiesSet' //这个是获取token值,获取即可
//import Qs from 'qs' //如果需要转换
// 创建 axios 实例
const service = axios.create({
baseURL: "/api", // 环境的不同,对应不同的baseURL
// transformRequest: [function(data) {
// return Qs.stringify(data)
// }],
//timeout: 5000 // 请求超时时间
})
//request请求拦截
service.interceptors.request.use(
config => {
var token=getToken()
if (token) {
config.headers.token = token // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
},
error => {
// do something with request error
return Promise.reject(error)
}
)
//响应拦截
service.interceptors.response.use(
response => {
const res = response
if (res.data.status !== 200) { //code返回参数根据实际后端返回参数
}
return res
},
error => {
//这里还可以根据实际情况增加一些功能
return Promise.reject(error)
}
)
export default service
二.后端代码
后端使用的是springboot ,使用之前要启动minio,redis,否则文件上传会出现异常。这里我都是使用windows版的
1.controller,文件上传接口
package com.xy.controller;
import com.xy.entity.FileInfo;
import com.xy.entity.FileUploadInfo;
import com.xy.service.UploadService;
import com.xy.service.VideoFileInfoService;
import com.xy.util.MinioUtils;
import com.xy.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import static com.xy.util.ResponseResult.error;
import static com.xy.util.ResponseResult.success;
import static com.xy.util.ResultCode.ACCESS_PARAMETER_INVALID;
/**
* minio上传流程
*
* 1.检查数据库中是否存在上传文件
*
* 2.根据文件信息初始化,获取分片预签名url地址,前端根据url地址上传文件
*
* 3.上传完成后,将分片上传的文件进行合并
*
* 4.保存文件信息到数据库
*/
@RestController
@Slf4j
public class FileMinioController {
@Resource
private UploadService uploadService;
@Resource
private VideoFileInfoService videoFileInfoService;
@Resource
private MinioUtils minioUtils;
/**
* 校验文件是否存在
*
* @param md5 String
* @return ResponseResult<Object>
*/
@GetMapping("/multipart/check")
public ResponseResult checkFileUploadedByMd5(@RequestParam("md5") String md5) {
log.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传", md5);
if (StringUtils.isEmpty(md5)) {
log.error("查询文件是否存在、入参无效");
return error(ACCESS_PARAMETER_INVALID);
}
return uploadService.getByFileSha256(md5);
}
/**
* 分片初始化
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/init")
public ResponseResult initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
return uploadService.initMultiPartUpload(fileUploadInfo);
}
/**
* 完成上传
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/merge")
public ResponseResult completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通过 {} 合并上传任务", fileUploadInfo);
//Map<String, Object> resMap = new HashMap<>();
//合并文件
boolean result = uploadService.mergeMultipartUpload(fileUploadInfo);
//获取上传文件地址
if(result){
String fliePath = uploadService.getFliePath(fileUploadInfo.getFileType().toLowerCase(), fileUploadInfo.getFileName());
return success(fliePath);
}
return error();
}
/**
* 保存文件信息到数据库
* @param fileInfo 文件信息
* @return
*/
@PostMapping("/multipart/uploadFileInfo")
public ResponseResult uploadFileInfo(@RequestBody FileInfo fileInfo){
log.info("REST: 上传文件信息 <{}> ", fileInfo);
if(fileInfo ==null){
return error(ACCESS_PARAMETER_INVALID);
}else{
FileInfo insert = videoFileInfoService.insert(fileInfo);
}
return success();
}
@PostMapping("/multipart/uploadScreenshot")
public ResponseResult uploaduploadScreenshot(@RequestPart("photos") MultipartFile[] photos,
@RequestParam("buckName") String buckName){
log.info("REST: 上传文件信息 <{}> ", photos);
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
uploadService.upload(photo,buckName);
}
}
return success();
}
@RequestMapping("/createBucket")
public void createBucket(@RequestParam("bucketName")String bucketName){
String bucket = minioUtils.createBucket(bucketName);
}
}
2.UploadService
package com.xy.service;
import com.xy.entity.FileUploadInfo;
import com.xy.util.ResponseResult;
import org.springframework.web.multipart.MultipartFile;
public interface UploadService {
/**
* 分片上传初始化
*
* @param fileUploadInfo
* @return Map<String, Object>
*/
ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo);
/**
* 完成分片上传
*
* @param fileUploadInfo
* @return boolean
*/
boolean mergeMultipartUpload(FileUploadInfo fileUploadInfo);
/**
* 通过 sha256 获取已上传的数据
* @param sha256 String
* @return Mono<Map<String, Object>>
*/
ResponseResult<Object> getByFileSha256(String sha256);
/**
* 获取文件地址
* @param bucketName
* @param fileName
*
*/
String getFliePath(String bucketName, String fileName);
/**
* 单文件上传
* @param file
* @param bucketName
* @return
*/
String upload(MultipartFile file, String bucketName);
}
3.UploadServiceImpl
package com.xy.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.xy.entity.FileUploadInfo;
import com.xy.service.UploadService;
import com.xy.util.MinioUtils;
import com.xy.util.RedisRepo;
import com.xy.util.ResponseResult;
import com.xy.util.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@Slf4j
@Service
public class UploadServiceImpl implements UploadService {
@Resource
private MinioUtils fileService;
@Resource
private RedisRepo redisRepo;
/**
* 通过 sha256 获取已上传的数据(断点续传)
*
* @param sha256 String
* @return Mono<Map < String, Object>>
*/
@Override
public ResponseResult<Object> getByFileSha256(String sha256) {
log.info("tip message: 通过 <{}> 查询数据是否存在", sha256);
// 获取文件名称和id
String value = redisRepo.get(sha256);
FileUploadInfo fileUploadInfo = null;
if (value != null) {
fileUploadInfo = JSONObject.parseObject(value, FileUploadInfo.class);
}
if (fileUploadInfo == null) {
// 返回数据不存在
log.error("error message: 文件数据不存在");
return ResponseResult.error(ResultCode.FOUND);
}
// 获取桶名称
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
return fileService.getByFileSha256(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName);
}
/**
* 文件分片上传
*
* @param fileUploadInfo
* @return Mono<Map < String, Object>>
*/
@Override
public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo) {
log.info("tip message: 通过 <{}> 开始初始化<分片上传>任务", fileUploadInfo);
// 获取桶
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
// 单文件上传可拆分,这里只做演示,可直接上传完成
if (fileUploadInfo.getPartCount() == 1) {
log.info("tip message: 当前分片数量 <{}> 进行单文件上传", fileUploadInfo.getPartCount());
return fileService.getUploadObjectUrl(fileUploadInfo.getFileName(), bucketName);
}
// 分片上传
else {
log.info("tip message: 当前分片数量 <{}> 进行分片上传", fileUploadInfo.getPartCount());
return fileService.initMultiPartUpload(fileUploadInfo, fileUploadInfo.getFileName(), fileUploadInfo.getPartCount(), fileUploadInfo.getContentType(), bucketName);
}
}
/**
* 文件合并
*
* @param
* @return boolean
*/
@Override
public boolean mergeMultipartUpload(FileUploadInfo fileUploadInfo) {
log.info("tip message: 通过 <{}> 开始合并<分片上传>任务", fileUploadInfo);
// 获取桶名称
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
return fileService.mergeMultipartUpload(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName);
}
@Override
public String getFliePath(String bucketName, String fileName) {
return fileService.getFliePath(bucketName, fileName);
}
@Override
public String upload(MultipartFile file, String bucketName) {
fileService.upload(file, bucketName);
return getFliePath(bucketName, file.getName());
}
}
4.MinioUtils
package com.xy.util;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.HashMultimap;
import com.xy.config.CustomMinioClient;
import com.xy.entity.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 javax.annotation.Resource;
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;
import static com.xy.util.ResultCode.DATA_NOT_EXISTS;
import static com.xy.util.ResultCode.UPLOAD_FILE_FAILED;
@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;
@Resource
private RedisRepo redisRepo;
private CustomMinioClient customMinioClient;
//初始化配置文件
private Properties SysLocalPropObject = new Properties();
/**
* 用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 ResponseResult<Object> getUploadObjectUrl(String objectName, String bucketName) {
try {
log.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName);
Map<String, Object> resMap = new HashMap<>();
List<String> partList = new ArrayList<>();
String url = customMinioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(objectName)
.expiry(1, TimeUnit.DAYS)
.build());
log.info("tip message: 单个文件上传、成功");
partList.add(url);
resMap.put("uploadId", "SingleFileUpload");
resMap.put("urlList", partList);
return ResponseResult.success(resMap);
} catch (Exception e) {
log.error("error message: 单个文件上传失败、原因:", e);
// 返回 文件上传失败
return ResponseResult.error(UPLOAD_FILE_FAILED);
}
}
/**
* 初始化分片上传
*
* @param fileUploadInfo
* @param objectName 文件全路径名称
* @param partCount 分片数量
* @param contentType 类型,如果类型使用默认流会导致无法预览
* @param bucketName 桶名称
* @return Mono<Map < String, Object>>
*/
public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int partCount, String contentType, String bucketName) {
log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName);
Map<String, Object> resMap = new HashMap<>();
try {
if (CharSequenceUtil.isBlank(contentType)) {
contentType = "application/octet-stream";
}
HashMultimap<String, String> headers = HashMultimap.create();
headers.put("Content-Type", contentType);
//获取uploadId
String uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
resMap.put("uploadId", uploadId);
fileUploadInfo.setUploadId(uploadId);
//redis保存文件信息
redisRepo.saveTimeout(fileUploadInfo.getFileMd5(), JSONObject.toJSONString(fileUploadInfo), 30, TimeUnit.MINUTES);
List<String> partList = new ArrayList<>();
Map<String, String> reqParams = new HashMap<>();
reqParams.put("uploadId", uploadId);
for (int i = 1; i <= partCount; 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 ResponseResult.success(resMap);
} catch (Exception e) {
log.error("error message: 初始化分片上传失败、原因:", e);
// 返回 文件上传失败
return ResponseResult.error(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);
return false;
}
return true;
}
/**
* 通过 sha256 获取上传中的分片信息
*
* @param objectName 文件全路径名称
* @param uploadId 返回的uploadId
* @param bucketName 桶名称
* @return Mono<Map < String, Object>>
*/
public ResponseResult<Object> getByFileSha256(String objectName, String uploadId, String bucketName) {
log.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
//Map<String, Object> resMap = new HashMap<>();
try {
// 查询上传后的分片数据
ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
List<Integer> collect = partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
// resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
// resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
// resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), collect);
return ResponseResult.uploading(collect);
} catch (Exception e) {
log.error("error message: 查询上传后的分片信息失败、原因:", e);
return ResponseResult.error(DATA_NOT_EXISTS);
}
}
/**
* 获取文件下载地址
*
* @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<String> listBuckets() throws Exception {
// List<Bucket> list = minioClient.listBuckets();
// List<String> names = new ArrayList<>();
// list.forEach(b -> {
// names.add(b.name());
// });
// return names;
// }
//
// /**
// * 列出一个桶中的所有文件和目录
// */
// public List<Fileinfo> listFiles(String bucket) throws Exception {
// Iterable<Result<Item>> results = minioClient.listObjects(
// ListObjectsArgs.builder().bucket(bucket).recursive(true).build());
//
// List<Fileinfo> 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;
// }
5.CustomMinioClient
package com.xy.config;
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<String, String> 请求头
* @param extraQueryParams Multimap<String, String>
* @return String
*/
public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> 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<String, String>
* @param extraQueryParams Multimap<String, String>
* @return ObjectWriteResponse
*/
public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
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<String, String>
* @param extraQueryParams Multimap<String, String>
* @return ListPartsResponse
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
}
6.CorsConfig
package com.xy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 全局跨域处理
* @author CV
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
接下来是返回信息工具类
7.ResponseResult
package com.xy.util;
import lombok.Data;
@Data
public class ResponseResult<T> {
private int code;
private String enMessage;
private String zhMessage;
private T data;
public ResponseResult() {
}
public ResponseResult(int code, String enMessage, String zhMessage) {
this.code = code;
this.enMessage = enMessage;
this.zhMessage = zhMessage;
}
/**
* 成功
*/
public static <T> ResponseResult<T> success() {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
return result;
}
/**
* 成功
*/
public static <T> ResponseResult<T> success(T data) {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
result.setData(data);
return result;
}
/**
* 失败
*/
public static <T> ResponseResult <T> error() {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.FAIL.getCode());
result.setEnMessage(ResultCode.FAIL.getEnMessage());
result.setZhMessage(ResultCode.FAIL.getZhMessage());
return result;
}
/**
* 失败
*/
public static <T> ResponseResult<T> error(T data) {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.FAIL.getCode());
result.setEnMessage(ResultCode.FAIL.getEnMessage());
result.setZhMessage(ResultCode.FAIL.getZhMessage());
result.setData(data);
return result;
}
/**
*
* @param data 数据
* @param <T>
* @return
*/
public static <T> ResponseResult<T> uploading(T data) {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.UPLOADING.getCode());
result.setEnMessage(ResultCode.UPLOADING.getEnMessage());
result.setZhMessage(ResultCode.UPLOADING.getZhMessage());
result.setData(data);
return result;
}
/**
* 成功
*/
public static <T> ResponseResult<T> success(int code, String enMessage, String zhMessage) {
return new ResponseResult(code, enMessage, zhMessage);
}
/**
* 失败
*/
public static <T> ResponseResult<T> error(int code, String enMessage, String zhMessage) {
return new ResponseResult(code, enMessage, zhMessage);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getEnMessage() {
return enMessage;
}
public void setEnMessage(String enMessage) {
this.enMessage = enMessage;
}
public String getZhMessage() {
return zhMessage;
}
public void setZhMessage(String zhMessage) {
this.zhMessage = zhMessage;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
// public static ResponseResult<Void> SUCCESS = new ResponseResult<>(200,"成功");
// public static ResponseResult<Void> INTEVER_ERROR = new ResponseResult<>(500,"服务器错误");
// public static ResponseResult<Void> NOT_FOUND = new ResponseResult<>(404,"未找到");
}
8.ResultCode
package com.xy.util;
/**
* http状态码枚举类
*/
public enum ResultCode {
SUCCESS(1, "Success", "成功"),
UPLOADING(2, "Uploading", "上传中"),
FAIL(-1, "Err", "失败"),
DATABASE_OPERATION_FAILED(504, "数据库操作失败"),
CONTINUE(100, "Continue", "请继续发送请求的剩余部分"),
SWITCHING_PROTOCOLS(101, "Switching Protocols", "协议切换"),
PROCESSING(102, "Processing", "请求将继续执行"),
CHECKPOINT(103, "Checkpoint", "可以预加载"),
OK(200, "OK", "请求已经成功处理"),
CREATED(201, "Created", "请求已经成功处理,并创建了资源"),
ACCEPTED(202, "Accepted", "请求已经接受,等待执行"),
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information", "请求已经成功处理,但是信息不是原始的"),
NO_CONTENT(204, "No Content", "请求已经成功处理,没有内容需要返回"),
RESET_CONTENT(205, "Reset Content", "请求已经成功处理,请重置视图"),
PARTIAL_CONTENT(206, "Partial Content", "部分Get请求已经成功处理"),
MULTI_STATUS(207, "Multi-Status", "请求已经成功处理,将返回XML消息体"),
ALREADY_REPORTED(208, "Already Reported", "请求已经成功处理,一个DAV的绑定成员被前一个请求枚举,并且没有被再一次包括"),
IM_USED(226, "IM Used", "请求已经成功处理,将响应一个或者多个实例"),
MULTIPLE_CHOICES(300, "Multiple Choices", "提供可供选择的回馈"),
MOVED_PERMANENTLY(301, "Moved Permanently", "请求的资源已经永久转移"),
FOUND(302, "Found", "请重新发送请求"),
SEE_OTHER(303, "See Other", "请以Get方式请求另一个URI"),
NOT_MODIFIED(304, "Not Modified", "资源未改变"),
USE_PROXY(305, "Use Proxy", "请通过Location域中的代理进行访问"),
TEMPORARY_REDIRECT(307, "Temporary Redirect", "请求的资源临时从不同的URI响应请求"),
RESUME_INCOMPLETE(308, "Resume Incomplete", "请求的资源已经永久转移"),
BAD_REQUEST(400, "Bad Request", "请求错误,请修正请求"),
UNAUTHORIZED(401, "Unauthorized", "没有被授权或者授权已经失效"),
PAYMENT_REQUIRED(402, "Payment Required", "预留状态"),
FORBIDDEN(403, "Forbidden", "请求被理解,但是拒绝执行"),
NOT_FOUND(404, "Not Found", "资源未找到"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed", "请求方法不允许被执行"),
NOT_ACCEPTABLE(406, "Not Acceptable", "请求的资源不满足请求者要求"),
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required", "请通过代理进行身份验证"),
REQUEST_TIMEOUT(408, "Request Timeout", "请求超时"),
CONFLICT(409, "Conflict", "请求冲突"),
GONE(410, "Gone", "请求的资源不可用"),
LENGTH_REQUIRED(411, "Length Required", "Content-Length未定义"),
PRECONDITION_FAILED(412, "Precondition Failed", "不满足请求的先决条件"),
REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large", "请求发送的实体太大"),
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long", "请求的URI超长"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type", "请求发送的实体类型不受支持"),
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable", "Range指定的范围与当前资源可用范围不一致"),
EXPECTATION_FAILED(417, "Expectation Failed", "请求头Expect中指定的预期内容无法被服务器满足"),
UNPROCESSABLE_ENTITY(422, "Unprocessable Entity", "请求格式正确,但是由于含有语义错误,无法响应"),
LOCKED(423, "Locked", "当前资源被锁定"),
FAILED_DEPENDENCY(424, "Failed Dependency", "由于之前的请求发生错误,导致当前请求失败"),
UPGRADE_REQUIRED(426, "Upgrade Required", "客户端需要切换到TLS1.0"),
PRECONDITION_REQUIRED(428, "Precondition Required", "请求需要提供前置条件"),
TOO_MANY_REQUESTS(429, "Too Many Requests", "请求过多"),
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large", "请求头超大,拒绝请求"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error", "服务器内部错误"),
NOT_IMPLEMENTED(501, "Not Implemented", "服务器不支持当前请求的部分功能"),
BAD_GATEWAY(502, "Bad Gateway", "响应无效"),
SERVICE_UNAVAILABLE(503, "Service Unavailable", "服务器维护或者过载,拒绝服务"),
GATEWAY_TIMEOUT(504, "Gateway Timeout", "上游服务器超时"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported", "不支持的HTTP版本"),
VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates", "服务器内部配置错误"),
INSUFFICIENT_STORAGE(507, "Insufficient Storage", "服务器无法完成存储请求所需的内容"),
LOOP_DETECTED(508, "Loop Detected", "服务器处理请求时发现死循环"),
BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded", "服务器达到带宽限制"),
NOT_EXTENDED(510, "Not Extended", "获取资源所需的策略没有被满足"),
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required", "需要进行网络授权"),
ACCESS_PARAMETER_INVALID(1001,"Invalid access parameter","访问参数无效"),
UPLOAD_FILE_FAILED(1002,"File upload failure","文件上传失败"),
DATA_NOT_EXISTS(1003,"Data does not exist","数据不存在"),
;
private int code;
private String enMessage;
private String zhMessage;
ResultCode(int code, String enMessage, String zhMessage) {
this.code = code;
this.enMessage = enMessage;
this.zhMessage = zhMessage;
}
ResultCode(int code, String message) {
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getEnMessage() {
return enMessage;
}
public void setEnMessage(String enMessage) {
this.enMessage = enMessage;
}
public String getZhMessage() {
return zhMessage;
}
public void setZhMessage(String zhMessage) {
this.zhMessage = zhMessage;
}
}
9.FileUploadInfo,还有最重要的实体类
package com.xy.entity;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class FileUploadInfo {
//@NotBlank(message = "文件名不能为空")
private String fileName;
// @NotNull(message = "文件大小不能为空")
private Double fileSize;
// @NotBlank(message = "Content-Type不能为空")
private String contentType;
// @NotNull(message = "分片数量不能为空")
private Integer partCount;
// @NotBlank(message = "uploadId 不能为空")
private String uploadId;
// 桶名称
//private String bucketName;
//md5
private String fileMd5;
//文件类型
private String fileType;
public FileUploadInfo() {
}
}
10.RedisRepo
package com.xy.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisRepo {
@Autowired
private StringRedisTemplate redisTemplate;
public String get(String key) {
BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
return ops.get();
}
public void save(String key,String str){
BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
ops.set(str);
}
public void saveTimeout(String key, String value, long timeout, TimeUnit unit ){
redisTemplate.boundValueOps(key).setIfAbsent(value,timeout,unit);
}
public void delete(String key){
redisTemplate.delete(key);
}
public long expire(String key){
return redisTemplate.opsForValue().getOperations().getExpire(key);
}
}
11.yaml配置
minio: endpoint: http://localhost:9000 accesskey: minioadmin secretkey: minioadmin spring: redis: host: localhost port: 6379
12.pom配置
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.2</version>
</dependency>
本文仅介绍上传流程的简单实现,很多功能未完善,如文件夹上传、上传暂停、停止等功能。代码有何异常或者不完整欢迎在评论区留言