网上关于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 * 1024let currentFileIndex = 0const 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 }), //截图工具ffmpegimgDataUrl: '',}},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. 计算文件MD5this.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.successcurrentFile.uploadProgress = 100return} else if (checkResult.code === 2) { // "上传中" 状态// 获取已上传分片列表let chunkUploadedList = checkResult.datacurrentFile.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. 获取上传urllet uploadIdInfoResult = await self.getFileUploadUrls(param)debuggerlet uploadIdInfo = uploadIdInfoResult.data.dataself.saveFileUploadId(uploadIdInfo.uploadId)let uploadUrls = uploadIdInfo.urlListself.$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.errorself.$message.error(mergeResult.error)} else {currentFile.status = FileStatus.successconsole.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 = nullcurrentFileIndex = 0},/** * 上传文件列表 * @param {*} file * @param {*} fileList */handleFileChange(file, fileList) {//if (!this.beforeUploadVideo(file)) returnthis.uploadFileList = fileListthis.uploadFileList.forEach((item) => {// 初始化自定义属性this.initFileProperties(item)})},//初始化文件属性initFileProperties(file) {file.chunkList = []file.status = FileStatus.waitfile.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.webkitSliceconst fileReader = new FileReader()// 计算分片数const totalChunks = Math.ceil(file.size / chunkSize)console.log('总分片数:' + totalChunks)let currentChunk = 0const 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 * chunkSizeconst end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize// 注意这里的 fileRawfileReader.readAsArrayBuffer(blobSlice.call(file, start, end))}},/** * 文件分片 */createFileChunk(file, size = chunkSize) {const fileChunkList = []let count = 0while (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.chunkUploadedListif (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 = thislet successCount = 0let totalChunks = chunkList.lengthreturn 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.dataif (!data.data) {data.msg = FileStatus.errorresolve(data)} else {data.msg = FileStatus.successresolve(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.uploadinglet progressStatus = 'normal'if (item.progress >= 100) {status = FileStatus.successprogressStatus = 'success'}let chunkIndex = item.chunkNumber - 1let currentChunk = this.uploadFileList[currentFileIndex].chunkList[chunkIndex]// 修改状态currentChunk.status = statuscurrentChunk.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.chunkListconst 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 = progressthis.$set(this.uploadFileList, currentFile)},},filters: {transformByte(size) {if (!size) {return '0B'}const unitSize = 1024if (size < unitSize) {return size + ' B'}// KBif (size < Math.pow(unitSize, 2)) {return (size / unitSize).toFixed(2) + ' K';}// MBif (size < Math.pow(unitSize, 3)) {return (size / Math.pow(unitSize, 2)).toFixed(2) + ' MB'}// GBif (size < Math.pow(unitSize, 4)) {return (size / Math.pow(unitSize, 3)).toFixed(2) + ' GB';}// TBreturn (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@Slf4jpublic 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@Servicepublic 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@Componentpublic 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 */@Configurationpublic 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;@Datapublic 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;@Componentpublic 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: minioadminspring: 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>
本文仅介绍上传流程的简单实现,很多功能未完善,如文件夹上传、上传暂停、停止等功能。代码有何异常或者不完整欢迎在评论区留言