eggJS大檔案分片上傳與合併
阿新 • • 發佈:2018-12-20
前臺上傳使用vue+axios
前臺程式碼:
// 計算分片總數 for (let i = 0; i < Math.ceil(this.file.size / this.uploadFragment.fragment); i++) { this.uploadFragment.fragmentSum.push(i); } const upload = function(arr, that) { const frag = arr.shift(); let start = frag * that.uploadFragment.fragment; // 當前分片結束下標 let end = Math.min(that.file.size, start + that.uploadFragment.fragment); // 檔案內容 let param = new FormData(); // 建立form物件 param.append('file', that.file.slice(start, end), that.file.name); const reqData = { param, fragmentIndex: frag+1, // 還可以加上別的資料上傳,用請求header帶上 }; that.teacherUploadVideo(reqData).then(result => { that.uploadFragment.fragmentList.push(result); // 通知父元件已經開始上傳,開啟進度條,並帶上進度引數 that.$emit('onUploading', Math.round(end / that.file.size * 100)); // 最後一次請求就傳送合併請求 if (arr.length <= 0) { const data = { // 需要傳送到後臺的資料 } that.mergeVideo(data).then(res => { // 清空檔案物件 that.file = null; // 通知父元件已經上傳完畢,關閉載入進度條 that.$emit('onUploadSuccess', 'video', '1'); }); } else { upload(arr, that); } }).catch(err => { throw new Error(err); }); } upload(this.uploadFragment.fragmentSum, this);
eggJS的邏輯:
// 分片上傳 async addFragVideo() { const { ctx } = this; try { const stream = await ctx.getFileStream(); // 這些都是用header傳遞的資料 const courseName = decodeURIComponent(ctx.request.headers['course-name']); const sectionId = ctx.request.headers['section-id']; // fragmentIndex是片段編號,合併時需要 const fragmentIndex = ctx.request.headers['fragment-index']; const courseId = ctx.helper.getSaltyMd5(courseName, 'courseName'); const uplaodBasePath = `${pc.video}/${courseId}/${sectionId}`; // 具體的檔案上傳可以參考我之前的檔案上傳文章 const filename = await ctx.helper.handleSaveFragDoc(uplaodBasePath, fragmentIndex, stream); // 返回給前端,前端會把檔案記錄到數組裡,用於視訊合併 ctx.body = { data: filename, }; } catch (error) { throw new Error(error); } } // 合併視訊 async mergeVideo() { try { const { ctx } = this; const { courseName, chapterName, sectionName } = ctx.request.body; const doc = ctx.helper.getSaltySha1(courseName, 'courseName'); const courseId = ctx.helper.getSaltyMd5(courseName, 'courseName'); const sectionId = ctx.request.body.id; // 一個存放分片視訊順序的陣列 const fragmentList = ctx.request.body.fragmentList; // 合併出來的視訊地址 const uplaodBasePath = `${pc.video}/${courseId}/${sectionId}/prepare.avi`; // 用於拼接分片檔案的基礎目錄,如`${pc.video}/${courseId}/${sectionId}/chunks/01.avi` const dirName = `${pc.video}/${courseId}/${sectionId}`; const tag = await ctx.helper.mergeFrag(dirName, uplaodBasePath, fragmentList); ctx.body = { data: tag, }; } catch (error) { throw new Error(error); } } // helper外掛 'use strict'; const fs = require('fs'); const path = require('path'); // 合併分片 function mergeChunks(dirName, fileName, chunks) { const chunkPaths = chunks.map(function(name) { return path.join(dirName, 'chunks', name); }); // 採用Stream方式合併 const targetStream = fs.createWriteStream(fileName); const readStream = function(chunkArray) { const path = chunkArray.shift(); const originStream = fs.createReadStream(path); originStream.pipe(targetStream, { end: false }); originStream.on('end', function() { // 刪除檔案 fs.unlinkSync(path); if (chunkArray.length > 0) { readStream(chunkArray); } else { targetStream.close(); originStream.close(); } }); }; readStream(chunkPaths); } module.exports = { mergeChunks, };