Java實(shí)現(xiàn)大文件的分片上傳與下載(springboot+vue3)
源碼:
- https://gitee.com/gaode-8/big-file-upload
演示視頻
- https://www.bilibili.com/video/BV1CA411f7np/?vd_source=1fe29350b37642fa583f709b9ae44b35
1.1 項(xiàng)目背景
對(duì)于超大文件上傳我們可能遇到以下問題
- • 大文件直接上傳,占用過多內(nèi)存,可能導(dǎo)致內(nèi)存溢出甚至系統(tǒng)崩潰
- • 受網(wǎng)絡(luò)環(huán)境影響,可能導(dǎo)致傳輸中斷,只能重新傳輸
- • 傳輸時(shí)間長,用戶無法知道傳輸進(jìn)度,用戶體驗(yàn)不佳
1.2 項(xiàng)目目標(biāo)
對(duì)于上述問題,我們需要對(duì)文件做分片傳輸。分片傳輸就是把文件分割成許多較小的文件,然后分多次上傳,最后再完成合并。
受網(wǎng)絡(luò)環(huán)境影響,我們還要實(shí)現(xiàn)斷點(diǎn)續(xù)傳,以節(jié)省傳輸時(shí)間和資源。斷點(diǎn)續(xù)傳就是已經(jīng)上傳或者下載過的文件分片不再傳輸。
對(duì)于已經(jīng)上傳過的文件,可以不再上傳,實(shí)現(xiàn)秒傳。秒傳就是根據(jù)文件的唯一標(biāo)識(shí),確認(rèn)是否需要上傳。
實(shí)現(xiàn)多任務(wù)上傳或下載。多任務(wù)就是同時(shí)多個(gè)文件上傳或下載。
2.1 業(yè)務(wù)流程
用戶上傳文件的流程圖如圖1所示,用戶首先選擇要上傳的文件,上傳過程中可以選擇暫?;蚶^續(xù)上傳。
用戶上傳文件的流程圖如圖2所示,用戶首先可以瀏覽可以下載的文件列表,然后點(diǎn)擊下載,下載過程中可以選擇暫?;蚶^續(xù)下載
2.2 系統(tǒng)用例
系統(tǒng)用例圖如圖3所示,用戶可以上傳文件,在文件上傳過程中可以查看文件的上傳進(jìn)度和速度,也可以暫?;蜷_始上傳;用戶可以查看已經(jīng)上傳過的,也就是可以下載的文件列表;用戶可以下載文件,在下載過程中可以查看文件下載的速度和進(jìn)度,用戶可以暫?;蜷_始下載。
2.3 系統(tǒng)總體功能
系統(tǒng)總體功能圖如圖4所示,分為上傳和下載。上傳包括秒傳,分片上傳,斷點(diǎn)續(xù)傳,多任務(wù)。下載包括分片下載,斷點(diǎn)續(xù)傳,多任務(wù)。
3.1 技術(shù)選型
后端:
• 語言:Java8
• 框架:SpringBoot2.6
• 開發(fā)工具:Idea 2021
前端:
• 語言:Html5、css3、JavaScript
• 框架:Vue3
• 開發(fā)工具:Vscode、Edge
數(shù)據(jù)庫:
• mysql8
4.1 文件上傳模塊
文件上傳模塊的流程圖如圖6所示,順序圖如圖7所示
首先前端讀取文件生成文件的唯一標(biāo)識(shí)MD5,這里采用常用的MD5生成框架:spark-md5.js。對(duì)于大文件一次性讀取比較慢,而且容易造成瀏覽器崩潰,因此這里采用分片讀取的方式計(jì)算MD5。
然后向服務(wù)器發(fā)送請(qǐng)求,查看該文件時(shí)候已經(jīng)上傳,如果已經(jīng)上傳,就提示用戶已經(jīng)秒傳。
如果數(shù)據(jù)庫中沒有記錄該文件,就表示該文件沒有上傳或沒有上傳完成,那么服務(wù)器就查詢并返回記錄的chunk分片列表。
async 和 await配可以實(shí)現(xiàn)等待異步函數(shù)計(jì)算完成
//計(jì)算文件的md5值 function computeMd5(file, uploadFile) { return new Promise((resolve, reject) => { //分片讀取并計(jì)算md5 const chunkTotal = 100; //分片數(shù) const chunkSize = Math.ceil(file.size / chunkTotal); const fileReader = new FileReader(); const md5 = new SparkMD5(); let index = 0; const loadFile = (uploadFile) => { uploadFile.parsePercentage.value = parseInt((index / file.size) * 100); const slice = file.slice(index, index + chunkSize); fileReader.readAsBinaryString(slice); }; loadFile(uploadFile); fileReader.onload = (e) => { md5.appendBinary(e.target.result); if (index < file.size) { index += chunkSize; loadFile(uploadFile); } else { // md5.end() 就是文件md5碼 resolve(md5.end()); } }; }); } //檢查文件是否存在 function checkFile(md5) { return request({ url: "/check", method: "get", params: { md5: md5, }, }); } //文件上傳之前,el-upload自動(dòng)觸發(fā) async function beforeUpload(file) { console.log("2.上傳文件之前"); var uploadFile = {}; uploadFile.name = file.name; uploadFile.size = file.size; uploadFile.parsePercentage = ref(0); uploadFile.uploadPercentage = ref(0); uploadFile.uploadSpeed = "0 M/s"; uploadFile.chunkList = null; uploadFile.file = file; uploadFile.uploadingStop = false; uploadFileList.value.push(uploadFile); var md5 = await computeMd5(file, uploadFile);//async 和 await配可以實(shí)現(xiàn)等待異步函數(shù)計(jì)算完成 uploadFile.md5 = md5; var res = await checkFile(md5); //上傳服務(wù)器檢查,以確認(rèn)是否秒傳 var data = res.data.data; if (!data.isUploaded) { uploadFile.chunkList = data.chunkList; uploadFile.needUpload = true; } else { uploadFile.needUpload = false; uploadFile.uploadPercentage.value = 100; console.log("文件已秒傳"); ElMessage({ showClose: true, message: "文件已秒傳", type: "warning", }); } }
前端分片請(qǐng)求文件,如果分片編號(hào)被包含在分片列表內(nèi),就標(biāo)識(shí)該分片已經(jīng)上傳,跳過;反之,表示還未上傳,那么前端通過file的slice方法分割文件,向服務(wù)端傳遞。同時(shí)在頁面上顯示上傳進(jìn)度和速度。
服務(wù)端,收到前端的分片文件后,通過Java的RandomAccess類(隨機(jī)讀寫類),從文件的指定位置,寫入指定字節(jié),并記錄chunk到數(shù)據(jù)庫,如果是最后一個(gè)分片再記錄file到數(shù)據(jù)庫。
圖6 文件上傳流程圖
圖7 文件上傳順序圖
前端代碼
<template> <div class="main"> <!-- 文件上傳按鈕 --> <el-upload action="#" :http-request="upload" :before-upload="beforeUpload" :show-file-list="false" > <el-button type="primary">選擇上傳文件</el-button> </el-upload> <el-divider content-position="left">上傳列表</el-divider> <!-- 正在上傳的文件列表 --> <div class="uploading" v-for="uploadFile in uploadFileList"> <span class="fileName">{{ uploadFile.name }}</span> <span class="fileSize">{{ formatSize(uploadFile.size) }}</span> <div class="parse"> <span>解析進(jìn)度: </span> <el-progress :text-inside="true" :stroke-width="16" :percentage="uploadFile.parsePercentage" > </el-progress> </div> <div class="progress"> <span>上傳進(jìn)度:</span> <el-progress :text-inside="true" :stroke-width="16" :percentage="uploadFile.uploadPercentage" > </el-progress> <span v-if=" (uploadFile.uploadPercentage > 0) & (uploadFile.uploadPercentage < 100) " > <span class="uploadSpeed">{{ uploadFile.uploadSpeed }}</span> <el-button circle link @click="changeUploadingStop(uploadFile)"> <el-icon size="20" v-if="uploadFile.uploadingStop == false" ><VideoPause /></el-icon> <el-icon size="20" v-else><VideoPlay /></el-icon> </el-button> </span> </div> </div> </div> </template> <script setup> import emitter from "../utils/eventBus.js"; import { ElMessage } from "element-plus"; import SparkMD5 from "spark-md5"; import { VideoPause, VideoPlay } from "@element-plus/icons-vue"; import { ref, reactive, getCurrentInstance, nextTick } from "vue"; const { appContext } = getCurrentInstance(); const request = appContext.config.globalProperties.request; var uploadFileList = ref([]); //換算文件的大小單位 function formatSize(size) { //size的單位大小k var unit; var units = [" B", " K", " M", " G"]; var pointLength = 2; while ((unit = units.shift()) && size > 1024) { size = size / 1024; } return ( (unit === "B" ? size : size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit ); } //計(jì)算文件的md5值 function computeMd5(file, uploadFile) { return new Promise((resolve, reject) => { //分片讀取并計(jì)算md5 const chunkTotal = 100; //分片數(shù) const chunkSize = Math.ceil(file.size / chunkTotal); const fileReader = new FileReader(); const md5 = new SparkMD5(); let index = 0; const loadFile = (uploadFile) => { uploadFile.parsePercentage.value = parseInt((index / file.size) * 100); const slice = file.slice(index, index + chunkSize); fileReader.readAsBinaryString(slice); }; loadFile(uploadFile); fileReader.onload = (e) => { md5.appendBinary(e.target.result); if (index < file.size) { index += chunkSize; loadFile(uploadFile); } else { // md5.end() 就是文件md5碼 resolve(md5.end()); } }; }); } //檢查文件是否存在 function checkFile(md5) { return request({ url: "/check", method: "get", params: { md5: md5, }, }); } //文件上傳之前,el-upload自動(dòng)觸發(fā) async function beforeUpload(file) { console.log("2.上傳文件之前"); var uploadFile = {}; uploadFile.name = file.name; uploadFile.size = file.size; uploadFile.parsePercentage = ref(0); uploadFile.uploadPercentage = ref(0); uploadFile.uploadSpeed = "0 M/s"; uploadFile.chunkList = null; uploadFile.file = file; uploadFile.uploadingStop = false; uploadFileList.value.push(uploadFile); var md5 = await computeMd5(file, uploadFile);//async 和 await配可以實(shí)現(xiàn)等待異步函數(shù)計(jì)算完成 uploadFile.md5 = md5; var res = await checkFile(md5); //上傳服務(wù)器檢查,以確認(rèn)是否秒傳 var data = res.data.data; if (!data.isUploaded) { uploadFile.chunkList = data.chunkList; uploadFile.needUpload = true; } else { uploadFile.needUpload = false; uploadFile.uploadPercentage.value = 100; console.log("文件已秒傳"); ElMessage({ showClose: true, message: "文件已秒傳", type: "warning", }); } } //點(diǎn)擊暫停或開始上傳 function changeUploadingStop(uploadFile) { uploadFile.uploadingStop = !uploadFile.uploadingStop; if (!uploadFile.uploadingStop) { uploadChunk(uploadFile.file, 1, uploadFile); } } //上傳文件,替換el-upload的action function upload(xhrData) { var uploadFile = null; for (var i = 0; i < uploadFileList.value.length; i++) { if ( (xhrData.file.name == uploadFileList.value[i].name) & (xhrData.file.size == uploadFileList.value[i].size) ) { uploadFile = uploadFileList.value[i]; break; } } if (uploadFile.needUpload) { console.log("3.上傳文件"); // 分片上傳文件 // 確定分片的大小 uploadChunk(xhrData.file, 1, uploadFile); } } //上傳文件分片 function uploadChunk(file, index, uploadFile) { var chunkSize = 1024 * 1024 * 10; //10mb var chunkTotal = Math.ceil(file.size / chunkSize); if (index <= chunkTotal) { // 根據(jù)是否暫停,確定是否繼續(xù)上傳 // console.log("4.上傳分片"); var startTime = new Date().valueOf(); var exit = uploadFile.chunkList.includes(index); // console.log("是否存在",exit); if (!exit) { // console.log("3.3上傳文件",uploadingStop); if (!uploadFile.uploadingStop) { // 分片上傳,同時(shí)計(jì)算進(jìn)度條和上傳速度 // 已經(jīng)上傳的不在上傳、 // 上傳完成后提示,上傳成功 // console.log("上傳分片1",index); var form = new FormData(); var start = (index - 1) * chunkSize; let end = index * chunkSize >= file.size ? file.size : index * chunkSize; let chunk = file.slice(start, end); // downloadBlob(chunk,file) // console.log("chunk",chunk); form.append("chunk", chunk); form.append("index", index); form.append("chunkTotal", chunkTotal); form.append("chunkSize", chunkSize); form.append("md5", uploadFile.md5); form.append("fileSize", file.size); form.append("fileName", file.name); // console.log("上傳分片", index); request({ url: "/upload/chunk", method: "post", data: form, }).then((res) => { var endTime = new Date().valueOf(); var timeDif = (endTime - startTime) / 1000; // console.log("上傳文件大小",formatSize(chunkSize)); // console.log("耗時(shí)",timeDif); // console.log("then",index); // uploadSpeed = (chunkSize/(1024*1024)) / timeDif +" M / s" uploadFile.uploadSpeed = (10 / timeDif).toFixed(1) + " M/s"; // console.log(res.data.data); // console.log("f2",uploadFile); uploadFile.chunkList.push(index); // console.log("f3",uploadFile); uploadFile.uploadPercentage = parseInt( (uploadFile.chunkList.length / chunkTotal) * 100 ); // console.log("上傳進(jìn)度",uploadFile.uploadPercentage); if (index == chunkTotal) { emitter.emit("reloadFileList"); } uploadChunk(file, index + 1, uploadFile); }); } } else { uploadFile.uploadPercentage = parseInt( (uploadFile.chunkList.length / chunkTotal) * 100 ); uploadChunk(file, index + 1, uploadFile); } // } } } </script> <style scoped> .main { margin-top: 40px; margin-bottom: 40px; } .uploading { padding-top: 27px; } .progress { /* width: 700px; */ display: flex; } .uploading .parse { display: flex; } .parse .el-progress { /* font-size: 18px; */ width: 590px; } .progress .el-progress { /* font-size: 18px; */ width: 590px; } .uploading .fileName { font-size: 17px; margin-right: 40px; margin-left: 80px; /* width: 80px; */ } .uploading .fileSize { font-size: 17px; /* width: 80px; */ } .progress .uploadSpeed { font-size: 17px; margin-left: 5px; padding-left: 5px; padding-right: 10px; } </style>
后端代碼
package com.cugb.bigfileupload.controller; import com.cugb.bigfileupload.bean.FilePO; import com.cugb.bigfileupload.bean.Result; import com.cugb.bigfileupload.servie.ChunkService; import com.cugb.bigfileupload.servie.FileService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @RestController @CrossOrigin public class FileController { Logger logger = LoggerFactory.getLogger(getClass()); @Value("${file.path}") private String filePath; @Autowired private FileService fileService; @Autowired private ChunkService chunkService; @GetMapping("/check") public Result checkFile(@RequestParam("md5") String md5){ logger.info("檢查MD5:"+md5); //首先檢查是否有完整的文件 Boolean isUploaded = fileService.selectFileByMd5(md5); Map<String, Object> data = new HashMap<>(); data.put("isUploaded",isUploaded); //如果有,就返回秒傳 if(isUploaded){ return new Result(201,"文件已經(jīng)秒傳",data); } //如果沒有,就查找分片信息,并返回給前端 List<Integer> chunkList = chunkService.selectChunkListByMd5(md5); data.put("chunkList",chunkList); return new Result(201,"",data); } @PostMapping("/upload/chunk") public Result uploadChunk(@RequestParam("chunk") MultipartFile chunk, @RequestParam("md5") String md5, @RequestParam("index") Integer index, @RequestParam("chunkTotal")Integer chunkTotal, @RequestParam("fileSize")Long fileSize, @RequestParam("fileName")String fileName, @RequestParam("chunkSize")Long chunkSize ){ String[] splits = fileName.split("\\."); String type = splits[splits.length-1]; String resultFileName = filePath+md5+"."+type; chunkService.saveChunk(chunk,md5,index,chunkSize,resultFileName); logger.info("上傳分片:"+index +" ,"+chunkTotal+","+fileName+","+resultFileName); if(Objects.equals(index, chunkTotal)){ FilePO filePO = new FilePO(fileName, md5, fileSize); fileService.addFile(filePO); chunkService.deleteChunkByMd5(md5); return new Result(200,"文件上傳成功",index); }else{ return new Result(201,"分片上傳成功",index); } } @GetMapping("/fileList") public Result getFileList(){ logger.info("查詢文件列表"); List<FilePO> fileList = fileService.selectFileList(); return new Result(201,"文件列表查詢成功",fileList); } }
4.2 文件下載模塊
文件下載的流程圖如圖8所示,順序圖如圖9所示
文件下載是首先,前端向后端發(fā)送分片下載的請(qǐng)求,請(qǐng)求的responseType設(shè)為blob(Binary large Object) ,然后后端通過RandomAccess類讀取指定字節(jié)的內(nèi)容,再寫入到響應(yīng)的文件流中。
瀏覽器前端的請(qǐng)求的分片數(shù)據(jù),會(huì)暫時(shí)保存在“C:\Users\用戶名\AppData\Local\Microsoft\Edge\User Data\Default\blob_storage\”中,(請(qǐng)確保c盤有足夠的空間),當(dāng)所有分片下載完成,會(huì)合并成一個(gè)大文件(很快),分片不是放在內(nèi)存中,所以不用擔(dān)心文件太大是不是不行。
,
刷新瀏覽器,也會(huì)刪除已經(jīng)下載好的分片
當(dāng)前端請(qǐng)求了所有的文件分片之后,再把所有的blob合并成一個(gè)blob
if (index == chunkTotal) { var resBlob = new Blob(file.blobList, { type: "application/octet-stream", }); // console.log("resb", resBlob); let url = window.URL.createObjectURL(resBlob); // 將獲取的文件轉(zhuǎn)化為blob格式 let a = document.createElement("a"); // 此處向下是打開一個(gè)儲(chǔ)存位置 a.style.display = "none"; a.href = url; // 下面兩行是自己項(xiàng)目需要的處理,總之就是得到下載的文件名(加后綴)即可 var fileName = file.name; a.setAttribute("download", fileName); document.body.appendChild(a); a.click(); //點(diǎn)擊下載 document.body.removeChild(a); // 下載完成移除元素 window.URL.revokeObjectURL(url); // 釋放掉blob對(duì)象 }
圖9文件上傳順序圖
前端代碼
<template> <div class="main"> <div class="fileList"> <div class="title"> 文件列表 <!-- <hr> --> </div> <el-table :data="fileList" border style="width: 360px"> <el-table-column prop="name" label="文件名" width="150"> </el-table-column> <el-table-column prop="size" label="文件大小" width="110"> <template #default="scope"> {{ formatSize(scope.row) }} </template> </el-table-column> <el-table-column prop="" label="操作" width="100"> <template #default="scope"> <el-button size="small" type="primary" @click="downloadFile(scope.row)" >下載</el-button > </template> </el-table-column> </el-table> </div> <div class="downloadList"> <el-divider content-position="left">下載列表</el-divider> <div v-for="file in downloadingFileList"> <div class="downloading"> <span class="fileName">{{ file.name }}</span> <span class="fileSize">{{ formatSize(file) }}</span> <span class="downloadSpeed">{{ file.downloadSpeed }}</span> <div class="progress"> <span>下載進(jìn)度:</span> <el-progress :text-inside="true" :stroke-width="16" :percentage="file.downloadPersentage" > </el-progress> <el-button circle link @click="changeDownloadStop(file)"> <el-icon size="20" v-if="file.downloadingStop == false" ><VideoPause /></el-icon> <el-icon size="20" v-else><VideoPlay /></el-icon> </el-button> </div> </div> </div> </div> </div> </template> <script setup> import axios from "axios"; import { ref, reactive, getCurrentInstance } from "vue"; import emitter from "../utils/eventBus.js"; import { VideoPause, VideoPlay } from "@element-plus/icons-vue"; const { appContext } = getCurrentInstance(); const request = appContext.config.globalProperties.request; var fileList = reactive([]); var downloadingFileList = ref([]); //上傳文件之后,重新加載文件列表 emitter.on("reloadFileList", () => { load(); }); function load() { fileList.length = 0; request({ url: "/fileList", method: "get", }).then((res) => { // console.log("res", res.data.data); fileList.push(...res.data.data); }); } load(); //換算文件的大小單位 function formatSize(file) { //console.log("size",file.size); var size = file.size; var unit; var units = [" B", " K", " M", " G"]; var pointLength = 2; while ((unit = units.shift()) && size > 1024) { size = size / 1024; } return ( (unit === "B" ? size : size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit ); } //點(diǎn)擊暫停下載 function changeDownloadStop(file) { file.downloadingStop = !file.downloadingStop; if (!file.downloadingStop) { console.log("開始。。"); downloadChunk(1, file); } } //點(diǎn)擊下載文件 function downloadFile(file) { // console.log("下載", file); file.downloadingStop = false; file.downloadSpeed = "0 M/s"; file.downloadPersentage = 0; file.blobList = []; file.chunkList = []; downloadingFileList.value.push(file); downloadChunk(1, file); } //點(diǎn)擊下載文件分片 function downloadChunk(index, file) { var chunkSize = 1024 * 1024 * 5; var chunkTotal = Math.ceil(file.size / chunkSize); if (index <= chunkTotal) { // console.log("下載進(jìn)度",index); var exit = file.chunkList.includes(index); console.log("存在", exit); if (!exit) { if (!file.downloadingStop) { var formData = new FormData(); formData.append("fileName", file.name); formData.append("md5", file.md5); formData.append("chunkSize", chunkSize); formData.append("index", index); formData.append("chunkTotal", chunkTotal); if (index * chunkSize >= file.size) { chunkSize = file.size - (index - 1) * chunkSize; formData.set("chunkSize", chunkSize); } var startTime = new Date().valueOf(); axios({ url: "http://localhost:9001/download", method: "post", data: formData, responseType: "blob", timeout: 50000, }).then((res) => { file.chunkList.push(index); var endTime = new Date().valueOf(); var timeDif = (endTime - startTime) / 1000; file.downloadSpeed = (5 / timeDif).toFixed(1) + " M/s"; //todo file.downloadPersentage = parseInt((index / chunkTotal) * 100); // var chunk = res.data.data.chunk // const blob = new Blob([res.data]); const blob = res.data; file.blobList.push(blob); // console.log("res", blobList); if (index == chunkTotal) { var resBlob = new Blob(file.blobList, { type: "application/octet-stream", }); // console.log("resb", resBlob); let url = window.URL.createObjectURL(resBlob); // 將獲取的文件轉(zhuǎn)化為blob格式 let a = document.createElement("a"); // 此處向下是打開一個(gè)儲(chǔ)存位置 a.style.display = "none"; a.href = url; // 下面兩行是自己項(xiàng)目需要的處理,總之就是得到下載的文件名(加后綴)即可 var fileName = file.name; a.setAttribute("download", fileName); document.body.appendChild(a); a.click(); //點(diǎn)擊下載 document.body.removeChild(a); // 下載完成移除元素 window.URL.revokeObjectURL(url); // 釋放掉blob對(duì)象 } downloadChunk(index + 1, file); }); } } else { file.downloadPersentage = parseInt((index / chunkTotal) * 100); downloadChunk(index + 1, file); } } } </script> <style scoped> .main { display: flex; } .fileList { width: 400px; } .downloadList { width: 450px; } .title { margin-top: 5px; margin-bottom: 5px; } .downloading { margin-top: 10px; } .downloading .fileName { margin-left: 76px; margin-right: 30px; } .downloading .fileSize { /* margin-left: 70px; */ margin-right: 30px; } .downloading .progress { display: flex; } .progress .el-progress { /* font-size: 18px; */ width: 310px; } </style>
后端代碼
package com.cugb.bigfileupload.controller; import com.cugb.bigfileupload.servie.ChunkService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.util.Objects; @Controller @CrossOrigin public class DownLoadController { Logger logger = LoggerFactory.getLogger(getClass()); @Value("${file.path}") private String filePath; @Autowired private ChunkService chunkService; @PostMapping("/download") public void download(@RequestParam("md5") String md5, @RequestParam("fileName") String fileName, @RequestParam("chunkSize") Integer chunkSize, @RequestParam("chunkTotal") Integer chunkTotal, @RequestParam("index")Integer index, HttpServletResponse response) { String[] splits = fileName.split("\\."); String type = splits[splits.length - 1]; String resultFileName = filePath + md5 + "." + type; File resultFile = new File(resultFileName); long offset = (long) chunkSize * (index - 1); if(Objects.equals(index, chunkTotal)){ offset = resultFile.length() -chunkSize; } byte[] chunk = chunkService.getChunk(index, chunkSize, resultFileName,offset); logger.info("下載文件分片" + resultFileName + "," + index + "," + chunkSize + "," + chunk.length+","+offset); // response.addHeader("Access-Control-Allow-Origin","Content-Disposition"); response.addHeader("Content-Disposition", "attachment;filename=" + fileName); response.addHeader("Content-Length", "" + (chunk.length)); response.setHeader("filename", fileName); response.setContentType("application/octet-stream"); ServletOutputStream out = null; try { out = response.getOutputStream(); out.write(chunk); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } }
4.3 數(shù)據(jù)庫設(shè)計(jì)
4.3.1 概念結(jié)構(gòu)設(shè)計(jì)
數(shù)據(jù)庫設(shè)計(jì)只有倆個(gè)表,一個(gè)file表來記錄已經(jīng)完整上傳的文件信息,一個(gè)chunk表用來記錄還未上傳完成的分片信息
5.1 大文件上傳實(shí)現(xiàn)
上傳頁面如圖13所示,有一個(gè)“選擇上傳文件”的按鈕,下面是顯示正在上傳文件的列表
圖13 上傳頁面首頁
我們選擇要上傳的文件,確認(rèn)上傳,首先會(huì)顯示解析進(jìn)度,當(dāng)解析完成后,就會(huì)開始上傳,并顯示上傳進(jìn)度和速度;同時(shí),我們可以選擇多個(gè)文件一同上傳;在上傳的同時(shí)我們還可以暫停上傳。如圖14所示
圖14 上傳文件中
當(dāng)文件上傳成功之后,就會(huì)彈窗提示文件上傳成功。如圖15所示
圖15 文件上傳成功
5.2 大文件下載實(shí)現(xiàn)
文件下載頁面如圖16所示,左邊是可以下載文件的列表,右邊是下載中的文件
當(dāng)所有的分片下載完成后,前端會(huì)將所有的分片合并成一個(gè)文件。如圖18所示
以上就是Java實(shí)現(xiàn)大文件的分片上傳與下載(springboot+vue3)的詳細(xì)內(nèi)容,更多關(guān)于Java 大文件分片上傳與下載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 使用Springboot+Vue實(shí)現(xiàn)文件上傳和下載功能
- 基于SpringBoot和Vue實(shí)現(xiàn)頭像上傳與回顯功能
- Vue?+?SpringBoot?實(shí)現(xiàn)文件的斷點(diǎn)上傳、秒傳存儲(chǔ)到Minio的操作方法
- springboot+vue實(shí)現(xiàn)阿里云oss大文件分片上傳的示例代碼
- springboot整合vue2-uploader實(shí)現(xiàn)文件分片上傳、秒傳、斷點(diǎn)續(xù)傳功能
- 利用Springboot+vue實(shí)現(xiàn)圖片上傳至數(shù)據(jù)庫并顯示的全過程
- Vue+Element+Springboot圖片上傳的實(shí)現(xiàn)示例
- Springboot+Vue-Cropper實(shí)現(xiàn)頭像剪切上傳效果
- springboot + vue+elementUI實(shí)現(xiàn)圖片上傳功能
相關(guān)文章
MyBatis Plus構(gòu)建一個(gè)簡單的項(xiàng)目的實(shí)現(xiàn)
這篇文章主要介紹了MyBatis Plus構(gòu)建一個(gè)簡單的項(xiàng)目的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11springboot+webmagic實(shí)現(xiàn)java爬蟲jdbc及mysql的方法
今天小編就為大家分享一篇springboot+webmagic實(shí)現(xiàn)java爬蟲jdbc及mysql的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08線上dubbo線程池耗盡CyclicBarrier線程屏障異常解決記錄
系統(tǒng)相關(guān)使用人員反饋系統(tǒng)故障,這篇文章主要介紹了線上dubbo線程池耗盡CyclicBarrier線程屏障異常解決的記錄,有需要的朋友可以借鑒參考下2022-03-03基于Java實(shí)現(xiàn)互聯(lián)網(wǎng)實(shí)時(shí)聊天系統(tǒng)(附源碼)
Netty?是一個(gè)利用?Java?的高級(jí)網(wǎng)絡(luò)的能力,隱藏其背后的復(fù)雜性而提供一個(gè)易于使用的?API?的客戶端/服務(wù)器框架。本文將利用它實(shí)現(xiàn)互聯(lián)網(wǎng)實(shí)時(shí)聊天系統(tǒng),感興趣的可以了解一下2022-09-09Java通過word模板實(shí)現(xiàn)創(chuàng)建word文檔報(bào)告
這篇文章主要為大家詳細(xì)介紹了Java如何通過word模板實(shí)現(xiàn)創(chuàng)建word文檔報(bào)告的教程,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以學(xué)習(xí)一下2022-09-09從前端Vue到后端Spring Boot接收J(rèn)SON數(shù)據(jù)的正確姿勢(shì)(常見錯(cuò)誤及問題)
這篇文章主要介紹了從前端Vue到后端Spring Boot接收J(rèn)SON數(shù)據(jù)的正確姿勢(shì)(常見錯(cuò)誤及問題),本文將從前端Vue到后端Spring Boot,詳細(xì)介紹接收J(rèn)SON數(shù)據(jù)的正確姿勢(shì),幫助開發(fā)人員更好地處理JSON數(shù)據(jù),感興趣的朋友一起看看吧2024-02-02Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法
下面小編就為大家分享一篇Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助2017-11-11Java中的阻塞隊(duì)列BlockingQueue使用詳解
這篇文章主要介紹了Java中的阻塞隊(duì)列BlockingQueue使用詳解,阻塞隊(duì)列是一種線程安全的數(shù)據(jù)結(jié)構(gòu),用于在多線程環(huán)境下進(jìn)行數(shù)據(jù)交換,它提供了一種阻塞的機(jī)制,當(dāng)隊(duì)列為空時(shí),消費(fèi)者線程將被阻塞,直到隊(duì)列中有數(shù)據(jù)可供消費(fèi),需要的朋友可以參考下2023-10-10