发布时间:2023-04-28 文章分类:WEB开发, 电脑百科 投稿人:赵颖 字号: 默认 | | 超大 打印

网上关于minio分片上传的资料不太详细,缺斤少两,所以我基于他们的代码做了一些修改,demo能够正常运行起来,但是偶尔也会发生一些小bug,不过这些都无伤大雅,最终目的是理解代码背后的逻辑和流程

流程:

  1. 前端获取生成文件MD5,发送至后台判断是否有该文件缓存,有信息终止上传,无则开始进行文件分片  。这里,我为了简单方便实现便没有使用数据库,直接用redis存储文件信息;
  2. 前端后端返回的结果进行分片,然后将文件分片的信息传输给后端,后端调用 minio 初始化,返回分片上传地址和 uploadId;
  3. 前端则根据获取的分片上传地址直接通过axios上传分片文件,不走后端;
  4. 上传完成后,前端发送请求至后端,后端调用 minio 合并文件;

流程图:

springboot整合Minio + vue 实现文件分片上传(完整代码)

效果图

  1.vue前端

springboot整合Minio + vue 实现文件分片上传(完整代码)

2. minio文件桶

springboot整合Minio + vue 实现文件分片上传(完整代码)

一.前端vue代码(代码较多,我就分开贴)

 项目中使用到的类库:spark-md5axioselement-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>

本文仅介绍上传流程的简单实现,很多功能未完善,如文件夹上传、上传暂停、停止等功能。代码有何异常或者不完整欢迎在评论区留言