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

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

流程:

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

流程图:

效果图

  1.vue前端

2. 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

// 用于axios请求的取消
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, // 暂停true 继续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() {
            // 创建一个允许同时执行3个任务的任务队列
            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;

                    // 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. 正在创建分片
                    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 过滤并生成新的数组
            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;
                    // 计算平均网速(字节/秒) chunkSize: 文件分片大小 uploadTime: 时间差
                    currentFile.uploadSpeed = (chunkSize / uploadTime / 1024 / 1024).toFixed(1);

                    result[index] = true;
                    return true;
                } catch (error) {
                    console.log('上传失败');
                    //chunkList.push(chunk);
                    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++;
                        }
                    };
                });
            }
        },


        /**
         * 文件合并
         * @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) => {
                // 去除重复文件
                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
        * 取第一个和最后一个切片全部内容; 再加上取中间的,分片个数为偶数个,取中间的两个分片; 为奇数的取中间的一个分片; 来计算 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();

                // 取第一个切片内容
                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;
        },





        /**
         * 根据文件信息获取分片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 {
                // 无论是否发生异常,都会执行
            }
        },

        /**
        * 检查分片上传进度
        */
        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) {
            //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); // 返回'.'后的字符串  
        },

        // 字节转标准单位
        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; // 获取任务 id 和任务本身
                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'

// 创建 axios 实例
const service = axios.create({
  baseURL: "/api", // 环境的不同,对应不同的baseURL
  // transformRequest: [function(data) {
  //   return Qs.stringify(data)
  // }],
  //timeout: 5000 // 请求超时时间
})
 
 
//request请求拦截
service.interceptors.request.use(
  config => {
    // var token=getToken()
    // if (token) {
    //     config.headers.token = token // 让每个请求携带自定义token 请根据实际情况自行修改
    //   }
    return config;
},
  error => {
    // do something with request error
    return Promise.reject(error)
  }
)
 
 
//响应拦截
service.interceptors.response.use(
  response => {
    const res = response
    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.*;


/**
 * minio上传流程
 *
 * 1.检查数据库中是否存在上传文件
 *
 * 2.根据文件信息初始化,获取分片预签名url地址,前端根据url地址上传文件
 *
 * 3.上传完成后,将分片上传的文件进行合并
 *
 * 4.保存文件信息到数据库
 */
@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: 通过查询 <{}> 文件是否存在、是否进行断点续传", md5);
        return uploadService.getByFileMd5(md5);
    }

    /**
     * 分片初始化
     *
     * @param fileUploadInfo 文件信息
     * @return ResponseResult<Object>
     */
    @PostMapping("/multipart/init")
    public ResponseResult initMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
        log.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
        return uploadService.initMultiPartUpload(fileUploadInfo);
    }

    /**
     * 完成上传
     *
     * @param fileUploadInfo  文件信息
     * @return ResponseResult<Object>
     */
    @PostMapping("/multipart/merge")
    public ResponseResult completeMultiPartUpload(@RequestBody FileUploadInfo fileUploadInfo) {
        log.info("REST: 通过 <{}> 合并上传任务", fileUploadInfo);
        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 获取已上传的数据
     * @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 获取已上传的数据(断点续传)
     *
     * @param md5 String
     * @return Mono<Map < String, Object>>
     */
    @Override
    public ResponseResult<Object> getByFileMd5(String md5) {

        if (StringUtils.hasText(md5)) {
            log.error("查询文件是否存在、入参无效");
            return ResponseResult.error(ACCESS_PARAMETER_INVALID);
        }

        log.info("tip message: 通过 <{}> 查询数据是否存在", md5);

        // 获取文件名称和id
        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);
    }


    /**
     * 文件分片上传
     *
     * @param fileUploadInfo
     * @return Mono<Map < String, Object>>
     */
    @Override
    public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo) {

        log.info("tip message: 通过 <{}> 开始初始化<分片上传>任务", fileUploadInfo);

        // 获取文件桶名
        String bucketName = fileService.getBucketName(fileUploadInfo.getFileType());

        // 单文件上传可拆分,可直接上传完成
        if (fileUploadInfo.getPartCount() == 1) {

            log.info("tip message: 当前分片数量 <{}> 进行单文件上传", fileUploadInfo.getPartCount());

            // 获取文件分片上传的url
            return fileService.getUploadObjectUrl(fileUploadInfo.getFileName(), bucketName);

        }else {

            log.info("tip message: 当前分片数量 <{}> 进行分片上传", 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: 通过 <{}> 开始合并<分片上传>任务", 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();
    }




    @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      分片数量
     * @param contentType    类型,如果类型使用默认流会导致无法预览
     * @param bucketName     桶名称
     * @return Mono<Map < String, Object>>
     */

    public ResponseResult<Object> initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int partCount, String contentType, String bucketName) {
        log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName);

        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: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
            //目前仅做了最大1000分片
            Part[] parts = new Part[1000];
            // 查询上传后的分片数据
            ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
            int partNumber = 1;
            for (Part part : partResult.result().partList()) {
                parts[partNumber - 1] = new Part(partNumber, part.etag());
                partNumber++;
            }
            // 合并分片
            customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);

        } catch (Exception e) {
            log.error("error message: 合并失败、原因:", e);
            return false;
        }
        return true;
    }

    /**
     * 通过 sha256 获取上传中的分片信息
     *
     * @param objectName 文件全路径名称
     * @param uploadId   返回的uploadId
     * @param bucketName 桶名称
     * @return Mono<Map < String, Object>>
     */
    public ResponseResult<Object> 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);
        }
    }

    /**
     * 获取文件下载地址
     *
     * @param bucketName 桶名称
     * @param fileName   文件名
     * @return
     */
    public String getFilePath(String bucketName, String fileName) {
        return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径
    }

    /**
     * 创建一个桶
     *
     * @return
     */
    public String createBucket(String bucketName) {
        try {
            BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();

            //如果桶存在
            if (customMinioClient.bucketExists(bucketExistsArgs)) {
                return bucketName;
            }

            // 如果不存在则创建新文件桶
            MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();

            customMinioClient.makeBucket(makeBucketArgs);

            return bucketName;

        } catch (Exception e) {
            log.error("创建桶失败:{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }


    /**
     * 根据文件类型获取minio桶名称
     *
     * @param fileType
     * @return
     */
    public String getBucketName(String fileType) {
        try {
            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);
    }

    /**
     * 查询当前上传后的分片信息
     *
     * @param bucketName       String   桶名称
     * @param region           String
     * @param objectName       String   文件名称
     * @param maxParts         Integer  分片数量
     * @param partNumberMarker Integer  分片起始值
     * @param uploadId         String   上传的 uploadId
     * @param extraHeaders     Multimap<String, String>
     * @param extraQueryParams Multimap<String, String>
     * @return ListPartsResponse
     */
    public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }


}


6.CorsConfig

package com.xy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 全局跨域处理
 * @author CV
 */

@Configuration
public class CorsConfig implements WebMvcConfigurer {

        private CorsConfiguration buildConfig() {
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.addAllowedOrigin("*");
            corsConfiguration.addAllowedHeader("*");
            corsConfiguration.addAllowedMethod("*");
            corsConfiguration.setMaxAge(3600L);
            corsConfiguration.setAllowCredentials(true);
            return corsConfiguration;
        }

        @Bean
        public CorsFilter corsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", buildConfig());
            return new CorsFilter(source);
        }
    }

接下来是返回信息工具类

7.ResponseResult

package com.xy.util;

import lombok.Data;

@Data
public class ResponseResult<T> {
    private int code;
    private String enMessage;
    private String zhMessage;
    private T data;

    public ResponseResult() {
    }

    public ResponseResult(int code, String enMessage, String zhMessage) {
        this.code = code;
        this.enMessage = enMessage;
        this.zhMessage = zhMessage;
    }


    /**
     * 成功
     */
    public static <T> ResponseResult<T> success() {
        ResponseResult<T> result = new ResponseResult<T>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
        result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
        return result;
    }


    /**
     * 成功
     */
    public static <T> ResponseResult<T> success(T data) {
        ResponseResult<T> result = new ResponseResult<T>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setEnMessage(ResultCode.SUCCESS.getEnMessage());
        result.setZhMessage(ResultCode.SUCCESS.getZhMessage());
        result.setData(data);
        return result;
    }


    /**
     * 失败
     */
    public static  <T> ResponseResult <T> error() {
        ResponseResult<T> result = new ResponseResult<T>();
        result.setCode(ResultCode.FAIL.getCode());
        result.setEnMessage(ResultCode.FAIL.getEnMessage());
        result.setZhMessage(ResultCode.FAIL.getZhMessage());
        return result;
    }

    /**
     * 失败
     */
    public static <T> ResponseResult<T> error(T data) {
        ResponseResult<T> result = new ResponseResult<T>();
        result.setCode(ResultCode.FAIL.getCode());
        result.setEnMessage(ResultCode.FAIL.getEnMessage());
        result.setZhMessage(ResultCode.FAIL.getZhMessage());
        result.setData(data);
        return result;
    }


    /**
     *
     * @param data 数据
     * @param <T>
     * @return
     */
    public static <T> ResponseResult<T> uploading(T data) {
        ResponseResult<T> result = new ResponseResult<T>();
        result.setCode(ResultCode.UPLOADING.getCode());
        result.setEnMessage(ResultCode.UPLOADING.getEnMessage());
        result.setZhMessage(ResultCode.UPLOADING.getZhMessage());
        result.setData(data);
        return result;
    }

    /**
     * 成功
     */
    public static <T> ResponseResult<T> success(int code, String enMessage, String zhMessage) {
        return new ResponseResult(code, enMessage, zhMessage);
    }


    /**
     * 失败
     */
    public static <T> ResponseResult<T> error(int code, String enMessage, String zhMessage) {
        return new ResponseResult(code, enMessage, zhMessage);
    }


    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getEnMessage() {
        return enMessage;
    }

    public void setEnMessage(String enMessage) {
        this.enMessage = enMessage;
    }

    public String getZhMessage() {
        return zhMessage;
    }

    public void setZhMessage(String zhMessage) {
        this.zhMessage = zhMessage;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

//    public static ResponseResult<Void> SUCCESS = new ResponseResult<>(200,"成功");
//    public static ResponseResult<Void> INTEVER_ERROR = new ResponseResult<>(500,"服务器错误");
//    public static ResponseResult<Void> NOT_FOUND = new ResponseResult<>(404,"未找到");

}

8.ResultCode

package com.xy.util;



/**
 * http状态码枚举类
 */
public enum ResultCode {

    SUCCESS(1, "Success", "成功"),
    UPLOADING(2, "Uploading", "上传中"),
    FAIL(-1, "Err", "失败"),


    DATABASE_OPERATION_FAILED(504, "数据库操作失败"),
    CONTINUE(100, "Continue", "请继续发送请求的剩余部分"),
    SWITCHING_PROTOCOLS(101, "Switching Protocols", "协议切换"),
    PROCESSING(102, "Processing", "请求将继续执行"),
    CHECKPOINT(103, "Checkpoint", "可以预加载"),
    OK(200, "OK", "请求已经成功处理"),
    CREATED(201, "Created", "请求已经成功处理,并创建了资源"),
    ACCEPTED(202, "Accepted", "请求已经接受,等待执行"),
    NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information", "请求已经成功处理,但是信息不是原始的"),
    NO_CONTENT(204, "No Content", "请求已经成功处理,没有内容需要返回"),
    RESET_CONTENT(205, "Reset Content", "请求已经成功处理,请重置视图"),
    PARTIAL_CONTENT(206, "Partial Content", "部分Get请求已经成功处理"),
    MULTI_STATUS(207, "Multi-Status", "请求已经成功处理,将返回XML消息体"),
    ALREADY_REPORTED(208, "Already Reported", "请求已经成功处理,一个DAV的绑定成员被前一个请求枚举,并且没有被再一次包括"),
    IM_USED(226, "IM Used", "请求已经成功处理,将响应一个或者多个实例"),
    MULTIPLE_CHOICES(300, "Multiple Choices", "提供可供选择的回馈"),
    MOVED_PERMANENTLY(301, "Moved Permanently", "请求的资源已经永久转移"),
    FOUND(302, "Found", "请重新发送请求"),
    SEE_OTHER(303, "See Other", "请以Get方式请求另一个URI"),
    NOT_MODIFIED(304, "Not Modified", "资源未改变"),
    USE_PROXY(305, "Use Proxy", "请通过Location域中的代理进行访问"),
    TEMPORARY_REDIRECT(307, "Temporary Redirect", "请求的资源临时从不同的URI响应请求"),
    RESUME_INCOMPLETE(308, "Resume Incomplete", "请求的资源已经永久转移"),
    BAD_REQUEST(400, "Bad Request", "请求错误,请修正请求"),
    UNAUTHORIZED(401, "Unauthorized", "没有被授权或者授权已经失效"),
    PAYMENT_REQUIRED(402, "Payment Required", "预留状态"),
    FORBIDDEN(403, "Forbidden", "请求被理解,但是拒绝执行"),
    NOT_FOUND(404, "Not Found", "资源未找到"),
    METHOD_NOT_ALLOWED(405, "Method Not Allowed", "请求方法不允许被执行"),
    NOT_ACCEPTABLE(406, "Not Acceptable", "请求的资源不满足请求者要求"),
    PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required", "请通过代理进行身份验证"),
    REQUEST_TIMEOUT(408, "Request Timeout", "请求超时"),
    CONFLICT(409, "Conflict", "请求冲突"),
    GONE(410, "Gone", "请求的资源不可用"),
    LENGTH_REQUIRED(411, "Length Required", "Content-Length未定义"),
    PRECONDITION_FAILED(412, "Precondition Failed", "不满足请求的先决条件"),
    REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large", "请求发送的实体太大"),
    REQUEST_URI_TOO_LONG(414, "Request-URI Too Long", "请求的URI超长"),
    UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type", "请求发送的实体类型不受支持"),
    REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable", "Range指定的范围与当前资源可用范围不一致"),
    EXPECTATION_FAILED(417, "Expectation Failed", "请求头Expect中指定的预期内容无法被服务器满足"),
    UNPROCESSABLE_ENTITY(422, "Unprocessable Entity", "请求格式正确,但是由于含有语义错误,无法响应"),
    LOCKED(423, "Locked", "当前资源被锁定"),
    FAILED_DEPENDENCY(424, "Failed Dependency", "由于之前的请求发生错误,导致当前请求失败"),
    UPGRADE_REQUIRED(426, "Upgrade Required", "客户端需要切换到TLS1.0"),
    PRECONDITION_REQUIRED(428, "Precondition Required", "请求需要提供前置条件"),
    TOO_MANY_REQUESTS(429, "Too Many Requests", "请求过多"),
    REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large", "请求头超大,拒绝请求"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error", "服务器内部错误"),
    NOT_IMPLEMENTED(501, "Not Implemented", "服务器不支持当前请求的部分功能"),
    BAD_GATEWAY(502, "Bad Gateway", "响应无效"),
    SERVICE_UNAVAILABLE(503, "Service Unavailable", "服务器维护或者过载,拒绝服务"),
    GATEWAY_TIMEOUT(504, "Gateway Timeout", "上游服务器超时"),
    HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported", "不支持的HTTP版本"),
    VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates", "服务器内部配置错误"),
    INSUFFICIENT_STORAGE(507, "Insufficient Storage", "服务器无法完成存储请求所需的内容"),
    LOOP_DETECTED(508, "Loop Detected", "服务器处理请求时发现死循环"),
    BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded", "服务器达到带宽限制"),
    NOT_EXTENDED(510, "Not Extended", "获取资源所需的策略没有被满足"),
    NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required", "需要进行网络授权"),
    ACCESS_PARAMETER_INVALID(1001,"Invalid access parameter","访问参数无效"),
    UPLOAD_FILE_FAILED(1002,"File upload failure","文件上传失败"),
    DATA_NOT_EXISTS(1003,"Data does not exist","数据不存在"),

    ;



    private int code;
    private String enMessage;
    private String zhMessage;

    ResultCode(int code, String enMessage, String zhMessage) {
        this.code = code;
        this.enMessage = enMessage;
        this.zhMessage = zhMessage;
    }

    ResultCode(int code, String message) {

    }


    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getEnMessage() {
        return enMessage;
    }

    public void setEnMessage(String enMessage) {
        this.enMessage = enMessage;
    }

    public String getZhMessage() {
        return zhMessage;
    }

    public void setZhMessage(String zhMessage) {
        this.zhMessage = zhMessage;
    }
}

9.FileUploadInfo,还有最重要的实体类

package com.xy.entity;

import lombok.Data;
import lombok.experimental.Accessors;


@Data
@Accessors(chain = true)
public class FileUploadInfo {

    //@NotBlank(message = "文件名不能为空")
    private String fileName;

    // @NotNull(message = "文件大小不能为空")
    private Double fileSize;

    // @NotBlank(message = "Content-Type不能为空")
    private String contentType;

    //  @NotNull(message = "分片数量不能为空")
    private Integer partCount;

    // @NotBlank(message = "uploadId 不能为空")
    private String uploadId;

    // 桶名称
    //private String bucketName;

    //md5
    private String fileMd5;

    //文件类型
    private String fileType;


    public FileUploadInfo() {
    }


}


10.RedisRepo

package com.xy.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;


@Component
public class RedisRepo {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public String get(String key) {
        BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
        return ops.get();
    }

    public void save(String key,String str){
        BoundValueOperations<String, String> ops = redisTemplate.boundValueOps(key);
        ops.set(str);
    }

    public void saveTimeout(String key, String value, long timeout, TimeUnit unit ){
        redisTemplate.boundValueOps(key).setIfAbsent(value,timeout,unit);
    }

    public void delete(String key){
        redisTemplate.delete(key);
    }

    public long expire(String key){
        return redisTemplate.opsForValue().getOperations().getExpire(key);
    }
}

11.yaml配置

minio:
  endpoint: http://localhost:9000
  accesskey: minioadmin
  secretkey: minioadmin

spring:
  redis:
    host: localhost
    port: 6379

12.pom配置

      <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.2</version>
        </dependency>

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​​​​​​​

转载请注明出处

  • 11
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值