1. 程式人生 > >eggJS大檔案分片上傳與合併

eggJS大檔案分片上傳與合併

前臺上傳使用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,
};