網(wǎng)上關(guān)于minio分片上傳的資料不太詳細(xì),缺斤少兩,所以我基于他們的代碼做了一些修改,demo能夠正常運行起來,但是偶爾也會發(fā)生一些小bug,不過這些都無傷大雅,最終目的是理解代碼背后的邏輯和流程
流程:
- 前端獲取生成文件MD5,發(fā)送至后臺判斷是否有該文件緩存,有信息終止上傳,無則開始進行文件分片? 。這里,我為了簡單方便實現(xiàn)便沒有使用數(shù)據(jù)庫,直接用redis存儲文件信息;
- 前端后端返回的結(jié)果進行分片,然后將文件分片的信息傳輸給后端,后端調(diào)用 minio 初始化,返回分片上傳地址和 uploadId;
- 前端則根據(jù)獲取的分片上傳地址直接通過axios上傳分片文件,不走后端;
- 上傳完成后,前端發(fā)送請求至后端,后端調(diào)用 minio 合并文件;
流程圖:
效果圖
? 1.vue前端
2. minio文件桶
一.前端vue代碼(代碼較多,我就分開貼)
?項目中使用到的類庫:spark-md5
、axios
、element-ui
;
spark-md5
?主要用來計算文件MD5,安裝命令:
npm install spark-md5 --S
npm install axios --S
? ?1.template?
<template>
<div class="container">
<h2>上傳示例</h2>
<h4> 文件上傳任務(wù)數(shù):{{ 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 ? '繼續(xù)' : '暫停'
}}</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 === '正在創(chuàng)建序列'" size="default"
type="warning">正在創(chuàng)建序列</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="狀態(tài)" 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
// 用于axios請求的取消
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
const FileStatus = {
wait: '等待上傳',
getMd5: '校驗MD5',
chip: '正在創(chuàng)建序列',
uploading: '正在上傳',
success: '上傳成功',
error: '上傳錯誤'
}
export default {
data() {
return {
changeDisabled: false,
uploadDisabled: false,
// 上傳并發(fā)數(shù)
currentFileIndex: 0,
maxConcurrency: 3,
uploadIdInfoList: reactive([]),
uploadFileList: reactive([]),
isPaused: false, // 暫停true 繼續(xù)false
taskQueue: null, // 上傳任務(wù)隊列
}
},
computed: {
taskQueuePaused() {
return this.taskQueue ? this.taskQueue.isEmpty() : true
},
taskQueueRunningNum() {
return this.taskQueue ? this.taskQueue.isRunning() : 0
},
},
created() {
window.mydata = this
},
methods: {
async handlerPlus() {
// 創(chuàng)建一個允許同時執(zhí)行3個任務(wù)的任務(wù)隊列
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)
}); // 將任務(wù)加入隊列
}
},
/**
* 暫停上傳文件
*/
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("執(zhí)行未完成的文件-->", pausedFileList)
for (let i = 0; i < pausedFileList.length; i++) {
let file = pausedFileList[i]
// 將任務(wù)加入隊列
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 {
// 判斷文件是否已經(jīng)進行分片
if (currentFile.uploadProgress < 100 && currentFile.chunkList.length > 0) {
self.processUpload(currentFile)
resolve();
return
}
// 更新上傳標(biāo)簽
currentFile.status = FileStatus.getMd5;
// 1. 計算文件MD5
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;
}
// 3. 正在創(chuàng)建分片
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) {
// 使用 reduce 過濾并生成新的數(shù)組
return chunkList.reduce((acc, chunkItem) => {
if (chunkItem.progress < 100) {
acc.push(chunkItem);
}
return acc;
}, []);
},
/**
* 上傳分片文件
* @param {*} chunkList
*/
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;
// 計算平均網(wǎng)速(字節(jié)/秒) chunkSize: 文件分片大小 uploadTime: 時間差
currentFile.uploadSpeed = (chunkSize / uploadTime / 1024 / 1024).toFixed(1);
result[index] = true;
return true;
} catch (error) {
console.log('上傳失敗');
//chunkList.push(chunk);
return false;
}
}
// 限制請求并發(fā)數(shù)量
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++;
}
};
});
}
},
/**
* 文件合并
* @param {*} uploadIdInfo
* @param {*} currentFileIndex
*/
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 = 0
currentFile.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 = []
},
/**
* 上傳文件列表
* @param {*} file
* @param {*} fileList
*/
handleFileChange(file, fileList) {
fileList.forEach((item) => {
// 去除重復(fù)文件
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
},
/**
* 移除文件列表
* @param {*} file
* @param {*} fileList
*/
handleRemove(file, fileList) {
this.uploadFileList = []
},
getNewFileName(file, md5) {
return new Date().getTime() + file.name
},
/**
* 分片讀取文件 MD5
* 取第一個和最后一個切片全部內(nèi)容; 再加上取中間的,分片個數(shù)為偶數(shù)個,取中間的兩個分片; 為奇數(shù)的取中間的一個分片; 來計算 hash
*/
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();
// 取第一個切片內(nèi)容
const firstChunk = await loadChunk(0, Math.min(chunkSize, file.size));
spark.append(firstChunk);
// 取最后一個切片內(nèi)容
const lastChunkStart = Math.max(0, file.size - chunkSize);
const lastChunk = await loadChunk(lastChunkStart, file.size);
spark.append(lastChunk);
// 取中間的內(nèi)容
if (totalChunks % 2 === 0) {
// 偶數(shù)個分片,取中間的兩個分片
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 {
// 奇數(shù)個分片,取中間的一個分片
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;
},
/**
* 根據(jù)文件信息獲取分片url
* @param {*} fileParam
*/
getFileUploadUrls(fileParam) {
return initUpload(fileParam)
},
/**
* 檢查文件上傳的md5,判斷是否上傳
* @param {*} md5
*/
async checkFileUploadedByMd5(md5) {
try {
const response = await checkUpload(md5)
console.log(response.data)
return response.data
} catch (error) {
console.error(error)
} finally {
// 無論是否發(fā)生異常,都會執(zhí)行
}
},
/**
* 檢查分片上傳進度
*/
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)
}
},
/**
* 獲取當(dāng)前文件進度
*/
getCurrentFileProgress(currentFile) {
//const currentFile = this.uploadFileList[currentFileIndex];
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); // 返回'.'后的字符串
},
// 字節(jié)轉(zhuǎn)標(biāo)準(zhǔn)單位
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; // 同時執(zhí)行的任務(wù)數(shù)量限制
this.running = 0; // 當(dāng)前正在執(zhí)行的任務(wù)數(shù)量
this.queue = []; // 任務(wù)隊列
this.paused = false; // 是否已暫停任務(wù)執(zhí)行
this.pauseQueue = []; // 暫停隊列
}
push(task) {
this.queue.push(task); // 將任務(wù)加入隊列
this.next(); // 嘗試執(zhí)行下一個任務(wù)
}
pushPauseQueue(task) {
this.pauseQueue.push(task); // 將任務(wù)加入暫停隊列
}
async next() {
while (this.running < this.concurrency && (this.queue.length > 0 || this.pauseQueue.length > 0)) {
if (!this.paused) { // 判斷是否已暫停任務(wù)執(zhí)行
const taskObj = this.pauseQueue.length > 0 ? this.pauseQueue.shift() : this.queue.shift(); // 優(yōu)先執(zhí)行暫停隊列中的任務(wù)
const { name, task } = taskObj; // 獲取任務(wù) id 和任務(wù)本身
this.running++; // 增加正在執(zhí)行的任務(wù)數(shù)量
try {
console.log('正在執(zhí)行隊列任務(wù)', name);
await task(); // 等待任務(wù)執(zhí)行完成
} catch (error) {
console.error(error);
}
this.running--; // 減少正在執(zhí)行的任務(wù)數(shù)量
} else {
break; // 若已暫停任務(wù)執(zhí)行,則退出循環(huán)
}
}
}
pause() {
this.paused = true; // 暫停任務(wù)執(zhí)行
this.running = 0;
}
resume() {
this.paused = false; // 繼續(xù)任務(wù)執(zhí)行
this.next(); // 嘗試執(zhí)行下一個任務(wù)
}
setConcurrency(concurrency) {
this.concurrency = concurrency; // 設(shè)置同時執(zhí)行的任務(wù)數(shù)量限制
}
isPaused() {
return this.paused; // 返回是否已暫停任務(wù)執(zhí)行
}
isEmpty() {
return this.queue.length === 0 && this.pauseQueue.length === 0; // 判斷任務(wù)隊列和暫停隊列是否都為空
}
isRunning() {
return this.running; // 返回同時執(zhí)行的任務(wù)數(shù)量
}
}
</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'
// 創(chuàng)建 axios 實例
const service = axios.create({
baseURL: "/api", // 環(huán)境的不同,對應(yīng)不同的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 請根據(jù)實際情況自行修改
// }
return config;
},
error => {
// do something with request error
return Promise.reject(error)
}
)
//響應(yīng)攔截
service.interceptors.response.use(
response => {
const res = response
return res
},
error => {
//這里還可以根據(jù)實際情況增加一些功能
return Promise.reject(error)
}
)
export default service
二.后端代碼
后端使用的是springboot ,使用之前要啟動minio,redis,否則文件上傳會出現(xiàn)異常。這里我都是使用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.*;
/**
* minio上傳流程
*
* 1.檢查數(shù)據(jù)庫中是否存在上傳文件
*
* 2.根據(jù)文件信息初始化,獲取分片預(yù)簽名url地址,前端根據(jù)url地址上傳文件
*
* 3.上傳完成后,將分片上傳的文件進行合并
*
* 4.保存文件信息到數(shù)據(jù)庫
*/
@Slf4j
@RestController
@RequestMapping("/upload")
public class FileMinioController {
@Resource
private UploadService uploadService;
@Resource
private MinioUtils minioUtils;
/**
* 校驗文件是否存在
*
* @param md5 String
* @return ResponseResult<Object>
*/
@GetMapping("/multipart/check")
public ResponseResult checkFileUploadedByMd5(@RequestParam("md5") String md5) {
log.info("REST: 通過查詢 <{}> 文件是否存在、是否進行斷點續(xù)傳", md5);
return uploadService.getByFileMd5(md5);
}
/**
* 分片初始化
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/init")
public ResponseResult initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通過 <{}> 初始化上傳任務(wù)", fileUploadInfo);
return uploadService.initMultiPartUpload(fileUploadInfo);
}
/**
* 完成上傳
*
* @param fileUploadInfo 文件信息
* @return ResponseResult<Object>
*/
@PostMapping("/multipart/merge")
public ResponseResult completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
log.info("REST: 通過 <{}> 合并上傳任務(wù)", 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 {
/**
* 分片上傳初始化
*
* @param fileUploadInfo
* @return Map<String, Object>
*/
ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo);
/**
* 完成分片上傳
*
* @param fileUploadInfo
* @return boolean
*/
ResponseResult<Object> mergeMultipartUpload(FileUploadInfo fileUploadInfo);
/**
* 通過 sha256 獲取已上傳的數(shù)據(jù)
* @param sha256 String
* @return Mono<Map<String, Object>>
*/
ResponseResult<Object> getByFileMd5(String sha256);
/**
* 獲取文件地址
* @param bucketName
* @param fileName
*
*/
String getFilePath(String bucketName, String fileName);
/**
* 單文件上傳
* @param file
* @param bucketName
* @return
*/
String upload(MultipartFile file, String bucketName);
/**
* 判斷文件是否存在
* @param fileUploadInfo
* @return
*/
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
@Service
public class UploadServiceImpl implements UploadService {
@Resource
private MinioUtils fileService;
@Resource
private RedisRepo redisRepo;
/**
* 通過 md5 獲取已上傳的數(shù)據(jù)(斷點續(xù)傳)
*
* @param md5 String
* @return Mono<Map < String, Object>>
*/
@Override
public ResponseResult<Object> getByFileMd5(String md5) {
if (StringUtils.hasText(md5)) {
log.error("查詢文件是否存在、入?yún)o效");
return ResponseResult.error(ACCESS_PARAMETER_INVALID);
}
log.info("tip message: 通過 <{}> 查詢數(shù)據(jù)是否存在", md5);
// 獲取文件名稱和id
String value = redisRepo.get(md5);
FileUploadInfo fileUploadInfo = null;
if (StringUtils.hasText(value)) {
fileUploadInfo = JSONObject.parseObject(value, FileUploadInfo.class);
}
if (Objects.isNull(fileUploadInfo)) {
// 返回數(shù)據(jù)不存在
log.error("error message: 文件數(shù)據(jù)不存在");
return ResponseResult.error(ResultCode.FOUND);
}
// 獲取桶名稱
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
return fileService.getByFileMd5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), bucketName);
}
/**
* 文件分片上傳
*
* @param fileUploadInfo
* @return Mono<Map < String, Object>>
*/
@Override
public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo) {
log.info("tip message: 通過 <{}> 開始初始化<分片上傳>任務(wù)", fileUploadInfo);
// 獲取文件桶名
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
// 單文件上傳可拆分,可直接上傳完成
if (fileUploadInfo.getPartCount() == 1) {
log.info("tip message: 當(dāng)前分片數(shù)量 <{}> 進行單文件上傳", fileUploadInfo.getPartCount());
// 獲取文件分片上傳的url
return fileService.getUploadObjectUrl(fileUploadInfo.getFileName(), bucketName);
}else {
log.info("tip message: 當(dāng)前分片數(shù)量 <{}> 進行分片上傳", fileUploadInfo.getPartCount());
// 獲取文件分片上傳的url
return fileService.initMultiPartUpload(fileUploadInfo, fileUploadInfo.getFileName(), fileUploadInfo.getPartCount(), fileUploadInfo.getContentType(), bucketName);
}
}
/**
* 文件合并
*
* @param
* @return boolean
*/
@Override
public ResponseResult mergeMultipartUpload(FileUploadInfo fileUploadInfo) {
log.info("tip message: 通過 <{}> 開始合并<分片上傳>任務(wù)", fileUploadInfo);
// 獲取桶名稱
String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());
// 獲取合并結(jié)果
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();
}
@Override
public String getFilePath(String bucketName, String fileName) {
return fileService.getFilePath(bucketName, fileName);
}
@Override
public 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;
/**
* 用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) {
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);
}
}
/**
* 文件分片上傳
*
* @param fileUploadInfo
* @param objectName 文件全路徑名稱
* @param partCount 分片數(shù)量
* @param contentType 類型,如果類型使用默認(rèn)流會導(dǎo)致無法預(yù)覽
* @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: 通過 <{}-{}-{}-{}> 開始初始化<分片上傳>數(shù)據(jù)", objectName, partCount, contentType, bucketName);
try {
String uploadId = getUploadId(bucketName, objectName, contentType);
fileUploadInfo.setUploadId(uploadId);
//redis保存文件信息
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);
}
}
/**
* 分片上傳完后合并
*
* @param objectName 文件全路徑名稱
* @param uploadId 返回的uploadId
* @param bucketName 桶名稱
* @return boolean
*/
public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
try {
log.info("tip message: 通過 <{}-{}-{}> 合并<分片上傳>數(shù)據(jù)", objectName, uploadId, bucketName);
//目前僅做了最大1000分片
Part[] parts = new Part[1000];
// 查詢上傳后的分片數(shù)據(jù)
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> getByFileMd5(String objectName, String uploadId, String bucketName) {
log.info("通過 <{}-{}-{}> 查詢<minio>上傳分片數(shù)據(jù)", objectName, uploadId, bucketName);
try {
// 查詢上傳后的分片數(shù)據(jù)
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);
}
}
/**
* 獲取文件下載地址
*
* @param bucketName 桶名稱
* @param fileName 文件名
* @return
*/
public String getFilePath(String bucketName, String fileName) {
return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件訪問路徑
}
/**
* 創(chuàng)建一個桶
*
* @return
*/
public String createBucket(String bucketName) {
try {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
//如果桶存在
if (customMinioClient.bucketExists(bucketExistsArgs)) {
return bucketName;
}
// 如果不存在則創(chuàng)建新文件桶
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
customMinioClient.makeBucket(makeBucketArgs);
return bucketName;
} catch (Exception e) {
log.error("創(chuàng)建桶失?。簕}", e.getMessage());
throw new RuntimeException(e);
}
}
/**
* 根據(jù)文件類型獲取minio桶名稱
*
* @param fileType
* @return
*/
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;
}
/**
* 單文件獲取上傳url
* @param bucketName
* @param objectName
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws InvalidResponseException
* @throws XmlParserException
* @throws InternalException
*/
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());
}
/**
* 獲取合并id
* @param bucketName
* @param objectName
* @param contentType
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws InternalException
*/
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);
}
/**
* 獲取文件分片urls
* @param uploadId
* @param partCount
* @param bucketName
* @param objectName
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws InvalidResponseException
* @throws XmlParserException
* @throws InternalException
*/
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;
}
/**
* 判斷文件是否存在
*
* @param bucketName 存儲桶
* @param objectName 對象
* @return true:存在
*/
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;
}
/**
* 文件上傳
*
* @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;
}
}
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);
}
/**
* 查詢當(dāng)前上傳后的分片信息
*
* @param bucketName String 桶名稱
* @param region String
* @param objectName String 文件名稱
* @param maxParts Integer 分片數(shù)量
* @param partNumberMarker Integer 分片起始值
* @param uploadId String 上傳的 uploadId
* @param extraHeaders Multimap<String, String>
* @param extraQueryParams Multimap<String, String>
* @return ListPartsResponse
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
}
6.CorsConfig
package com.xy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 全局跨域處理
* @author CV
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
接下來是返回信息工具類
7.ResponseResult
package com.xy.util;
import lombok.Data;
@Data
public class ResponseResult<T> {
private int code;
private String enMessage;
private String zhMessage;
private T data;
public ResponseResult() {
}
public ResponseResult(int code, String enMessage, String zhMessage) {
this.code = code;
this.enMessage = enMessage;
this.zhMessage = zhMessage;
}
/**
* 成功
*/
public static <T> ResponseResult<T> success() {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
return result;
}
/**
* 成功
*/
public static <T> ResponseResult<T> success(T data) {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
result.setData(data);
return result;
}
/**
* 失敗
*/
public static <T> ResponseResult <T> error() {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.FAIL.getCode());
result.setEnMessage(ResultCode.FAIL.getEnMessage());
result.setZhMessage(ResultCode.FAIL.getZhMessage());
return result;
}
/**
* 失敗
*/
public static <T> ResponseResult<T> error(T data) {
ResponseResult<T> result = new ResponseResult<T>();
result.setCode(ResultCode.FAIL.getCode());
result.setEnMessage(ResultCode.FAIL.getEnMessage());
result.setZhMessage(ResultCode.FAIL.getZhMessage());
result.setData(data);
return result;
}
/**
*
* @param data 數(shù)據(jù)
* @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,"服務(wù)器錯誤");
// public static ResponseResult<Void> NOT_FOUND = new ResponseResult<>(404,"未找到");
}
8.ResultCode
package com.xy.util;
/**
* http狀態(tài)碼枚舉類
*/
public enum ResultCode {
SUCCESS(1, "Success", "成功"),
UPLOADING(2, "Uploading", "上傳中"),
FAIL(-1, "Err", "失敗"),
DATABASE_OPERATION_FAILED(504, "數(shù)據(jù)庫操作失敗"),
CONTINUE(100, "Continue", "請繼續(xù)發(fā)送請求的剩余部分"),
SWITCHING_PROTOCOLS(101, "Switching Protocols", "協(xié)議切換"),
PROCESSING(102, "Processing", "請求將繼續(xù)執(zhí)行"),
CHECKPOINT(103, "Checkpoint", "可以預(yù)加載"),
OK(200, "OK", "請求已經(jīng)成功處理"),
CREATED(201, "Created", "請求已經(jīng)成功處理,并創(chuàng)建了資源"),
ACCEPTED(202, "Accepted", "請求已經(jīng)接受,等待執(zhí)行"),
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information", "請求已經(jīng)成功處理,但是信息不是原始的"),
NO_CONTENT(204, "No Content", "請求已經(jīng)成功處理,沒有內(nèi)容需要返回"),
RESET_CONTENT(205, "Reset Content", "請求已經(jīng)成功處理,請重置視圖"),
PARTIAL_CONTENT(206, "Partial Content", "部分Get請求已經(jīng)成功處理"),
MULTI_STATUS(207, "Multi-Status", "請求已經(jīng)成功處理,將返回XML消息體"),
ALREADY_REPORTED(208, "Already Reported", "請求已經(jīng)成功處理,一個DAV的綁定成員被前一個請求枚舉,并且沒有被再一次包括"),
IM_USED(226, "IM Used", "請求已經(jīng)成功處理,將響應(yīng)一個或者多個實例"),
MULTIPLE_CHOICES(300, "Multiple Choices", "提供可供選擇的回饋"),
MOVED_PERMANENTLY(301, "Moved Permanently", "請求的資源已經(jīng)永久轉(zhuǎn)移"),
FOUND(302, "Found", "請重新發(fā)送請求"),
SEE_OTHER(303, "See Other", "請以Get方式請求另一個URI"),
NOT_MODIFIED(304, "Not Modified", "資源未改變"),
USE_PROXY(305, "Use Proxy", "請通過Location域中的代理進行訪問"),
TEMPORARY_REDIRECT(307, "Temporary Redirect", "請求的資源臨時從不同的URI響應(yīng)請求"),
RESUME_INCOMPLETE(308, "Resume Incomplete", "請求的資源已經(jīng)永久轉(zhuǎn)移"),
BAD_REQUEST(400, "Bad Request", "請求錯誤,請修正請求"),
UNAUTHORIZED(401, "Unauthorized", "沒有被授權(quán)或者授權(quán)已經(jīng)失效"),
PAYMENT_REQUIRED(402, "Payment Required", "預(yù)留狀態(tài)"),
FORBIDDEN(403, "Forbidden", "請求被理解,但是拒絕執(zhí)行"),
NOT_FOUND(404, "Not Found", "資源未找到"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed", "請求方法不允許被執(zhí)行"),
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", "請求發(fā)送的實體太大"),
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long", "請求的URI超長"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type", "請求發(fā)送的實體類型不受支持"),
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable", "Range指定的范圍與當(dāng)前資源可用范圍不一致"),
EXPECTATION_FAILED(417, "Expectation Failed", "請求頭Expect中指定的預(yù)期內(nèi)容無法被服務(wù)器滿足"),
UNPROCESSABLE_ENTITY(422, "Unprocessable Entity", "請求格式正確,但是由于含有語義錯誤,無法響應(yīng)"),
LOCKED(423, "Locked", "當(dāng)前資源被鎖定"),
FAILED_DEPENDENCY(424, "Failed Dependency", "由于之前的請求發(fā)生錯誤,導(dǎo)致當(dāng)前請求失敗"),
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", "服務(wù)器內(nèi)部錯誤"),
NOT_IMPLEMENTED(501, "Not Implemented", "服務(wù)器不支持當(dāng)前請求的部分功能"),
BAD_GATEWAY(502, "Bad Gateway", "響應(yīng)無效"),
SERVICE_UNAVAILABLE(503, "Service Unavailable", "服務(wù)器維護或者過載,拒絕服務(wù)"),
GATEWAY_TIMEOUT(504, "Gateway Timeout", "上游服務(wù)器超時"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported", "不支持的HTTP版本"),
VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates", "服務(wù)器內(nèi)部配置錯誤"),
INSUFFICIENT_STORAGE(507, "Insufficient Storage", "服務(wù)器無法完成存儲請求所需的內(nèi)容"),
LOOP_DETECTED(508, "Loop Detected", "服務(wù)器處理請求時發(fā)現(xiàn)死循環(huán)"),
BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded", "服務(wù)器達到帶寬限制"),
NOT_EXTENDED(510, "Not Extended", "獲取資源所需的策略沒有被滿足"),
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required", "需要進行網(wǎng)絡(luò)授權(quán)"),
ACCESS_PARAMETER_INVALID(1001,"Invalid access parameter","訪問參數(shù)無效"),
UPLOAD_FILE_FAILED(1002,"File upload failure","文件上傳失敗"),
DATA_NOT_EXISTS(1003,"Data does not exist","數(shù)據(jù)不存在"),
;
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 = "分片數(shù)量不能為空")
private Integer partCount;
// @NotBlank(message = "uploadId 不能為空")
private String uploadId;
// 桶名稱
//private String bucketName;
//md5
private String fileMd5;
//文件類型
private String fileType;
public FileUploadInfo() {
}
}
10.RedisRepo
package com.xy.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisRepo {
@Autowired
private StringRedisTemplate redisTemplate;
public String get(String key) {
BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
return ops.get();
}
public void save(String key,String str){
BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
ops.set(str);
}
public void saveTimeout(String key, String value, long timeout, TimeUnit unit ){
redisTemplate.boundValueOps(key).setIfAbsent(value,timeout,unit);
}
public void delete(String key){
redisTemplate.delete(key);
}
public long expire(String key){
return redisTemplate.opsForValue().getOperations().getExpire(key);
}
}
11.yaml配置
minio: endpoint: http://localhost:9000 accesskey: minioadmin secretkey: minioadmin spring: redis: host: localhost port: 6379
12.pom配置
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.2</version>
</dependency>
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
本文僅介紹上傳流程的簡單實現(xiàn),很多功能未完善,如文件夾上傳、上傳暫停、停止等功能。代碼有何異?;蛘卟煌暾麣g迎在評論區(qū)留言???????
2023.12.20?
前端升級到了 vite + vue + element plus? 后端升級到了 jdk17 + springboot3.0?
前端升級了框架, 增加了暫停上傳功能, 做了大量的代碼優(yōu)化.?
tip: 暫停上傳功能在文件體積較小的情況下可能會因為文件上傳速度太快出現(xiàn)功能異常
2023.12.23
前端功能優(yōu)化, 計算md5 采用分片抽樣計算,優(yōu)化了上傳邏輯,增加了任務(wù)隊列實現(xiàn)多文件同時上傳,暫停功能也進行優(yōu)化,修復(fù)了功能 . 同時增加了網(wǎng)速顯示功能
tip: 網(wǎng)速顯示不太準(zhǔn)確,參考意義不大,網(wǎng)上沒有較好的網(wǎng)速計算方式,大家如果看到歡迎留言
項目傳送門:?gitee???????文章來源:http://www.zghlxwxcb.cn/news/detail-430563.html
轉(zhuǎn)載請注明出處文章來源地址http://www.zghlxwxcb.cn/news/detail-430563.html
到了這里,關(guān)于springboot整合Minio + vue 實現(xiàn)文件分片上傳(完整代碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!