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

1,155 阅读13分钟

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

流程:

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

流程图:

效果图

  1.vue前端

  1. minio文件桶

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

 项目中使用到的类库:spark-md5axioselement-ui

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

npm install spark-md5 --S
​
npm install axios --S

1.template 

<template>
​
<div class="container">
​
<h2>上传示例</h2>
​
<h4> 文件上传任务数:{{ taskQueueRunningNum }} </h4>
​
<el-upload class="upload-demo" ref="upload" :on-remove="handleRemove" :on-change="handleFileChange"
​
:file-list="uploadFileList" :show-file-list="false" :auto-upload="false" multiple>
​
<template #trigger>
​
<el-button type="primary" plain>选择文件</el-button>
​
</template>
​
<el-button style="margin-left: 5px;" type="success" @click="handlerPlus"
​
:disabled="uploadDisabled">上传</el-button>
​
<el-button type="danger" @click="clearFileHandler">清空</el-button>
​
<el-button :type="isPaused ? 'success' : 'danger'" :disabled="taskQueueRunningNum == 0"
​
@click="continueOrPauseUpload">{{
​
isPaused ? '继续' : '暂停'
​
}}</el-button>
​
</el-upload>
​
<div class="file-list-wrapper">
​
<el-collapse>
​
<el-collapse-item v-for="(item, index) in uploadFileList" :key="index" :name="index">
​
<template #title>
​
<el-row style="width:800px " type="flex" align="middle">
​
<el-col :span="9">
​
<div class="file-name" :title="item.name">{{ item.name }}</div>
​
</el-col>
​
<el-col :span="3">
​
<div class="file-size">{{ transformByte(item.size) || item.size }}</div>
​
</el-col>
​
<el-col :span="6">
​
<el-progress :percentage="item.uploadProgress" />
​
</el-col>
​
<el-col :span="3">
​
<div class="file-size">{{ `${item.uploadSpeed ? item.uploadSpeed : 0} M/s` }}</div>
​
</el-col>
​
<el-col :span="3">
​
<div>
​
<el-tag v-if="item.status === '等待上传'" size="default" type="info">等待上传</el-tag>
​
<el-tag v-else-if="item.status === '校验MD5'" size="default" type="warning">校验MD5</el-tag>
​
<el-tag v-else-if="item.status === '正在创建序列'" size="default"type="warning">正在创建序列</el-tag>
​
<el-tag v-else-if="item.status === '正在上传'" size="default">正在上传</el-tag>
​
<el-tag v-else-if="item.status === '上传成功'" size="default" type="success">上传完成</el-tag>
​
<el-tag v-else size="default" type="danger">上传错误</el-tag>
​
</div>
​
</el-col>
​
</el-row>
​
</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="14" />
​
</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 { reactive } from 'vue';
​
import { checkUpload, initUpload, mergeUpload, fileIsExits } from "./upload";
​
import SparkMD5 from 'spark-md5'
​
import axios from 'axios'
​
const chunkSize = 10 * 1024 * 1024
​
const CancelToken = axios.CancelToken;
​
let source = CancelToken.source();
​
const FileStatus = {
​
wait: '等待上传',
​
getMd5: '校验MD5',
​
chip: '正在创建序列',
​
uploading: '正在上传',
​
success: '上传成功',
​
error: '上传错误'
​
}
​
export default {
​
data() {
​
return {
​
changeDisabled: false,
​
uploadDisabled: false,
​
currentFileIndex: 0,
​
maxConcurrency: 3,
​
uploadIdInfoList: reactive([]),
​
uploadFileList: reactive([]),
​
isPaused: false,
​
taskQueue: null,
​
}
​
},
​
computed: {
​
taskQueuePaused() {
​
return this.taskQueue ? this.taskQueue.isEmpty() : true
​
},
​
taskQueueRunningNum() {
​
return this.taskQueue ? this.taskQueue.isRunning() : 0
​
},
​
},
​
created() {
​
window.mydata = this
​
},
​
methods: {
​
async handlerPlus() {
​
this.taskQueue = new TaskQueue(this.maxConcurrency);
​
for (let i = 0; i < this.uploadFileList.length; i++) {
​
let file = this.uploadFileList[i]
​
this.taskQueue.push({
​
name: file.name,
​
task: () => this.handler(file)
​
});
​
}
​
},
​
async continueOrPauseUpload() {
​
const self = this;
​
if (self.isPaused) {
​
self.isPaused = false
​
let pausedFileList = self.uploadFileList.filter(item => item.uploadProgress < 100 && item.chunkList.length > 0);
​
console.log("执行未完成的文件-->", pausedFileList)
​
for (let i = 0; i < pausedFileList.length; i++) {
​
let file = pausedFileList[i]
​
self.taskQueue.pushPauseQueue({
​
name: file.name,
​
task: () => self.handler(file)
​
});
​
}
​
self.taskQueue.resume()
​
} else {
​
try {
​
self.taskQueue.pause();
​
source.cancel('中断上传!');source = CancelToken.source();
​
} catch (err) { }
​
self.isPaused = true
​
}
​
},
​
handler(currentFile) {
​
const self = this;
​
const paused = async () => {
​
await new Promise((resolve) => {
​
const interval = setInterval(() => {
​
if (!self.isPaused) {
​
clearInterval(interval);
​
resolve();
​
}
​
}, 1000);
​
});
​
}
​
paused()
​
if (self.uploadFileList.length === 0) {
​
self.$message.error('请先选择文件');
​
return;
​
}
​
if (!currentFile) {
​
self.uploadDisabled = false;
​
return;
​
}
​
self.uploadDisabled = true;
​
return new Promise(async (resolve, reject) => {
​
try {
​
if (currentFile.uploadProgress < 100 && currentFile.chunkList.length > 0) {
​
self.processUpload(currentFile)
​
resolve();
​
return
​
}
​
currentFile.status = FileStatus.getMd5;
​
const md5 = await new Promise((resolveMd5, rejectMd5) => {
​
self.getFileMd5(currentFile.raw, (md5, totalChunks) => {
​
resolveMd5(md5);
​
});
​
});
​
const checkResult = await self.checkFileUploadedByMd5(md5);
​
if (checkResult.code === 1) {
​
self.$message.success(`上传成功,文件地址:${checkResult.data.url}`);currentFile.status = FileStatus.success;currentFile.uploadProgress = 100;
​
resolve();
​
} else if (checkResult.code === 2) {
​
currentFile.chunkUploadedList = checkResult.data;
​
}
​
currentFile.status = FileStatus.chip;
​
const fileChunks = self.createFileChunk(currentFile.raw);
​
const fileName = self.getNewFileName(currentFile);
​
const fileType = self.fileSuffixTypeUtil(currentFile.name);
​
const uploadIdInfoResult = await self.getFileUploadUrls({
​
fileName,
​
fileSize: currentFile.size,
​
chunkSize: chunkSize,
​
partCount: fileChunks.length,
​
fileMd5: md5,
​
contentType: 'application/octet-stream',
​
fileType,
​
});
​
let uploadIdInfo = uploadIdInfoResult.data.data;
​
const uploadUrls = uploadIdInfo.urlList;currentFile.chunkList = fileChunks.map((chunkItem, index) => ({
​
chunkNumber: index + 1,
​
chunk: chunkItem,
​
uploadUrl: uploadUrls[index],
​
progress: 0,
​
status: '—',
​
}));uploadIdInfo.fileName = fileName;uploadIdInfo.fileType = fileType;uploadIdInfo.md5 = md5;currentFile.uploadIdInfo = uploadIdInfo;
​
await this.processUpload(currentFile);
​
resolve();
​
} catch (error) {
​
reject(error);
​
}
​
});
​
},
​
async processUpload(currentFile) {
​
const self = this;
​
let tempFileChunks = [];
​
currentFile.chunkList.forEach((item) => {
​
tempFileChunks.push(item);
​
});currentFile.status = FileStatus.uploading;tempFileChunks = self.processUploadChunkList(tempFileChunks);
​
console.log("删除已上传的分片-->", tempFileChunks);
​
await self.uploadChunkBase(tempFileChunks, currentFile);
​
self.mergeFiles(currentFile.uploadIdInfo, currentFile);
​
},
​
processUploadChunkList(chunkList) {
​
return chunkList.reduce((acc, chunkItem) => {
​
if (chunkItem.progress < 100) {
​
acc.push(chunkItem);
​
}
​
return acc;
​
}, []);
​
},
​
async uploadChunkBase(chunkList, currentFile) {
​
const self = this;
​
const startTime = Date.now();
​
async function uploadSingleChunk(chunk, currentFile, result, index) {
​
try {
​
if (self.isPaused) {
​
await new Promise(resolve => self.resumeCallback = resolve);
​
}
​
await axios.put(chunk.uploadUrl, chunk.chunk.file, {
​
onUploadProgress: self.checkChunkUploadProgress(chunk, currentFile),
​
headers: {
​
'Content-Type': 'application/octet-stream'
​
},
​
cancelToken: source.token,
​
});
​
const uploadTime = (Date.now() - startTime) / 1000;currentFile.uploadSpeed = (chunkSize / uploadTime / 1024 / 1024).toFixed(1);
​
result[index] = true;
​
return true;
​
} catch (error) {
​
console.log('上传失败');
​
return false;
​
}
​
}
​
const maxConcurrentRequests = 10;
​
const results = new Array(chunkList.length).fill(false);
​
const uploadPromises = chunkList.map((chunk, index) => {
​
return () => uploadSingleChunk(chunk, currentFile, results, index);
​
});
​
let i = 0;
​
while (i < Math.min(maxConcurrentRequests, uploadPromises.length)) {
​
const success = await uploadNextChunk();
​
if (success) {
​
i++;
​
}
​
}
​
async function uploadNextChunk() {
​
if (uploadPromises.length > 0) {
​
const uploadPromise = uploadPromises.shift();
​
const success = await uploadPromise();
​
if (success) {
​
if (uploadPromises.length > 0) {
​
return uploadNextChunk();
​
} else if (!results.includes(false)) {
​
console.log('所有请求处理完毕');
​
}
​
}
​
return success;
​
}
​
return false;
​
}
​
while (self.isPaused) {
​
await new Promise(resolve => {
​
self.pauseCallback = () => {
​
resolve();
​
if (!self.isPaused && i < maxConcurrentRequests && i < uploadPromises.length) {
​
void uploadNextChunk();
​
i++;
​
}
​
};
​
});
​
}
​
},
​
async mergeFiles(uploadIdInfo, currentFile) {
​
const self = this;
​
if (uploadIdInfo.uploadId === 'SingleFileUpload') {
​
currentFile.status = FileStatus.success;
​
} else {
​
const fileInfo = {
​
uploadId: uploadIdInfo.uploadId,
​
fileName: uploadIdInfo.fileName,
​
fileMd5: uploadIdInfo.md5,
​
fileType: uploadIdInfo.fileType,
​
};
​
try {
​
const mergeResult = await new Promise((resolve, reject) => {
​
mergeUpload(fileInfo).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 => {
​
reject(error);
​
});
​
});
​
if (!mergeResult.data) {
​
const fileIsExit = await fileIsExits(fileInfo);
​
if (fileIsExit && !fileIsExit.data.code) {
​
currentFile.status = FileStatus.error;
​
self.$message.error(mergeResult.error);
​
return;
​
}
​
}
​
currentFile.uploadSpeed = 0currentFile.status = FileStatus.success;
​
console.log('文件访问地址:' + mergeResult.data);
​
self.$message.success(`上传成功,文件地址:${mergeResult.data}`);
​
} catch (error) {
​
currentFile.status = FileStatus.error;
​
self.$message.error(error.message);
​
}
​
}
​
},
​
clearFileHandler() {
​
this.uploadFileList = []
​
this.uploadIdInfoList = []
​
},
​
handleFileChange(file, fileList) {
​
fileList.forEach((item) => {
​
if (this.uploadFileList.indexOf(item) == -1) {
​
item.chunkList = [];item.status = FileStatus.wait;item.progressStatus = 'warning';item.uploadProgress = 0;item.uploadSpeed = 0
​
this.uploadFileList.push(item);
​
}
​
})
​
this.uploadDisabled = false
​
},
​
handleRemove(file, fileList) {
​
this.uploadFileList = []
​
},
​
getNewFileName(file, md5) {
​
return new Date().getTime() + file.name
​
},
​
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)
​
const loadChunk = (start, end) => {
​
return new Promise((resolve, reject) => {
​
fileReader.onload = function (e) {
​
try {
​
resolve(e.target.result);
​
} catch (error) {
​
reject(error);
​
}
​
};fileReader.onerror = function () {
​
reject(new Error('读取Md5失败,文件读取错误'));
​
};
​
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
​
});
​
};
​
const calculateHash = async () => {
​
const spark = new SparkMD5.ArrayBuffer();
​
const firstChunk = await loadChunk(0, Math.min(chunkSize, file.size));
​
spark.append(firstChunk);
​
const lastChunkStart = Math.max(0, file.size - chunkSize);
​
const lastChunk = await loadChunk(lastChunkStart, file.size);
​
spark.append(lastChunk);
​
if (totalChunks % 2 === 0) {
​
const middleChunkIndex = totalChunks / 2;
​
const middleChunk1Start = (middleChunkIndex - 1) * chunkSize;
​
const middleChunk1 = await loadChunk(middleChunk1Start, middleChunk1Start + chunkSize);
​
spark.append(middleChunk1);
​
const middleChunk2Start = middleChunkIndex * chunkSize;
​
const middleChunk2 = await loadChunk(middleChunk2Start, middleChunk2Start + chunkSize);
​
spark.append(middleChunk2);
​
} else {
​
const middleChunkIndex = Math.floor(totalChunks / 2);
​
const middleChunkStart = middleChunkIndex * chunkSize;
​
const middleChunk = await loadChunk(middleChunkStart, middleChunkStart + chunkSize);
​
spark.append(middleChunk);
​
}
​
return spark.end();
​
};
​
calculateHash()
​
.then((hash) => {
​
callback(hash, totalChunks);
​
})
​
.catch((error) => {
​
console.error('获取Md5错误:', error);
​
});
​
},
​
createFileChunk(file, size = chunkSize) {
​
const chunks = Array.from({ length: Math.ceil(file.size / size) }, (_, i) => {
​
const start = i * size;
​
const end = Math.min(start + size, file.size);
​
return { file: file.slice(start, end) };
​
});
​
return chunks;
​
},
​
getFileUploadUrls(fileParam) {
​
return initUpload(fileParam)
​
},
​
async checkFileUploadedByMd5(md5) {
​
try {
​
const response = await checkUpload(md5)
​
console.log(response.data)
​
return response.data
​
} catch (error) {
​
console.error(error)
​
} finally {
​
}
​
},
​
checkChunkUploadProgress(item, currentFile) {
​
return p => {
​
item.progress = parseInt(String((p.loaded / p.total) * 100))
​
if (item.progress >= 100) {
​
item.status = FileStatus.success
​
item.progressStatus = 'success'
​
}
​
this.getCurrentFileProgress(currentFile)
​
}
​
},
​
getCurrentFileProgress(currentFile) {
​
if (!currentFile || !currentFile.chunkList) {
​
return;
​
}
​
const chunkList = currentFile.chunkList;
​
const uploadedSize = chunkList.reduce((acc, cur) => acc + cur.chunk.file.size * cur.progress, 0);
​
let progress = parseInt((uploadedSize / currentFile.size).toFixed(2));currentFile.uploadProgress = progress;
​
},
​
fileSuffixTypeUtil(filename) {
​
const lastDotIndex = filename.lastIndexOf('.');
​
if (lastDotIndex === -1) {
​
return '';
​
}
​
return filename.slice(lastDotIndex + 1);
​
},
​
transformByte(size) {
​
const units = ['B', 'K', 'M', 'G', 'T'];
​
if (!size) return '0B';
​
let index = 0;
​
while (size >= 1024 && index < units.length - 1) {
​
size /= 1024;
​
index++;
​
}
​
return `${size.toFixed(2)}${units[index]}`;
​
},
​
},
​
}
​
class TaskQueue {
​
constructor(concurrency) {
​
this.concurrency = concurrency;this.running = 0;this.queue = [];this.paused = false;this.pauseQueue = [];
​
}
​
push(task) {
​
this.queue.push(task);
​
this.next();
​
}
​
pushPauseQueue(task) {
​
this.pauseQueue.push(task);
​
}
​
async next() {
​
while (this.running < this.concurrency && (this.queue.length > 0 || this.pauseQueue.length > 0)) {
​
if (!this.paused) {
​
const taskObj = this.pauseQueue.length > 0 ? this.pauseQueue.shift() : this.queue.shift();
​
const { name, task } = taskObj;
​
this.running++;
​
try {
​
console.log('正在执行队列任务', name);
​
await task();
​
} catch (error) {
​
console.error(error);
​
}
​
this.running--;
​
} else {
​
break;
​
}
​
}
​
}
​
pause() {
​
this.paused = true;this.running = 0;
​
}
​
resume() {
​
this.paused = false;
​
this.next();
​
}
​
setConcurrency(concurrency) {
​
this.concurrency = concurrency;
​
}
​
isPaused() {
​
return this.paused;
​
}
​
isEmpty() {
​
return this.queue.length === 0 && this.pauseQueue.length === 0;
​
}
​
isRunning() {
​
return this.running;
​
}
​
}
​
</script>

3.css

<style scoped>.container {
​
width: 800px;
​
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: 300px;
​
white-space: nowrap;
​
overflow: hidden;
​
text-overflow: ellipsis;
​
}
​
.file-size {
​
width: 100px;
​
}
​
</style>

4.upload.js

import request from '@/utils/request'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
​
})
​
};
​
export function fileIsExits(data) {
​
return request({
​
url: `upload/multipart/fileIsExits`,
​
method: 'post',
​
data
​
})
​
};

5.request.js

import axios from 'axios'const service = axios.create({
​
baseURL: "/api",
​
})
​
service.interceptors.request.use(
​
config => {
​
return config;
​
},
​
error => {
​
return Promise.reject(error)
​
}
​
)
​
service.interceptors.response.use(
​
response => {
​
const res = response
​
return res
​
},
​
error => {
​
return Promise.reject(error)
​
}
​
)
​
export default service

二.后端代码

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

1.controller,文件上传接口

package com.xy.fileservice.controller;
​
import com.xy.fileservice.entity.FileUploadInfo;
​
import com.xy.fileservice.service.UploadService;
​
import com.xy.fileservice.util.MinioUtils;
​
import com.xy.fileservice.util.ResponseResult;
​
import jakarta.annotation.Resource;
​
import lombok.extern.slf4j.Slf4j;
​
import org.springframework.web.bind.annotation.*;
​
@Slf4j
​
@RestController
​
@RequestMapping("/upload")
​
public class FileMinioController {
​
@Resource
​
private UploadService uploadService;
​
@Resource
​
private MinioUtils minioUtils;
​
@GetMapping("/multipart/check")
​
public ResponseResult checkFileUploadedByMd5(@RequestParam("md5") String md5) {
​
log.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传", md5);
​
return uploadService.getByFileMd5(md5);
​
}
​
@PostMapping("/multipart/init")
​
public ResponseResult initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
​
log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
​
return uploadService.initMultiPartUpload(fileUploadInfo);
​
}
​
@PostMapping("/multipart/merge")
​
public ResponseResult completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
​
log.info("REST: 通过 <{}> 合并上传任务", fileUploadInfo);
​
return uploadService.mergeMultipartUpload(fileUploadInfo);
​
}
​
@PostMapping("/multipart/fileIsExits")
​
public ResponseResult fileIsExits(@RequestBody FileUploadInfo fileUploadInfo) {
​
log.info("REST: 通过 <{}> 判断文件是否存在", fileUploadInfo);
​
return uploadService.fileIsExits(fileUploadInfo);
​
}
​
@RequestMapping("/createBucket")
​
public void createBucket(@RequestParam("bucketName")String bucketName){
​
String bucket = minioUtils.createBucket(bucketName);
​
}
​
}

2.UploadService

package com.xy.fileservice.service;
​
import com.xy.fileservice.entity.FileUploadInfo;
​
import com.xy.fileservice.util.ResponseResult;
​
import org.springframework.web.multipart.MultipartFile;
​
public interface UploadService {
​
ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo);
​
ResponseResult<Object> mergeMultipartUpload(FileUploadInfo fileUploadInfo);
​
ResponseResult<Object> getByFileMd5(String sha256);
​
String getFilePath(String bucketName, String fileName);
​
String upload(MultipartFile file, String bucketName);
​
ResponseResult fileIsExits(FileUploadInfo fileUploadInfo);
​
}

3.UploadServiceImpl

package com.xy.fileservice.service.impl;
​
import com.alibaba.fastjson.JSONObject;
​
import com.xy.fileservice.entity.FileUploadInfo;
​
import com.xy.fileservice.service.UploadService;
​
import com.xy.fileservice.util.MinioUtils;
​
import com.xy.fileservice.util.RedisRepo;
​
import com.xy.fileservice.util.ResponseResult;
​
import com.xy.fileservice.util.ResultCode;
​
import jakarta.annotation.Resource;
​
import lombok.extern.slf4j.Slf4j;
​
import org.springframework.stereotype.Service;
​
import org.springframework.util.StringUtils;
​
import org.springframework.web.multipart.MultipartFile;
​
import java.util.Objects;
​
import static com.xy.fileservice.util.ResultCode.ACCESS_PARAMETER_INVALID;
​
@Slf4j@Servicepublic class UploadServiceImpl implements UploadService {
​
@Resourceprivate MinioUtils fileService;
​
@Resourceprivate RedisRepo redisRepo;
​
@Overridepublic ResponseResult<Object> getByFileMd5(String md5) {
​
if (StringUtils.hasText(md5)) {
​
log.error("查询文件是否存在、入参无效");
​
return ResponseResult.error(ACCESS_PARAMETER_INVALID);
​
}
​
log.info("tip message: 通过 <{}> 查询数据是否存在", md5);
​
String value = redisRepo.get(md5);
​
FileUploadInfo fileUploadInfo = null;
​
if (StringUtils.hasText(value)) {
​
fileUploadInfo = JSONObject.parseObject(value, FileUploadInfo.class);
​
}
​
if (Objects.isNull(fileUploadInfo)) {
​
log.error("error message: 文件数据不存在");
​
return ResponseResult.error(ResultCode.FOUND);
​
}
​
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
​
return fileService.getByFileMd5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName);
​
}
​
@Overridepublic 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);
​
}
​
}
​
@Overridepublic ResponseResult mergeMultipartUpload(FileUploadInfo fileUploadInfo) {
​
log.info("tip message: 通过 <{}> 开始合并<分片上传>任务", fileUploadInfo);
​
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
​
boolean result = fileService.mergeMultipartUpload(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName);
​
if(result){
​
String filePath = fileService.getFilePath(fileUploadInfo.getFileType().toLowerCase(), fileUploadInfo.getFileName());
​
return ResponseResult.success(filePath);
​
}
​
log.error("error message: 文件合并异常");
​
return ResponseResult.error();
​
}
​
@Overridepublic String getFilePath(String bucketName, String fileName) {
​
return fileService.getFilePath(bucketName, fileName);
​
}
​
@Overridepublic String upload(MultipartFile file, String bucketName) {
​
fileService.upload(file, bucketName);
​
return getFilePath(bucketName, file.getName());
​
}
​
public ResponseResult fileIsExits(FileUploadInfo fileUploadInfo){
​
boolean b = fileService.doesObjectExist(fileUploadInfo.getFileType(), fileUploadInfo.getFileName());
​
if(b){
​
return ResponseResult.success();
​
}
​
return ResponseResult.error();
​
}
​
}

4.MinioUtils

package com.xy.fileservice.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.fileservice.config.CustomMinioClient;
​
import com.xy.fileservice.entity.FileUploadInfo;
​
import io.minio.*;
​
import io.minio.errors.*;
​
import io.minio.http.Method;
​
import io.minio.messages.Part;
​
import jakarta.annotation.PostConstruct;
​
import jakarta.annotation.Resource;
​
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 java.io.IOException;
​
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.fileservice.util.ResultCode.DATA_NOT_EXISTS;
​
import static com.xy.fileservice.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;
​
@PostConstruct
​
public void init() {
​
MinioClient minioClient = MinioClient.builder()
​
.endpoint(endpoint)
​
.credentials(accesskey, secretkey)
​
.build();customMinioClient = new CustomMinioClient(minioClient);
​
}
​
public ResponseResult<Object> getUploadObjectUrl(String objectName, String bucketName) {
​
log.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName);
​
try {
​
String url = getPresidedObjectUrl(bucketName, objectName);
​
Map<String, Object> resMap = new HashMap<>();
​
resMap.put("uploadId", "SingleFileUpload");
​
resMap.put("urlList", Collections.singletonList(url));
​
return ResponseResult.success(resMap);
​
} catch (Exception e) {
​
log.error("error message: 初始化分片上传失败、原因:", e);
​
return ResponseResult.error(UPLOAD_FILE_FAILED);
​
}
​
}
​
public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int partCount, String contentType, String bucketName) {
​
log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName);
​
try {
​
String uploadId = getUploadId(bucketName, objectName, contentType);
​
fileUploadInfo.setUploadId(uploadId);
​
redisRepo.saveTimeout(fileUploadInfo.getFileMd5(), JSONObject.toJSONString(fileUploadInfo), 30, TimeUnit.MINUTES);
​
List<String> partList = getPartUploadUrls(uploadId, partCount, bucketName, objectName);
​
Map<String, Object> resMap = new HashMap<>();
​
resMap.put("uploadId", uploadId);
​
resMap.put("urlList", partList);
​
log.info("tip message: 文件初始化<分片上传>、成功");
​
return ResponseResult.success(resMap);
​
} catch (Exception e) {
​
log.error("error message: 初始化分片上传失败、原因:", e);
​
return ResponseResult.error(UPLOAD_FILE_FAILED);
​
}
​
}
​
public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
​
try {
​
log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
​
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;
​
}
​
public ResponseResult<Object> getByFileMd5(String objectName, String uploadId, String bucketName) {
​
log.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
​
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());
​
return ResponseResult.uploading(collect);
​
} catch (Exception e) {
​
log.error("error message: 查询上传后的分片信息失败、原因:", e);
​
return ResponseResult.error(DATA_NOT_EXISTS);
​
}
​
}
​
public String getFilePath(String bucketName, String fileName) {
​
return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);
​
}
​
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);
​
}
​
}
​
public String getBucketName(String fileType) {
​
try {
​
if (StringUtils.isNotEmpty(fileType)) {
​
String bucketName = createBucket(fileType.toLowerCase());
​
if (StringUtils.isNotEmpty(bucketName)) {
​
return bucketName;
​
} else {
​
return fileType;
​
}
​
}
​
} catch (Exception e) {
​
log.error("Error reading bucket name ");
​
}
​
return fileType;
​
}
​
private String getPresidedObjectUrl(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
​
return customMinioClient.getPresignedObjectUrl(
​
GetPresignedObjectUrlArgs.builder()
​
.method(Method.PUT)
​
.bucket(bucketName)
​
.object(objectName)
​
.expiry(1, TimeUnit.DAYS)
​
.build());
​
}
​
private String getUploadId(String bucketName, String objectName, String contentType) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
​
if (CharSequenceUtil.isBlank(contentType)) {
​
contentType = "application/octet-stream";
​
}
​
HashMultimap<String, String> headers = HashMultimap.create();
​
headers.put("Content-Type", contentType);
​
return customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
​
}
​
private List<String> getPartUploadUrls(String uploadId, int partCount, String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
​
List<String> partList = new ArrayList<>();
​
for (int i = 1; i <= partCount; i++) {
​
Map<String, String> reqParams = new HashMap<>();
​
reqParams.put("uploadId", uploadId);
​
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);
​
}
​
return partList;
​
}
​
public boolean doesObjectExist(String bucketName, String objectName) {
​
boolean exist = true;
​
try {
​
customMinioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
​
} catch (Exception e) {
​
exist = false;
​
}
​
return exist;
​
}
​
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;
​
}
​
}

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 {
​
public CustomMinioClient(MinioClient client) {
​
super(client);
​
}
​
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();
​
}
​
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);
​
}
​
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;
​
@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;
​
}
​
@Beanpublic 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;
​
}
​
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;
​
}
​
}

8.ResultCode

package com.xy.util;
​
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 {
​
private String fileName;
​
private Double fileSize;
​
private String contentType;
​
private Integer partCount;
​
private String uploadId;
​
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 {
​
@Autowiredprivate 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>

13.中间件

为了方便中间件皆采用docker,使用时注意替换自己的本地目录

redis

地址:localhost:6379

docker run --name redis01 -p 6379:6379 -v D:\Docker-vm\folder\redis\data\redis.conf:/usr/local/etc/redis/redis.conf -d redis:5.0.14

minio

地址:localhost:9000

账号:minioadmin

密码:minioadmin

docker run -p 9000:9000 -p 9090:9090   --name minio      -d --restart=always      -e "MINIO_ACCESS_KEY=minioadmin"      -e "MINIO_SECRET_KEY=minioadmin"      -v D:\Docker-vm\folder\minio\data:/data      -v D:\Docker-vm\folder\minio\config:/root/.minio      minio/minio server      /data --console-address ":9090" -address ":9000"

2023.03.01

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

2023.12.20 

前端升级到了 vite + vue + element plus  后端升级到了 jdk17 + springboot3.0 

前端升级了框架, 增加了暂停上传功能, 做了大量的代码优化. 

tip: 暂停上传功能在文件体积较小的情况下可能会因为文件上传速度太快出现功能异常

2023.12.23

前端功能优化, 计算md5 采用分片抽样计算,优化了上传逻辑,增加了任务队列实现多文件同时上传,暂停功能也进行优化,修复了功能 . 同时增加了网速显示功能

tip: 网速显示不太准确,参考意义不大,网上没有较好的网速计算方式,大家如果看到欢迎留言

项目传送门: gitee

转载请注明出处