首页IT科技springboot上传文件大小限制(springboot整合Minio + vue 实现文件分片上传(完整代码))

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

时间2025-06-16 02:24:46分类IT科技浏览4906
导读:网上关于minio分片上传的资料不太详细,缺斤少两,所以我基于他们的代码做了一些修改,demo能够正常运行起来,但是偶尔也会发生一些小bug,不过这些都无伤大雅,最终目的是理解代码背后的逻辑和流程...

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

流程:

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

流程图:

效果图

  1.vue前端

2. minio文件桶

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

 项目中使用到的类库:spark-md5                、axios                      、element-ui;

spark-md5 主要用来计算文件MD5       ,安装命令:

npm install spark-md5 --S

   1.template 

<template> <div class="container"> <div style="display:none;"> <video width="320" height="240" controls id="upvideo"> </video> </div> <h2>上传示例</h2> <el-upload class="upload-demo" ref="upload" action="https://jsonplaceholder.typicode.com/posts/" :on-remove="handleRemove" :on-change="handleFileChange" :file-list="uploadFileList" :show-file-list="false" :auto-upload="false" multiple> <el-button slot="trigger" type="primary" plain>选择文件</el-button> <el-button style="margin-left: 5px;" type="success" @click="handler" plain>上传</el-button> <el-button type="danger" @click="clearFileHandler" plain>清空</el-button> </el-upload> <img :src="imgDataUrl" v-show="imgDataUrl" /> <!-- 文件列表 --> <div class="file-list-wrapper"> <el-collapse> <el-collapse-item v-for="(item, index) in uploadFileList" :key="index"> <template slot="title"> <div class="upload-file-item"> <div class="file-info-item file-name" :title="item.name">{{ item.name }}</div> <div class="file-info-item file-size">{{ item.size | transformByte }}</div> <div class="file-info-item file-progress"> <span class="file-progress-label"></span> <el-progress :percentage="item.uploadProgress" class="file-progress-value" /> </div> <div class="file-info-item file-size"><span></span> <el-tag v-if="item.status === 等待上传" size="medium" type="info">等待上传</el-tag> <el-tag v-else-if="item.status === 校验MD5" size="medium" type="warning">校验MD5</el-tag> <el-tag v-else-if="item.status === 正在上传" size="medium">正在上传</el-tag> <el-tag v-else-if="item.status === 上传成功" size="medium" type="success">上传完成</el-tag> <el-tag v-else size="medium" type="danger">上传错误</el-tag> </div> </div> </template> <div class="file-chunk-list-wrapper"> <!-- 分片列表 --> <el-table :data="item.chunkList" max-height="400" style="width: 100%"> <el-table-column prop="chunkNumber" label="分片序号" width="180"> </el-table-column> <el-table-column prop="progress" label="上传进度"> <template v-slot="{ row }"> <el-progress v-if="!row.status || row.progressStatus === normal" :percentage="row.progress" /> <el-progress v-else :percentage="row.progress" :status="row.progressStatus" :text-inside="true" :stroke-width="16" /> </template> </el-table-column> <el-table-column prop="status" label="状态" width="180"> </el-table-column> </el-table> </div> </el-collapse-item> </el-collapse> </div> </div> </template>

2.scirpt

<script> import { createFFmpeg, fetchFile } from @ffmpeg/ffmpeg import { checkUpload, initUpload, mergeUpload, uploadFileInfo } from "@/api/upload"; import {fileSuffixTypeUtil} from "@/utils/util" import SparkMD5 from spark-md5 const FILE_UPLOAD_ID_KEY = file_upload_id const chunkSize = 10 * 1024 * 1024 let currentFileIndex = 0 const FileStatus = { wait: 等待上传, getMd5: 校验MD5, chip: 正在创建序列, uploading: 正在上传, success: 上传成功, error: 上传错误 } export default { data() { return { changeDisabled: false, uploadDisabled: false, // 上传并发数 simultaneousUploads: 3, uploadIdInfo: null, uploadFileList: [], retryList: [], videoPoster: null, ffmpeg: createFFmpeg({ log: true }), //截图工具ffmpeg imgDataUrl: , } }, methods: { /** * 开始上传文件 */ handler() { const self = this //判断文件列表是否为空 if (this.uploadFileList.length === 0) { this.$message.error(请先选择文件) return } //当前操作文件 const currentFile = this.uploadFileList[currentFileIndex] //更新上传标签 currentFile.status = FileStatus.getMd5 //截取封面图片 //this.ScreenshotVideo(currentFile.raw); // 1. 计算文件MD5 this.getFileMd5(currentFile.raw, async (md5, totalChunks) => { // 2. 检查是否已上传 const checkResult = await self.checkFileUploadedByMd5(md5) // 确认上传状态 if (checkResult.code === 1) { self.$message.success(`上传成功                ,文件地址:${checkResult.data.url}`) console.log(文件访问地址: + checkResult.data.url) currentFile.status = FileStatus.success currentFile.uploadProgress = 100 return } else if (checkResult.code === 2) { // "上传中" 状态 // 获取已上传分片列表 let chunkUploadedList = checkResult.data currentFile.chunkUploadedList = chunkUploadedList } else { // 未上传 console.log(未上传) } // 3. 正在创建分片 //currentFile.status = FileStatus.chip; //创建分片 let fileChunks = self.createFileChunk(currentFile.raw, chunkSize); //重命名文件 let fileName = this.getNewFileName(currentFile) // 获取文件类型 //let type = currentFile.name.substring(currentFile.name.lastIndexOf(".") + 1) let type = fileSuffixTypeUtil(currentFile.name) let param = { fileName: fileName, fileSize: currentFile.size, chunkSize: chunkSize, partCount: totalChunks, fileMd5: md5, contentType: application/octet-stream, fileType: type, } // 4. 获取上传url let uploadIdInfoResult = await self.getFileUploadUrls(param) debugger let uploadIdInfo = uploadIdInfoResult.data.data self.saveFileUploadId(uploadIdInfo.uploadId) let uploadUrls = uploadIdInfo.urlList self.$set(currentFile, chunkList, []) if (uploadUrls !== undefined) { if (fileChunks.length !== uploadUrls.length) { self.$message.error(文件分片上传地址获取错误) return } } // else if (uploadUrls.length === 1) { // currentFileIndex++; // //文件上传成功 // //this.saveFileInfoToDB(currentFile, fileName, uploadIdInfoResult.data.data, md5); // currentFile.uploadProgress = 100 // currentFile.status = FileStatus.success // //return; // } fileChunks.map((chunkItem, index) => { currentFile.chunkList.push({ chunkNumber: index + 1, chunk: chunkItem, uploadUrl: uploadUrls[index], progress: 0, status: }) }) let tempFileChunks = [] currentFile.chunkList.forEach((item) => { tempFileChunks.push(item) }) //更新状态 currentFile.status = FileStatus.uploading // 5. 上传 await self.uploadChunkBase(tempFileChunks) // let imgParam = { // fileName: screenImg.name, // fileSize: screenImg.size, // partCount: 1, // contentType: application/octet-stream, // fileType: image, // } // //上传封面图 // let screenImgUrl = await self.getFileUploadUrls(imgParam) // 处理分片列表                       ,删除已上传的分片 tempFileChunks = self.processUploadChunkList(tempFileChunks) console.log(上传完成) //判断是否单文件上传或者分片上传 if (uploadIdInfo.uploadId === "SingleFileUpload") { console.log("单文件上传"); //更新状态 currentFile.status = FileStatus.success } else { // 6. 合并文件 const mergeResult = await self.mergeFile({ uploadId: uploadIdInfo.uploadId, fileName: fileName, fileMd5: md5, fileType: type, }) //合并文件状态 if (!mergeResult.data) { currentFile.status = FileStatus.error self.$message.error(mergeResult.error) } else { currentFile.status = FileStatus.success console.log(文件访问地址: + mergeResult.data) self.$message.success(`上传成功       ,文件地址:${mergeResult.data}`) //文件下标偏移 currentFileIndex++; //递归上传下一个文件 this.handler() } } //this.saveFileInfoToDB( currentFile, fileName, mergeResult.url, md5); }) }, /** * 保存文件信息到数据库 * @param {*} imgInfoUrl 上传图片封面 * @param {*} currentFile 上传文件 * @param {*} fileName 文件名 * @param {*} url 文件url地址 * @param {*} md5 md5校验 */ saveFileInfoToDB(currentFile, fileName, url, md5) { let userInfoCache = JSON.parse(localStorage.getItem(userInfo)) let VideoFileInfo = { userId: userInfoCache.id, fileRealName: currentFile.name, fileName: fileName, fileSize: currentFile.size, fileMd5: md5, fileAddress: url, imgAddress: imgInfoUrl, bucketName: video, fileType: video, } console.log(VideoFileInfo); uploadFileInfo(VideoFileInfo).then(res => { console.log(res.data); if (res.status == 200) { this.$message.success("文件信息存储成功"); //递归上传文件 if (this.uploadFileList.length > currentFileIndex) { this.handleUpload() } } else { this.$message.error("文件信息存储失败"); } }) }, /** * 清空列表 */ clearFileHandler() { this.uploadFileList = [] this.uploadIdInfo = null currentFileIndex = 0 }, /** * 上传文件列表 * @param {*} file * @param {*} fileList */ handleFileChange(file, fileList) { //if (!this.beforeUploadVideo(file)) return this.uploadFileList = fileList this.uploadFileList.forEach((item) => { // 初始化自定义属性 this.initFileProperties(item) }) }, //初始化文件属性 initFileProperties(file) { file.chunkList = [] file.status = FileStatus.wait file.progressStatus = warning file.uploadProgress = 0 }, /** * 移除文件列表 * @param {*} file * @param {*} fileList */ handleRemove(file, fileList) { this.uploadFileList = fileList }, /** * 检查上传文件格式 * @param {*} file */ beforeUploadVideo(file) { let type = file.name.substring(file.name.lastIndexOf(".") + 1); if ( [ "mp4", "ogg", "flv", "avi", "wmv", "rmvb" ].indexOf(type) == -1 ) { this.$message.error("请上传正确的视频格式"); return false; } }, getNewFileName(file,md5) { return new Date().getTime() + file.name //return md5+"-"+ file.name }, /** * 分片读取文件 MD5 */ getFileMd5(file, callback) { const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const fileReader = new FileReader() // 计算分片数 const totalChunks = Math.ceil(file.size / chunkSize) console.log(总分片数: + totalChunks) let currentChunk = 0 const spark = new SparkMD5.ArrayBuffer() loadNext() fileReader.onload = function (e) { try { spark.append(e.target.result) } catch (error) { console.log(获取Md5错误: + currentChunk) } if (currentChunk < totalChunks) { currentChunk++ loadNext() } else { callback(spark.end(), totalChunks) } } fileReader.onerror = function () { console.warn(读取Md5失败        ,文件读取错误) } function loadNext() { const start = currentChunk * chunkSize const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize // 注意这里的 fileRaw fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) } }, /** * 文件分片 */ createFileChunk(file, size = chunkSize) { const fileChunkList = [] let count = 0 while (count < file.size) { fileChunkList.push({ file: file.slice(count, count + size), }) count += size } return fileChunkList }, /** * 处理即将上传的分片列表                       ,判断是否有已上传的分片               ,有则从列表中删除 */ processUploadChunkList(chunkList) { const currentFile = this.uploadFileList[currentFileIndex] let chunkUploadedList = currentFile.chunkUploadedList if (chunkUploadedList === undefined || chunkUploadedList === null || chunkUploadedList.length === 0) { return chunkList } // for (let i = chunkList.length - 1; i >= 0; i--) { const chunkItem = chunkList[currentFileIndex] for (let j = 0; j < chunkUploadedList.length; j++) { if (chunkItem.chunkNumber === chunkUploadedList[j]) { chunkList.splice(i, 1) break } } } return chunkList }, uploadChunkBase(chunkList) { const self = this let successCount = 0 let totalChunks = chunkList.length return new Promise((resolve, reject) => { const handler = () => { if (chunkList.length) { const chunkItem = chunkList.shift() // 直接上传二进制        ,不需要构造 FormData                       ,否则上传后文件损坏 axios.put(chunkItem.uploadUrl, chunkItem.chunk.file, { // 上传进度处理 onUploadProgress: self.checkChunkUploadProgress(chunkItem), headers: { Content-Type: application/octet-stream } }).then(response => { if (response.status === 200) { console.log(分片: + chunkItem.chunkNumber + 上传成功) //如果长度为1               ,说明是单文件,直接退出 // if (chunkList.length === 1) { // return; // } successCount++ // 继续上传下一个分片 handler() } else { console.log(上传失败: + response.status +                        , + response.statusText) } }) .catch(error => { // 更新状态 console.log(分片: + chunkItem.chunkNumber + 上传失败                      , + error) // 重新添加到队列中 chunkList.push(chunkItem) handler() }) } if (successCount >= totalChunks) { resolve() } } // 并发 for (let i = 0; i < this.simultaneousUploads; i++) { handler() } }) }, getFileUploadUrls(fileParam) { return initUpload(fileParam) }, saveFileUploadId(data) { localStorage.setItem(FILE_UPLOAD_ID_KEY, data) }, checkFileUploadedByMd5(md5) { return new Promise((resolve, reject) => { checkUpload(md5).then(response => { console.log(response.data); resolve(response.data) }).catch(error => { reject(error) }) }) }, /** * 合并文件 * uploadId: self.uploadIdInfo.uploadId, fileName: currentFile.name, //fileMd5: fileMd5, bucketName: bucket */ mergeFile(fileParam) { const self = this; return new Promise((resolve, reject) => { mergeUpload(fileParam).then(response => { console.log(response.data); let data = response.data if (!data.data) { data.msg = FileStatus.error resolve(data) } else { data.msg = FileStatus.success resolve(data) } }) // .catch(error => { // self.$message.error(合并文件失败: + error) // file.status = FileStatus.error // reject() // }) }) }, /** * 检查分片上传进度 */ checkChunkUploadProgress(item) { return p => { item.progress = parseInt(String((p.loaded / p.total) * 100)) this.updateChunkUploadStatus(item) } }, updateChunkUploadStatus(item) { let status = FileStatus.uploading let progressStatus = normal if (item.progress >= 100) { status = FileStatus.success progressStatus = success } let chunkIndex = item.chunkNumber - 1 let currentChunk = this.uploadFileList[currentFileIndex].chunkList[chunkIndex] // 修改状态 currentChunk.status = status currentChunk.progressStatus = progressStatus // 更新状态 this.$set(this.uploadFileList[currentFileIndex].chunkList, chunkIndex, currentChunk) // 获取文件上传进度 this.getCurrentFileProgress() }, getCurrentFileProgress() { const currentFile = this.uploadFileList[currentFileIndex] if (!currentFile || !currentFile.chunkList) { return } const chunkList = currentFile.chunkList const uploadedSize = chunkList.map((item) => item.chunk.file.size * item.progress).reduce((acc, cur) => acc + cur) // 计算方式:已上传大小 / 文件总大小 let progress = parseInt((uploadedSize / currentFile.size).toFixed(2)) currentFile.uploadProgress = progress this.$set(this.uploadFileList, currentFile) }, }, filters: { transformByte(size) { if (!size) { return 0B } const unitSize = 1024 if (size < unitSize) { return size + B } // KB if (size < Math.pow(unitSize, 2)) { return (size / unitSize).toFixed(2) + K; } // MB if (size < Math.pow(unitSize, 3)) { return (size / Math.pow(unitSize, 2)).toFixed(2) + MB } // GB if (size < Math.pow(unitSize, 4)) { return (size / Math.pow(unitSize, 3)).toFixed(2) + GB; } // TB return (size / Math.pow(unitSize, 4)).toFixed(2) + TB; } } } </script>

3.css

<style scoped lang="less"> .container { width: 600px; margin: 0 auto; } .file-list-wrapper { margin-top: 20px; } h2 { text-align: center; } .file-info-item { margin: 0 10px; } .upload-file-item { display: flex; } .file-progress { display: flex; align-items: center; } .file-progress-value { width: 150px; } .file-name { width: 190px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-size { width: 100px; } .uploader-example { width: 880px; padding: 15px; margin: 40px auto 0; font-size: 12px; box-shadow: 0 0 10px rgba(0, 0, 0, .4); } .uploader-example .uploader-btn { margin-right: 4px; } .uploader-example .uploader-list { max-height: 440px; overflow: auto; overflow-x: hidden; overflow-y: auto; } </style>

4.upload.js

import request from @/utils/request //上传信息 export function uploadScreenshot(data){ return request({ url:upload/multipart/uploadScreenshot, method:post, data }) } //上传信息 export function uploadFileInfo(data){ return request({ url:upload/multipart/uploadFileInfo, method:post, data }) } // 上传校验 export function checkUpload(MD5) { return request({ url: `upload/multipart/check?md5=${MD5}`, method: get, }) }; // 初始化上传 export function initUpload(data) { return request({ url: `upload/multipart/init`, method: post, data }) }; // 初始化上传 export function mergeUpload(data) { return request({ url: `upload/multipart/merge`, method: post, data }) };

5.request.js

import axios from axios import { getToken } from @/utils/CookiesSet //这个是获取token值,获取即可 //import Qs from qs //如果需要转换 // 创建 axios 实例 const service = axios.create({ baseURL: "/api", // 环境的不同                ,对应不同的baseURL // transformRequest: [function(data) { // return Qs.stringify(data) // }], //timeout: 5000 // 请求超时时间 }) //request请求拦截 service.interceptors.request.use( config => { var token=getToken() if (token) { config.headers.token = token // 让每个请求携带自定义token 请根据实际情况自行修改 } return config; }, error => { // do something with request error return Promise.reject(error) } ) //响应拦截 service.interceptors.response.use( response => { const res = response if (res.data.status !== 200) { //code返回参数根据实际后端返回参数 } return res }, error => { //这里还可以根据实际情况增加一些功能 return Promise.reject(error) } ) export default service

二.后端代码

后端使用的是springboot                       ,使用之前要启动minio       ,redis                ,否则文件上传会出现异常                      。这里我都是使用windows版的

1.controller,文件上传接口

package com.xy.controller; import com.xy.entity.FileInfo; import com.xy.entity.FileUploadInfo; import com.xy.service.UploadService; import com.xy.service.VideoFileInfoService; import com.xy.util.MinioUtils; import com.xy.util.ResponseResult; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import static com.xy.util.ResponseResult.error; import static com.xy.util.ResponseResult.success; import static com.xy.util.ResultCode.ACCESS_PARAMETER_INVALID; /** * minio上传流程 * * 1.检查数据库中是否存在上传文件 * * 2.根据文件信息初始化                       ,获取分片预签名url地址       ,前端根据url地址上传文件 * * 3.上传完成后        ,将分片上传的文件进行合并 * * 4.保存文件信息到数据库 */ @RestController @Slf4j public class FileMinioController { @Resource private UploadService uploadService; @Resource private VideoFileInfoService videoFileInfoService; @Resource private MinioUtils minioUtils; /** * 校验文件是否存在 * * @param md5 String * @return ResponseResult<Object> */ @GetMapping("/multipart/check") public ResponseResult checkFileUploadedByMd5(@RequestParam("md5") String md5) { log.info("REST: 通过查询 <{}> 文件是否存在       、是否进行断点续传", md5); if (StringUtils.isEmpty(md5)) { log.error("查询文件是否存在                、入参无效"); return error(ACCESS_PARAMETER_INVALID); } return uploadService.getByFileSha256(md5); } /** * 分片初始化 * * @param fileUploadInfo 文件信息 * @return ResponseResult<Object> */ @PostMapping("/multipart/init") public ResponseResult initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) { log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo); return uploadService.initMultiPartUpload(fileUploadInfo); } /** * 完成上传 * * @param fileUploadInfo 文件信息 * @return ResponseResult<Object> */ @PostMapping("/multipart/merge") public ResponseResult completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) { log.info("REST: 通过 {} 合并上传任务", fileUploadInfo); //Map<String, Object> resMap = new HashMap<>(); //合并文件 boolean result = uploadService.mergeMultipartUpload(fileUploadInfo); //获取上传文件地址 if(result){ String fliePath = uploadService.getFliePath(fileUploadInfo.getFileType().toLowerCase(), fileUploadInfo.getFileName()); return success(fliePath); } return error(); } /** * 保存文件信息到数据库 * @param fileInfo 文件信息 * @return */ @PostMapping("/multipart/uploadFileInfo") public ResponseResult uploadFileInfo(@RequestBody FileInfo fileInfo){ log.info("REST: 上传文件信息 <{}> ", fileInfo); if(fileInfo ==null){ return error(ACCESS_PARAMETER_INVALID); }else{ FileInfo insert = videoFileInfoService.insert(fileInfo); } return success(); } @PostMapping("/multipart/uploadScreenshot") public ResponseResult uploaduploadScreenshot(@RequestPart("photos") MultipartFile[] photos, @RequestParam("buckName") String buckName){ log.info("REST: 上传文件信息 <{}> ", photos); for (MultipartFile photo : photos) { if (!photo.isEmpty()) { uploadService.upload(photo,buckName); } } return success(); } @RequestMapping("/createBucket") public void createBucket(@RequestParam("bucketName")String bucketName){ String bucket = minioUtils.createBucket(bucketName); } }

2.UploadService

package com.xy.service; import com.xy.entity.FileUploadInfo; import com.xy.util.ResponseResult; import org.springframework.web.multipart.MultipartFile; public interface UploadService { /** * 分片上传初始化 * * @param fileUploadInfo * @return Map<String, Object> */ ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo); /** * 完成分片上传 * * @param fileUploadInfo * @return boolean */ boolean mergeMultipartUpload(FileUploadInfo fileUploadInfo); /** * 通过 sha256 获取已上传的数据 * @param sha256 String * @return Mono<Map<String, Object>> */ ResponseResult<Object> getByFileSha256(String sha256); /** * 获取文件地址 * @param bucketName * @param fileName * */ String getFliePath(String bucketName, String fileName); /** * 单文件上传 * @param file * @param bucketName * @return */ String upload(MultipartFile file, String bucketName); }

3.UploadServiceImpl

package com.xy.service.impl; import com.alibaba.fastjson.JSONObject; import com.xy.entity.FileUploadInfo; import com.xy.service.UploadService; import com.xy.util.MinioUtils; import com.xy.util.RedisRepo; import com.xy.util.ResponseResult; import com.xy.util.ResultCode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; @Slf4j @Service public class UploadServiceImpl implements UploadService { @Resource private MinioUtils fileService; @Resource private RedisRepo redisRepo; /** * 通过 sha256 获取已上传的数据(断点续传) * * @param sha256 String * @return Mono<Map < String, Object>> */ @Override public ResponseResult<Object> getByFileSha256(String sha256) { log.info("tip message: 通过 <{}> 查询数据是否存在", sha256); // 获取文件名称和id String value = redisRepo.get(sha256); FileUploadInfo fileUploadInfo = null; if (value != null) { fileUploadInfo = JSONObject.parseObject(value, FileUploadInfo.class); } if (fileUploadInfo == null) { // 返回数据不存在 log.error("error message: 文件数据不存在"); return ResponseResult.error(ResultCode.FOUND); } // 获取桶名称 String bucketName = fileService.getBucketName(fileUploadInfo.getFileType()); return fileService.getByFileSha256(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName); } /** * 文件分片上传 * * @param fileUploadInfo * @return Mono<Map < String, Object>> */ @Override public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo) { log.info("tip message: 通过 <{}> 开始初始化<分片上传>任务", fileUploadInfo); // 获取桶 String bucketName = fileService.getBucketName(fileUploadInfo.getFileType()); // 单文件上传可拆分                       ,这里只做演示               ,可直接上传完成 if (fileUploadInfo.getPartCount() == 1) { log.info("tip message: 当前分片数量 <{}> 进行单文件上传", fileUploadInfo.getPartCount()); return fileService.getUploadObjectUrl(fileUploadInfo.getFileName(), bucketName); } // 分片上传 else { log.info("tip message: 当前分片数量 <{}> 进行分片上传", fileUploadInfo.getPartCount()); return fileService.initMultiPartUpload(fileUploadInfo, fileUploadInfo.getFileName(), fileUploadInfo.getPartCount(), fileUploadInfo.getContentType(), bucketName); } } /** * 文件合并 * * @param * @return boolean */ @Override public boolean mergeMultipartUpload(FileUploadInfo fileUploadInfo) { log.info("tip message: 通过 <{}> 开始合并<分片上传>任务", fileUploadInfo); // 获取桶名称 String bucketName = fileService.getBucketName(fileUploadInfo.getFileType()); return fileService.mergeMultipartUpload(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName); } @Override public String getFliePath(String bucketName, String fileName) { return fileService.getFliePath(bucketName, fileName); } @Override public String upload(MultipartFile file, String bucketName) { fileService.upload(file, bucketName); return getFliePath(bucketName, file.getName()); } }

4.MinioUtils

package com.xy.util; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.HashMultimap; import com.xy.config.CustomMinioClient; import com.xy.entity.FileUploadInfo; import io.minio.*; import io.minio.errors.*; import io.minio.http.Method; import io.minio.messages.Part; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.xy.util.ResultCode.DATA_NOT_EXISTS; import static com.xy.util.ResultCode.UPLOAD_FILE_FAILED; @Slf4j @Component public class MinioUtils { @Value(value = "${minio.endpoint}") private String endpoint; @Value(value = "${minio.accesskey}") private String accesskey; @Value(value = "${minio.secretkey}") private String secretkey; @Resource private RedisRepo redisRepo; private CustomMinioClient customMinioClient; //初始化配置文件 private Properties SysLocalPropObject = new Properties(); /** * 用spring的自动注入会注入失败 */ @PostConstruct public void init() { MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accesskey, secretkey) .build(); customMinioClient = new CustomMinioClient(minioClient); } /** * 单文件签名上传 * * @param objectName 文件全路径名称 * @param bucketName 桶名称 * @return / */ public ResponseResult<Object> getUploadObjectUrl(String objectName, String bucketName) { try { log.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName); Map<String, Object> resMap = new HashMap<>(); List<String> partList = new ArrayList<>(); String url = customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucketName) .object(objectName) .expiry(1, TimeUnit.DAYS) .build()); log.info("tip message: 单个文件上传                       、成功"); partList.add(url); resMap.put("uploadId", "SingleFileUpload"); resMap.put("urlList", partList); return ResponseResult.success(resMap); } catch (Exception e) { log.error("error message: 单个文件上传失败       、原因:", e); // 返回 文件上传失败 return ResponseResult.error(UPLOAD_FILE_FAILED); } } /** * 初始化分片上传 * * @param fileUploadInfo * @param objectName 文件全路径名称 * @param partCount 分片数量 * @param contentType 类型        ,如果类型使用默认流会导致无法预览 * @param bucketName 桶名称 * @return Mono<Map < String, Object>> */ public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int partCount, String contentType, String bucketName) { log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName); Map<String, Object> resMap = new HashMap<>(); try { if (CharSequenceUtil.isBlank(contentType)) { contentType = "application/octet-stream"; } HashMultimap<String, String> headers = HashMultimap.create(); headers.put("Content-Type", contentType); //获取uploadId String uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null); resMap.put("uploadId", uploadId); fileUploadInfo.setUploadId(uploadId); //redis保存文件信息 redisRepo.saveTimeout(fileUploadInfo.getFileMd5(), JSONObject.toJSONString(fileUploadInfo), 30, TimeUnit.MINUTES); List<String> partList = new ArrayList<>(); Map<String, String> reqParams = new HashMap<>(); reqParams.put("uploadId", uploadId); for (int i = 1; i <= partCount; i++) { reqParams.put("partNumber", String.valueOf(i)); String uploadUrl = customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucketName) .object(objectName) .expiry(1, TimeUnit.DAYS) .extraQueryParams(reqParams) .build()); partList.add(uploadUrl); } log.info("tip message: 文件初始化<分片上传>        、成功"); resMap.put("urlList", partList); return ResponseResult.success(resMap); } catch (Exception e) { log.error("error message: 初始化分片上传失败                       、原因:", e); // 返回 文件上传失败 return ResponseResult.error(UPLOAD_FILE_FAILED); } } /** * 分片上传完后合并 * * @param objectName 文件全路径名称 * @param uploadId 返回的uploadId * @param bucketName 桶名称 * @return boolean */ public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) { try { log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName); //目前仅做了最大1000分片 Part[] parts = new Part[1000]; // 查询上传后的分片数据 ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); int partNumber = 1; for (Part part : partResult.result().partList()) { parts[partNumber - 1] = new Part(partNumber, part.etag()); partNumber++; } // 合并分片 customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null); } catch (Exception e) { log.error("error message: 合并失败               、原因:", e); return false; } return true; } /** * 通过 sha256 获取上传中的分片信息 * * @param objectName 文件全路径名称 * @param uploadId 返回的uploadId * @param bucketName 桶名称 * @return Mono<Map < String, Object>> */ public ResponseResult<Object> getByFileSha256(String objectName, String uploadId, String bucketName) { log.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName); //Map<String, Object> resMap = new HashMap<>(); try { // 查询上传后的分片数据 ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); List<Integer> collect = partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList()); // resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode()); // resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg()); // resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), collect); return ResponseResult.uploading(collect); } catch (Exception e) { log.error("error message: 查询上传后的分片信息失败        、原因:", e); return ResponseResult.error(DATA_NOT_EXISTS); } } /** * 获取文件下载地址 * * @param bucketName 桶名称 * @param fileName 文件名 * @return */ public String getFliePath(String bucketName, String fileName) { return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径 } /** * 创建一个桶 * * @return */ public String createBucket(String bucketName) { try { BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); //如果桶存在 if (customMinioClient.bucketExists(bucketExistsArgs)) { return bucketName; } MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); customMinioClient.makeBucket(makeBucketArgs); return bucketName; } catch (Exception e) { log.error("创建桶失败:{}", e.getMessage()); throw new RuntimeException(e); } } /** * 根据文件类型获取minio桶名称 * * @param fileType * @return */ public String getBucketName(String fileType) { try { //String bucketName = getProperty(fileType.toLowerCase()); if (fileType != null && !fileType.equals("")) { //判断桶是否存在 String bucketName2 = createBucket(fileType.toLowerCase()); if (bucketName2 != null && !bucketName2.equals("")) { return bucketName2; }else{ return fileType; } } } catch (Exception e) { log.error("Error reading bucket name "); } return fileType; } /** * 读取配置文件 * * @param fileType * @return * @throws IOException */ private String getProperty(String fileType) throws IOException { Properties SysLocalPropObject = new Properties(); //判断桶关系配置文件是否为空 if (SysLocalPropObject.isEmpty()) { InputStream is = getClass().getResourceAsStream("/BucketRelation.properties"); SysLocalPropObject.load(is); is.close(); } return SysLocalPropObject.getProperty("bucket." + fileType); } /** * 文件上传 * * @param file 文件 * @return Boolean */ public String upload(MultipartFile file, String bucketName) { String originalFilename = file.getOriginalFilename(); if (StringUtils.isBlank(originalFilename)) { throw new RuntimeException(); } String objectName = file.getName(); try { PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName) .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build(); //文件名称相同会覆盖 customMinioClient.putObject(objectArgs); } catch (Exception e) { e.printStackTrace(); return null; } // 查看

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
atlassian软件(Atlassian Software Tools Development, Collaboration, Code Review | Atlassian) 腾讯的喜剧(腾讯视频笑掉腹肌的喜剧片推荐_腾讯视频高分喜剧片排行榜)