線上直播系統原始碼,前後端大檔案上傳程式碼分析
<template> <div> <div @click.native="hanldeClick" class="upload_container"> <input name="請上傳檔案" type="file" ref="uploadRef" @change="handleChange" :multiple="multiple" :accept="accept"/> </div> <div ref="uploadSubmit" @click="handleUpload()">上傳</div> <div><span ref="uploadResultRef"></span></div> <div>md5Value:{{fileSparkMD5}}</div> </div> </template>
<script setup> import { ref,onMounted } from 'vue' import { ElMessage, } from 'element-plus' import SparkMD5 from 'spark-md5'; import { makePostRequest } from './axios.js'; defineProps({ multiple:{ type:Boolean, default:true }, accept:{ type:Array, default:[] } }) const uploadRef = ref(); // input 的ref const uploadResultRef = ref(null); //上傳結果展示 const fileSparkMD5 = ref([]); // 檔案MD5 weiyi標識 const fileChuncks = ref([]); // 檔案分片list const chunckSize = ref(1*1024*1024); // 分片大小 const promiseArr = []; // 分片上傳promise集合 const isUploadChuncks = ref([]); // 返回 [1,1,1,0,0,1] 格式陣列(這裡需要服務端返回的值是按照索引正序排列),標識對應下標上傳狀態 已上傳:1 ,未上傳:0 const uploadProgress = ref(0); // 上傳進度 const uploadQuantity = ref(0); // 總上傳數量 //檢測檔案是否上傳過, const checkFile = async (md5) => { const data = await makePostRequest('http://127.0.0.1:3000/checkChuncks', {md5}); if (data.length === 0) { return false; } const {file_hash:fileHash,chunck_total_number:chunckTotal} = data[0]; // 檔案的資訊,hash值,分片總數,每條分片都是一致的內容 if(fileHash === md5) { const allChunckStatusList = new Array(Number(chunckTotal)).fill(0); // 檔案所有分片狀態list,預設都填充為0(0: 未上傳,1:已上傳) const chunckNumberArr = data.map(item => item.chunck_number); // 遍歷已上傳的分片,獲取已上傳分片對應的索引 (chunck_number為每個檔案分片的索引) chunckNumberArr.forEach((item,index) => { // 遍歷已上傳分片的索引,將對應索引賦值為1,代表已上傳的分片 (注意這裡,服務端返回的值是按照索引正序排列) allChunckStatusList[item] = 1 }); isUploadChuncks.value = [...allChunckStatusList]; return true; // 返回是否上傳過,為下面的秒傳,斷點續傳做鋪墊 }else { return false; } } //檔案上傳function const handleUpload = async () => { const fileInput = uploadRef.value; const file = fileInput.files[0]; // 未選擇檔案 if (!file) { ElMessage({message:'請選擇檔案',type:'warning'}); return } //迴圈計算檔案MD5,多檔案上傳時候 const fileArr = fileInput.files; for(let i = 0; i < fileArr.length; i++){ const data = await getFileMD5(fileArr[i]); fileSparkMD5.value.push({md5Value:data,fileKey:fileArr[i].name}); sliceFile(fileArr[i]); } //檢測檔案是否已上傳過 const {md5Value} = fileSparkMD5.value[0]; // 這裡已單檔案做示例,預設取第 一個檔案 const isFlag = await checkFile(md5Value); //是否上傳過 if(isFlag) { const hasEmptyChunk = isUploadChuncks.value.findIndex(item => item === 0); //在所有的分片狀態中找到未上傳的分片,如果沒有表示已完整上傳 //上傳過,並且已經完整上傳,直接提示上傳成功(秒傳) if(hasEmptyChunk === -1) { ElMessage({message:'上傳成功',type:'success'}); return; }else { //上傳缺失的分片檔案,注意這裡的索引,就是檔案上傳的序號 for(let k = 0; k < isUploadChuncks.value.length; k++) { if(isUploadChuncks.value[k] !== 1) { const {md5Value,fileKey} = fileSparkMD5.value[0]; //單檔案處理,多檔案需要遍歷匹配對應的檔案 let data = new FormData(); data.append('totalNumber',fileChuncks.value.length); // 分片總數 data.append("chunkSize",chunckSize.value); // 分片大小 data.append("chunckNumber",k); // 分片序號 data.append('md5',md5Value); // 檔案weiyi標識 data.append('name',fileKey); // 檔名稱 data.append('file',new File([fileChuncks.value[k].fileChuncks],fileKey)) //分片檔案 httpRequest(data,k,fileChuncks.value.length); } } } }else { //未上傳,執行完整上傳邏輯 fileChuncks.value.forEach((e, i)=>{ const {md5Value,fileKey} = fileSparkMD5.value.find(item => item.fileKey === e.fileName); let data = new FormData(); data.append('totalNumber',fileChuncks.value.length); data.append("chunkSize",chunckSize.value); data.append("chunckNumber",i); data.append('md5',md5Value); //檔案weiyi標識 data.append('name',fileKey); data.append('file',new File([e.fileChuncks],fileKey)) httpRequest(data,i,fileChuncks.value.length); }) } let uploadResult = uploadResultRef.value; Promise.all(promiseArr).then((e)=>{ uploadResult.innerHTML = '上傳成功'; // pormise all 機制,所有上傳完畢,執行正常回撥,開啟合併檔案操作 mergeFile(fileSparkMD5.value,fileChuncks.value.length); }).catch(e=>{ ElMessage({message:'檔案未上傳完整,請繼續上傳',type:'error'}); uploadResult.innerHTML = '上傳失敗'; }) } //file:檔案內容,nowChunck:當前切片的排序,totalChunck:總的切片數量 const httpRequest = (file,nowChunck,totalChunck) => { const curPormise = new Promise((resolve,reject)=>{ let uploadResult = uploadResultRef.value; let xhr = new XMLHttpRequest(); // 當上傳完成時呼叫 xhr.onload = function() { if (xhr.status === 200) { // uploadResult.innerHTML = '上傳成功'+ xhr.responseText; //大檔案上傳進度 uploadQuantity.value ++; // 注意這裡,因為是分片,所以進度除以總數就是當前上傳的進度 uploadProgress.value = uploadQuantity.value / totalChunck * 100; uploadResult.innerHTML='上傳進度:' + uploadProgress.value + '%'; return resolve(nowChunck) } } xhr.onerror = function(e) { return reject(e) } // 傳送請求 xhr.open('POST', 'http://127.0.0.1:3000/upload', true); xhr.send(file); }) // 將所有請求推入pormise集合中 promiseArr.push(curPormise); } //獲取檔案MD5,注意這裡谷歌瀏覽器有最大檔案限制當檔案大於2G時瀏覽器無法讀取檔案 const getFileMD5 = (file) => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = (e) =>{ const fileMd5 = SparkMD5.ArrayBuffer.hash(e.target.result) resolve(fileMd5) } fileReader.onerror = (e) =>{ reject('檔案讀取失敗',e) } fileReader.readAsArrayBuffer(file); }) } //檔案切片 const sliceFile = (file) => { //檔案分片之後的集合 const chuncks = []; let start = 0 ; let end; while(start < file.size) { end = Math.min(start + chunckSize.value,file.size); //slice 擷取檔案位元組 chuncks.push({fileChuncks:file.slice(start,end),fileName:file.name}); start = end; } fileChuncks.value = [...chuncks]; } //合併檔案 const mergeFile = async (fileInfo,chunckTotal) => { const { md5Value,fileKey } = fileInfo[0]; const params = { totalNumber:chunckTotal, md5:md5Value, name:fileKey } const response = await makePostRequest('http://127.0.0.1:3000/merge', params); ElMessage({message:'上傳成功',type:'success'}); } </script>
"dependencies": { "fs-extra": "^11.2.0", "koa": "^2.14.2", "koa-body": "^6.0.1", "koa-multer": "^1.0.2", "koa-router": "^12.0.0", "koa-static": "^5.0.0", "koa2-cors": "^2.0.6", "mysql": "^2.18.1", "nodemon": "^2.0.22" }
const insertFile = async (md5,name,totalNumber,chunkSize,chunckNumber) => { const sql = `INSERT INTO fileupload.chunck_list (file_hash,chunck_number,chunck_size,chunck_total_number,file_name) VALUES ('${md5}','${chunckNumber}','${chunkSize}','${totalNumber}','${name}')`; const result = await connection.query(sql); console.log(result + '資料插入成功') } router.post("/upload",upload.single("file"), async (ctx, next) => { const { totalNumber, // 分片總數 chunckNumber, // 分片序號 chunkSize, // 分片大小 md5, // 檔案hash值(唯 一) name // 檔名稱 } = ctx.req.body; //指定hash檔案路徑 const chunckPath = path.join(uploadPath, md5,'/'); if(!fs.existsSync(chunckPath)){ fs.mkdirSync(chunckPath); } //移動檔案到指定目錄 fs.renameSync(ctx.req.file.path,chunckPath + md5 + '-' + chunckNumber); insertFile(md5,name,totalNumber,chunkSize,chunckNumber) ctx.status = 200; ctx.res.end('Success'); })
router.post("/merge", async (ctx, next) => { const {totalNumber,md5,name} = ctx.request.body; try { //分片儲存得資料夾路徑 const chunckPath = path.join(uploadPath, md5, '/'); //建立合併後的檔案 console.log(name+'我是影片地址') const filePath = path.join(uploadPath, name); //讀取對應hash資料夾下的所有分片檔名稱 const chunckList = fs.existsSync(chunckPath) ? fs.readdirSync(chunckPath) : []; console.log(chunckList+'我是影片地址') //建立儲存檔案 fs.writeFileSync(filePath,''); //判斷切片是否完整 console.log(chunckList.length,totalNumber,'我是總地址,和分片地址') if(chunckList.length !== totalNumber){ ctx.status = 500; ctx.message = 'Merge failed, missing file slices'; // ctx.res.end('error'); process.exit(); } for(let i = 0; i < totalNumber; i++){ const chunck = fs.readFileSync(chunckPath +md5+ '-' + i); //寫入當前切片 fs.appendFileSync(filePath,chunck); //刪除已合併的切片 fs.unlinkSync(chunckPath + md5 + '-' + i); } //刪除空資料夾 fs.rmdirSync(chunckPath); ctx.status = 200; ctx.message = 'success'; }catch (e) { ctx.status = 500; ctx.res.end('合併失敗'); } })
router.post("/checkChuncks", async (ctx, next) => { try { const {md5} = ctx.request.body; const queryResult = await new Promise((resolve,reject)=>{ const query = `SELECT (SELECT count(*) FROM chunck_list WHERE file_hash = '${md5}') as all_count, id as chunck_id,file_hash,chunck_number,chunck_total_number FROM chunck_list WHERE file_hash = '${md5}' GROUP BY id ORDER BY chunck_number`; connection.query(query,async (error,results,fields)=>{ if(error) reject(error); resolve(results || []); }); }) ctx.status = 200; ctx.body = queryResult; }catch (e) { ctx.status = 500; ctx.res.end('error'); } })
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/69978258/viewspace-3012431/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 線上直播原始碼,js 檔案上傳 圖片上傳 傳輸速度計算原始碼JS
- 線上直播系統原始碼,當前版本號頁面呈現原始碼
- ElementUI 下載檔案前後端程式碼UI後端
- Vue實現多檔案上傳功能(前端 + 後端程式碼)Vue前端後端
- 【全開源】AJAX家政系統原始碼小程式前後端開源原始碼原始碼後端
- 檔案上傳/下載後臺程式碼
- 線上直播系統原始碼,滑鼠懸停後彈出氣泡原始碼
- PHP檔案上傳原始碼分析(RFC1867)PHP原始碼
- 線上直播系統原始碼,Node.js中使用Koa實現上傳圖片功能原始碼Node.js
- javaWeb上傳檔案程式碼JavaWeb
- 影片直播系統原始碼,非同步處理實現程式碼分析原始碼非同步
- 鴻蒙輕核心原始碼分析:檔案系統LittleFS鴻蒙原始碼
- 線上直播系統原始碼,實現搜尋後介面顯示商品列表效果原始碼
- PHP檔案分享系統原始碼PHP原始碼
- 教育直播原始碼:如何進行線上教育系統搭建?原始碼
- 線上直播系統原始碼,自定義底部 BottomNavigationBar原始碼Navigation
- 線上直播系統原始碼,flutter 巢狀滑動實現原始碼Flutter巢狀
- 線上直播系統原始碼,彈出警告/提示類彈窗原始碼
- SpringMVC實現多檔案上傳原始碼SpringMVC原始碼
- SpringBoot+Vue.js前後端分離實現大檔案分塊上傳Spring BootVue.js後端
- 語音直播系統原始碼與視訊直播系統原始碼哪些區別原始碼
- [提問交流]上傳web原始碼檔案模板的線上預覽實現方法?Web原始碼
- vue+springboot檔案上傳下載(前後端分離)VueSpring Boot後端
- 線上直播系統原始碼,vue實現搜尋文字高亮功能原始碼Vue
- 線上直播系統原始碼,迴圈滾動RecyclerView的實現原始碼View
- 線上直播系統原始碼,Dart-Flutter DateTime日期轉換原始碼DartFlutter
- 使用Vue+go實現前後端檔案的上傳下載,csv檔案上傳下載可直接照搬VueGo後端
- 線上直播系統原始碼,進入新的介面後自動重新整理內容原始碼
- Android系統原始碼分析-Broadcast傳送Android原始碼AST
- 米安程式碼審計 05 檔案上傳漏洞
- Java檔案上傳功能程式碼——普遍適用Java
- 線上直播系統原始碼,實現在圖片上塗鴉並記錄塗鴉軌跡原始碼
- 呼叫支付介面,實現直播帶貨系統原始碼的線上支付原始碼
- 線上直播系統原始碼,平臺彈窗自適應裝置原始碼
- 線上直播系統原始碼,滾動式內容展示控制元件原始碼控制元件
- 線上直播系統原始碼,Vue3中全域性配置 axios原始碼VueiOS
- 教育直播原始碼:線上教育系統搭建要注重這些方面原始碼
- 線上直播原始碼開發IOS端問題解決方案原始碼iOS