Vue?+?SpringBoot?實現(xiàn)文件的斷點上傳、秒傳存儲到Minio的操作方法
一、前端
1. 計算文件的md5值
前端頁面使用的elment-plus的el-upload組件。
<el-upload action="#" :multiple="true" :auto-upload="false" :on-change="handleChange" :show-file-list="false"> <FileButton content="上傳文件" type="primary" class="file-button" /> </el-upload>
當(dāng)上傳文件后,會調(diào)用handleChange 方法,可以在這里進(jìn)行文件相關(guān)的操作。
//處理文件上傳 const handleChange = async (uploadFile) => { //文件名字 let fileName = uploadFile.name //文件的大小 const fileSize = uploadFile.size || 0 //當(dāng)前的文件對象 let fileItem = {} fileItem.fileName = fileName fileItem.fileSize = fileSize fileItem.state = 1 //解碼中 fileItem.progress = 0 //進(jìn)度是0 fileItem.filePid = 102903232 fileItem.fileMd5 = "" fileItem.uploadSize = 0 fileUploadList.value.addFile(fileItem) //彈框顯示 isVisible.value = true //獲得文件的md5 if (uploadFile.raw) { await generateMD5OfFile(uploadFile.raw).then( res => { fileItem.fileMd5 = res } ) } fileUploadList.value.addMd5(fileItem.fileName, fileItem.fileMd5) fileUploadList.value.changeFileState(fileItem.fileName, 2) //分片上傳 let chunkTotals = Math.ceil(fileSize / chunkSize); //分片上傳 if (chunkTotals > 0) { for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) { //文件最后的end let end = Math.min(fileSize, start + chunkSize); // el-mement - plus中,上傳的文件就在raw里面 const files = uploadFile.raw?.slice(start, end) //上傳的結(jié)果 const result = await uploadFileToServer(files, chunkNumber + 1, chunkTotals, fileName , getCurrentId(), fileItem.fileMd5,userId) console.log(result.data) console.log(result.data.data) if (result.data.data.status === 1) { // console.log("上傳中") //上傳的進(jìn)度 fileUploadList.value.changeProgress(fileItem.fileName, ((end / fileSize) * 100).toFixed(1)) //修改已經(jīng)上傳完成的文件大小 fileUploadList.value.changeUploadSize(fileItem.fileName, end) } else if (result.data.data.status === 3) { // console.log("上傳成功!"),這里是彈窗顯示的文件上傳進(jìn)度,可以適當(dāng)修改 fileUploadList.value.changeFileState(fileItem.fileName, 3) //上傳完成 fileUploadList.value.changeProgress(fileItem.fileName, 100) // 進(jìn)度100% //通過main,進(jìn)行刷新 $emit("addChangeNum") return ; //結(jié)束 } else { message("上傳失敗", 'error') return; //結(jié)束 } } } }
計算文件的MD5值
//計算文件的md5 function generateMD5OfFile(file) { return new Promise((resolve, reject) => { let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, // Read in chunks of 2MB chunks = Math.ceil(file.size / chunkSize), currentChunk = 0, spark = new SparkMD5.ArrayBuffer(), fileReader = new FileReader(); fileReader.onload = function (e) { console.log('read chunk nr', currentChunk + 1, 'of', chunks); spark.append(e.target.result); // Append array buffer currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()) } }; fileReader.onerror = function () { reject('MD5 calc error') }; function loadNext() { let start = currentChunk * chunkSize, end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }) }
2.計算文件切片數(shù)量
自定義文件切片大小
//默認(rèn)分片大小 const chunkSize = 5 * 1024 * 1024
3.分片上傳文件
上傳文件到服務(wù)器
// 上傳文件到服務(wù)器 const uploadFileToServer = async (file, chunkNumber, chunkTotal, fileName,filePid, fileMd5,userId) => { const form = new FormData(); // 這里的data是文件 form.append("file", file); form.append("chunkNumber", chunkNumber); form.append("chunkTotal", chunkTotal); form.append("fileName", fileName) form.append("fileMd5", fileMd5) form.append("filePid", filePid) form.append("userId", userId) var result = await axios({ url: env_server_production + '/file/upload', headers: { 'Content-Type': 'multipart/form-data' }, method: "post", timeout: 1000000, data: form }) return result }
4.實現(xiàn)相關(guān)文件的預(yù)覽
可以簡單的實現(xiàn)對一些文件的預(yù)覽,比如圖片、視頻、word、pdf等等。
pdf:
等等
這里使用的是vue-office
<template> <div class="preview-body"> <!-- word --> <vue-office-docx v-if="getFileType() == 1" :src="getFileUrl()" style="height: 400px;" @rendered="renderedHandler" @error="errorHandler" /> <!-- pdf --> <vue-office-pdf v-else-if="getFileType() == 2" :src="getFileUrl()" style="height: 400px;" @rendered="renderedHandler" @error="errorHandler" /> <!-- iamge --> <div v-else-if="getFileType() == 3"> <el-image :src="getFileUrl()" style="height: 100px; width: 100px;" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :preview-src-list="imageList" :initial-index="4" /> <br> <el-text style="margin-left: 0px;" link type="primary">點擊圖片查看詳情</el-text> </div> <!-- 不支持顯示 --> <div v-else-if="getFileType() == 4"> <br> 該文件不支持在線瀏覽,請下載后查看! </div> <!-- 視頻 --> <div v-else-if="getFileType() == 5"> <video autoplay width="1200px" height="400px" controls :src="getFileUrl()" id="myVideo" > </video> </div> <!-- 文本顯示 --> <div v-else> <el-scrollbar height="400px" class="document-preview"> <pre>{{ documentContent }}</pre> </el-scrollbar> </div> </div> </template> <script setup> //引入相關(guān)樣式 import VueOfficeDocx from '@vue-office/docx' import VueOfficePdf from '@vue-office/pdf' import '@vue-office/docx/lib/index.css' import { ref } from 'vue' import axios from 'axios'; const props = defineProps(['file']) const video = document.getElementById("myVideo") const getFileUrl = () => { return "http://60.205.141.200:9000/" + props.file.filePath; } const getFileType = () => { let category = props.file.fileCategory if (category == 18 || category == 19) { return 1 } else if (category == 13) return 2 else if (category == 9 || category == 14 || category == 5) { imageList.value.push(getFileUrl()) return 3 } else if (category == 20 || category == 11 || category == 15) return 4 else if (category == 12) { //視頻 return 5 } else { //文本 readDocumentContent(); } } const readDocumentContent = async () => { var res = await axios.get(getFileUrl(), { responseType: 'text', }) documentContent.value = `\n${res.data}\n` } //文件中的內(nèi)容 const documentContent = ref('') //圖片列表 const imageList = ref([]) const renderedHandler = () => { console.log("渲染成功") } const errorHandler = () => { console.log("渲染失敗") } </script> <style lang="scss" scoped> .document-preview { margin-right: 100px; background-color: #ccc; width: 1164px; border: 2px solid #ccc; height: 400px; border-radius: 0 0 10px 10px; text-align: left; } pre { font-family: 'Microsoft YaHei'; } </style>
二、后端
后端使用minio,minio先接收分片文件,上傳完成所有的分片文件后,在合并分片文件,刪除中間文件即可。
1.接收分片文件、合并文件。
/** * 上傳文件方法。 * 該方法負(fù)責(zé)檢查文件是否已存在,如果存在,則返回已存在標(biāo)志;如果不存在且是完整文件,則上傳文件到MinIO并保存文件信息到數(shù)據(jù)庫。 * * @param fileVO 文件相關(guān)信息VO,包含文件本身、MD5、文件名等。 * @return 如果文件已存在,返回秒傳狀態(tài)碼;如果文件上傳完成,返回上傳完成狀態(tài)碼;否則返回null。 * @throws GeneralException 如果文件為空,拋出通用異常。 */ @Override @Transactional(rollbackFor = Exception.class) //所有的操作都在一個事務(wù)里面。 public HashMap<Object, Object> uploadFile(FileVO fileVO) { if(fileVO.getFile().isEmpty()) throw new GeneralException("文件上傳異常"); FileInfo insertItem = new FileInfo(); Date now = new Date(); HashMap<Object, Object> map = new HashMap<>(); //第一片文件 if(fileVO.getChunkNumber() == 1){ //先去數(shù)據(jù)庫看看有沒有這個文件 QueryWrapper<FileInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("file_md5", fileVO.getFileMd5()); //通過Md5查詢,別人是不是已經(jīng)傳過這個文件了(文件名不影響文件的MD5值)。 List<FileInfo> fileInfoList = fileInfoMapper.selectList(queryWrapper); FileInfo fileInfo = null; if(fileInfoList.size() > 0){ fileInfo = fileInfoList.get(0); } //別人已經(jīng)上傳過這個文件了,直接秒傳 if(fileInfo != null){ log.info("服務(wù)器中有相同的文件,直接秒傳"); //說明minIO中有對應(yīng)的文件 insertItem.setUserId(fileVO.getUserId()); insertItem.setFileMd5(fileVO.getFileMd5()); insertItem.setFileName(fileInfo.getFileName()); insertItem.setFileCategory(fileInfo.getFileCategory()); insertItem.setFileId(StringUtil.getRandomString(10)); insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag()); insertItem.setFilePid(fileVO.getFilePid()); insertItem.setFilePath(fileInfo.getFilePath()); insertItem.setCreateTime(now); insertItem.setFileSize(fileInfo.getFileSize()); insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus()); fileInfoMapper.insert(insertItem); System.err.println(insertItem); map.put("status",UploadStatus.UPLOAD_FINISH.getStatus()); map.put("fileId",insertItem.getFileId()); return map; } //插入 一個切片 redisUtil.set(fileVO.getFileMd5(),0); } if(Integer.parseInt(redisUtil.get(fileVO.getFileMd5()).toString()) >= fileVO.getChunkNumber()){ //說明這片文件已經(jīng)上傳過了。 map.put("status",UploadStatus.UPLOADING.getStatus()); return map; } //只有一段,直接放到服務(wù)器就行 if(fileVO.getChunkTotal() == 1){ int lastDotIndex = fileVO.getFileName().lastIndexOf("."); String type = fileVO.getFileName().substring(lastDotIndex + 1); String url = minioUtils.uploadFile(MessageConstant.MINIO_BUCKET,fileVO.getFileName(), fileVO.getFile()); insertItem.setUserId(fileVO.getUserId()); insertItem.setFileMd5(fileVO.getFileMd5()); insertItem.setFileName(fileVO.getFileName()); insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory()); insertItem.setFileId(StringUtil.getRandomString(10)); insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag()); insertItem.setFilePid(fileVO.getFilePid()); insertItem.setFilePath(url); insertItem.setCreateTime(now); insertItem.setFileSize(fileVO.getFile().getSize()); insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus()); fileInfoMapper.insert(insertItem); //刪除redis中的切片上傳信息 redisUtil.del(fileVO.getFileMd5()); map.put("status",UploadStatus.UPLOAD_FINISH.getStatus()); map.put("fileId",insertItem.getFileId()); return map; } log.info("分片上傳====> md5 :{} ,=====> index :{}",fileVO.getFileMd5(),fileVO.getChunkNumber()); //不止一片,繼續(xù)上傳 //放切片文件的目錄是 文件的userId + md5值,這個是唯一的。 String objectName = fileVO.getUserId() + fileVO.getFileMd5() ; try { minioUtils.putChunkObject(fileVO.getFile().getInputStream(), MessageConstant.MINIO_BUCKET, objectName + "/" + fileVO.getChunkNumber()); } catch (IOException e) { throw new GeneralException("文件上傳異常!"); } //最后一片,進(jìn)行合并 if(Objects.equals(fileVO.getChunkNumber(), fileVO.getChunkTotal())){ //獲得文件類型 int lastDotIndex = fileVO.getFileName().lastIndexOf("."); String type = fileVO.getFileName().substring(lastDotIndex + 1); //objectName : userId+md5 String filePath = minioUtils.composeObject(MessageConstant.MINIO_BUCKET,MessageConstant.MINIO_BUCKET,objectName, type); insertItem.setUserId(fileVO.getUserId()); insertItem.setFileMd5(fileVO.getFileMd5()); insertItem.setFileName(fileVO.getFileName()); insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory()); insertItem.setFileId(StringUtil.getRandomString(10)); insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag()); insertItem.setFilePid(fileVO.getFilePid()); insertItem.setFilePath(filePath); insertItem.setCreateTime(now); Long fileSize = MessageConstant.DEFAULT_CHUNK_SIZE * (fileVO.getChunkTotal() - 1) + fileVO.getFile().getSize(); insertItem.setFileSize(fileSize); insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus()); //插入一條數(shù)據(jù) System.out.println(fileInfoMapper.insert(insertItem)); //刪除minio中的臨時文件目錄 System.out.println(minioUtils.deleteFolder(MessageConstant.MINIO_BUCKET, objectName)); //刪除redis中的切片上傳信息 redisUtil.del(fileVO.getFileMd5()); map.put("status",UploadStatus.UPLOAD_FINISH.getStatus()); map.put("fileId",insertItem.getFileId()); return map; } //更新redis中的切片上傳信息 redisUtil.incrby(fileVO.getFileMd5(),1); //上傳中 map.put("status",UploadStatus.UPLOADING.getStatus()); return map; }
如何做到秒傳?
一個文件有個不重復(fù)的md5值,所謂的秒傳其實就是你要上傳的文件,別人已經(jīng)上傳過了,minio中已經(jīng)有這個文件了,再解析完文件的md5值之后,后端發(fā)現(xiàn)數(shù)據(jù)庫中md5存在了,所以就不用上傳文件了,直接在數(shù)據(jù)庫中創(chuàng)建一個信息即可,也就實現(xiàn)了秒傳。
如何做到斷點傳遞?
傳統(tǒng)傳遞過程是一整個文件上傳,如果中斷了下次傳的時候,需要重新上傳;斷點傳遞,每次傳遞的時候,可以把分片信息放到redis中,同時下一次傳分片的時候,判斷一下,redis中時候已經(jīng)有了這個分片,如果有就不用上傳此分片文件,即斷點傳遞。
到此這篇關(guān)于Vue + SpringBoot 實現(xiàn)文件的斷點上傳、秒傳,存儲到Minio的文章就介紹到這了,更多相關(guān)SpringBoot斷點上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue項目使用localStorage+Vuex保存用戶登錄信息
這篇文章主要為大家詳細(xì)介紹了Vue項目使用localStorage+Vuex保存用戶登錄信息,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05VUE側(cè)邊導(dǎo)航欄實現(xiàn)篩選過濾的示例代碼
本文主要介紹了VUE側(cè)邊導(dǎo)航欄實現(xiàn)篩選過濾的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05關(guān)于Vue?"__ob__:Observer"屬性的解決方案詳析
在操作數(shù)據(jù)的時候發(fā)現(xiàn),__ob__: Observer這個屬性出現(xiàn)之后,如果單獨拿數(shù)據(jù)的值,就會返回undefined,下面這篇文章主要給大家介紹了關(guān)于Vue?"__ob__:Observer"屬性的解決方案,需要的朋友可以參考下2022-11-11vue-router+vuex addRoutes實現(xiàn)路由動態(tài)加載及菜單動態(tài)加載
本篇文章主要介紹了vue-router+vuex addRoutes實現(xiàn)路由動態(tài)加載及菜單動態(tài)加載,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09vue 對axios get pust put delete封裝的實例代碼
在本篇文章里我們給各位整理的是一篇關(guān)于vue 對axios get pust put delete封裝的實例代碼內(nèi)容,有需要的朋友們可以參考下。2020-01-01淺談Vue開發(fā)人員的7個最好的VSCode擴(kuò)展
這篇文章主要介紹了淺談Vue開發(fā)人員的7個最好的VSCode擴(kuò)展,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01