首页 > 编程语言 >在线直播系统源码,前后端大文件上传代码分析

在线直播系统源码,前后端大文件上传代码分析

时间:2024-04-13 09:00:43浏览次数:21  
标签:文件 const 端大 value 源码 file 分片 上传

在线直播系统源码,前后端大文件上传代码分析

前端代码:

<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 唯一标识
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); // 文件唯一标识
                    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); //文件唯一标识
            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,接口缺乏严谨性,小伙伴可以自行完善!

以上就是在线直播系统源码,前后端大文件上传代码分析, 更多内容欢迎关注之后的文章

 

标签:文件,const,端大,value,源码,file,分片,上传
From: https://www.cnblogs.com/yunbaomengnan/p/18132487

相关文章

  • typora上传图片问题
    typora上传博客园教程来源https://www.bilibili.com/video/BV1Rv4y1Y7KH/?spm_id_from=333.337.search-card.all.click&vd_source=66ac5da514fe1be9729ebba538a11951下载工具链接:https://pan.baidu.com/s/1e1iVqoA7vAsBT8lir11z2Q?pwd=5axe提取码:5axe配置步骤开启控制面板-......
  • Linux0.12内核源码解读(2)-Bootsect.S
    大家好,我是呼噜噜,在上一篇文章聊聊x86计算机启动发生的事?我们了解了x86计算机启动过程,MBR、0x7c00是什么?其中当bios引导结束后,操作系统接过计算机的控制权后,发生了哪些事?本文将揭开迷雾的序章-Bootsect.S回顾计算机启动过程我们先来回顾一下,上古时期计算机按下电源键的启动过程,......
  • 即时通讯技术文集(第36期):《跟着源码学IM》系列专题 [共12篇]
    为了更好地分类阅读52im.net总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第36 期。[-1-] 跟着源码学IM(一):手把手教你用Netty实现心跳机制、断线重连机制[链接] http://www.52im.net/thread-2663-1-1.html[摘要] 说到用Netty来开发IM或推送系统,以一个......
  • nexus 3.49 手动上传本地jar包记录
    1.登录nexus网页管理平台:http://192.168.3.100:8081/2.下载要上传的jar包https://releases.aspose.com/zh/diagram/java/22-12/ 3.选择要上传的库 4.上传 5.找到验证对应包,并使用 ......
  • lodash已死?radash最全使用介绍(附源码说明)—— Array方法篇(3)
    前言我们已经介绍了radash的相关信息和部分Array相关方法,详情可前往主页查看;本篇我们继续介绍radash中Array的相关方法;下期我们将介绍解析radash中剩余的Array相关方法,并整理出Array方法使用目录,包括文章说明和脑图说明。Radash的Array相关方法详解iterate:把一个函数迭代......
  • hashMap源码分析
    先分析hashMap的put方法:当执行put操作时会调用底层的putVal方法,以下是这个方法的分析执行Put方法时会先判断当前哈希表是否为空,为空则先扩容,然后计算出hash值对应的索引,判断索引位置上的节点是否为空,空则插入这个新节点。否则便要判断节点上的key是不是和原先的key相同,相同则进行......
  • Flink源码学习(4) TaskManager从节点启动分析
    taskManager是flink的worker节点,负责slot的资源管理和task执行一个taskManager就是一台服务器的抽象TaskManager基本资源单位是slot,一个作业的task会部署在一个TM的slot上运行,TM会负责维护本地的slot资源列表,并与Master和JobManager进行通信启动主类:TaskManagerRunnerTaskMan......
  • 如何将自己的SpringBoot项目上传到GitHub上面!
    我最近想把之前做过的一些Java项目上传到GitHub上面,在网上找教程,失败了很多次终于完成了,先将方法介绍给大家!对了,先确保自己电脑装git了!!下载地址(镜像站下载快):https://mirrors.tuna.tsinghua.edu.cn/github-release/git-for-windows/git/LatestRelease/选择自己的电脑版本,一......
  • 基于dremio 安装包进行源码依赖包maven 私服重建的一个思路
    dremio25.0版本已经发布了,但是如果希望自己源码构建,但是缺少一些依赖造成编译会有问题,但是我们可以直接基于官方提供的下载包的文件进行maven私服的重建,以下说明下简单流程参考流程下载软件包这个可以从dremio官网下载到最好选择一个可以构建的分支本地构建下此步......
  • ios ipa包上传需要什么工具
    ​ 目录 前言一、IPA包的原理二、IPA包上传的步骤2.apk软件制作工具创建应用程序3.构建应用程序4.生成证书和配置文件5.打包IPA包6.上传IPA包三、总结 前言iOSIPA包是iOS应用程序的安装包,可以通过iTunes或者其他第三方应用商店安装到iOS设备上。在开发iOS应用......