線上直播系統原始碼,前後端大檔案上傳程式碼分析

zhibo系统开发發表於2024-04-13

線上直播系統原始碼,前後端大檔案上傳程式碼分析

前端程式碼:

<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>

服務端程式碼:

這裡用node.js 代替java在本地模擬一個服務端。下面是node的所需依賴

 "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');
    }
})

自此服務端程式碼完結,這裡只是一個簡單的demo,介面缺乏嚴謹性,小夥伴可以自行完善!

以上就是線上直播系統原始碼,前後端大檔案上傳程式碼分析, 更多內容歡迎關注之後的文章


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/69978258/viewspace-3012431/,如需轉載,請註明出處,否則將追究法律責任。

相關文章