大文件切片上传(Vue+NodeJS)

思路

大文件前台切成若干份2M小文件,分别传输给后台,后台分别写入硬盘,在最后一片被后台接收后,读取所有切片并合并成为原始大文件

方式

主要实现两种方式的大数据切片传输

  1. 前台切片,后台合并
  2. node后台切片,另一服务器端合并

Code

1 前台切片

大文件上传时,前端切片,上传后,后端组合

先上界面
在这里插入图片描述

前台

<template>
  <div>
      <div style="margin:20px">
          <h2>Upload Large Files</h2>
          <br>
            <el-row>
                <el-col :span="4">
                <input id="uppic"    type="file" multiple @change="selectedFiles()"   ref="upload">
                    <label for="uppic">
                        <div id="wrapper">
                            <div id="cell">
                                <span>
                                
                                <i class="el-icon-folder-opened"></i>
                                Choose  Files
                                </span>
                            </div>
                        </div>
                    
                    </label>
                </el-col>
                <el-col :span="10" :offset="1">
                <el-button id="userbtn" class="bg-main tc userbtn" type="success" @click="upload">
                   <i class="el-icon-upload"></i>
                    Upload
                    </el-button>
                </el-col>
            </el-row>
            <el-row v-if="fileList.length!=0">
                 <el-table
                    :data="fileList"
                    style="width: 100%">
                    <el-table-column
                        prop="name"
                        label="Name"
                        width="500">
                    </el-table-column>
                    <el-table-column
                        prop="size"
                        label="Size (KB)"
                        width="180">
                    </el-table-column>
                    
                </el-table>
               
            </el-row>
            <el-row>
                <el-col :span="20">
                        <el-progress :text-inside="true" :stroke-width="26" :percentage="percentage"></el-progress>
                        
                </el-col>

            </el-row>
       </div>
         
        
  </div>
</template>

<script>
import async from 'async'
  export default {
    data: () => ({
      percentage:0,
      fileList:[]
    }),
    methods:{
         upload(){
            this.percentage=0
            let _this=this

            for(let k in this.$refs.upload.files){
                let file=this.$refs.upload.files[k],//上传文件主体
                name = file.name,        //文件名
                size = file.size,        //总大小
                succeed = 0;  //当前上传数
                let shardSize = 2 *1024*1024,    //以2MB为一个分片
                shardCount = Math.ceil(size / shardSize);  //总片数

                /*生成上传分片文件顺充,通过async.eachLimit()进行同步上传
                    attr里面是[0,1,2,3...,最后一位]    
                */
                let attr=[];
                for(let i=0;i<shardCount;++i){
                    attr.push(i);
                }
                async.eachLimit(attr,1,async function(item,callback){
                    let i=item;
                    let start = i * shardSize,//当前分片开始下标
                    end = Math.min(size, start + shardSize);//结束下标

                    //构造一个表单,FormData是HTML5新增的
                    let form = new FormData();
                    form.append("data", file.slice(start,end));  //slice方法用于切出文件的一部分
                    form.append("name", name);//文件名字
                    form.append("total", shardCount);  //总片数
                    form.append("index", i + 1);   //当前片数'

                //api是后台上传接口
                    await _this.$axios.post(‘api’,form,{
                        timeout: 120*1000
                    })
                    .then(res=>{
                        ++succeed;
                            
                            /*返回code为0是成功上传,1是请继续上传*/
                            if(res.data.code==0){
                                console.log(res.data.mssg);
                                 _this.percentage=0
                                  _this.fileList.shift()
                                 _this.$message({
                                   type:'success',
                                   message:'upload finish'+file.name,
                                   showClose:true
                                   
                               })
                            }else if(res.data.code==1){

                                console.log(res.data.msg);
                            }
                            //生成当前进度百分比
                            _this.percentage=Math.round(succeed/shardCount*100);
                     
                            callback()
                    })

                },function(err){
                    console.log('上传成功');
                });     



            }

           




        },
        selectedFiles(){
            console.log('selected',this.$refs.upload.files)
            this.fileList= [...this.$refs.upload.files]
        }
        

    },
    watch:{
        
    }
     
  }
</script>

<style scoped>
#uppic {
    width: 0.1px; 
    height: 0.1px; 
    opacity: 0; 
    overflow: hidden; 
    position: absolute; 
    z-index: -1;
}

.el-row {
    margin-bottom: 20px;
}
label{
    color: aquamarine;
    background-color: #796e02e3;
    height: 40px;
    display: block;
    text-align: center;
}
#wrapper {display:table;height:40px;margin:0 auto;color:rgb(255, 255, 255);}
#cell{display:table-cell; vertical-align:middle;}
</style>

后台

exports.upload=function user(req,res,config){ 
     
    let fs=require('fs');
    let async = require('async');//异步模块
    let formidable=require('formidable')
    let form=new formidable.IncomingForm();
    

    //设置编辑
    form.encoding = 'utf-8';

    let dirPath=__dirname+"/../uploadFiles/tep/";
     
    //设置文件存储路径
    form.uploadDir = dirPath;
    //设置单文件大小限制
   // form.maxFilesSize = 200 * 1024 * 1024;
    /*form.parse表单解析函数,fields是生成数组用获传过参数,files是bolb文件名称和路径*/
    form.parse(req, function (err,fields,files) {
         files=files['data'];//获取bolb文件
         let index=fields['index'];//当前片数
         let total=fields['total'];//总片数
         let name=fields['name'];//文件名称
         let url= dirPath+'/'+name.split('.')[0]+'_'+index+'.'+name.split('.')[1];//临时bolb文件新名字
         fs.renameSync(files.path,url);//修改临时文件名字
    
         try{
            if(index==total){//当最后一个分片上传成功,进行合并
                /*
                    检查文件是存在,如果存在,重新设置名称
                */
                let uid=uuid.v4()
         
                fs.mkdirSync(__dirname+"/../uploadFiles/"+uid)
                let pathname=__dirname+"/../uploadFiles/"+uid+'/'+name;//上传文件存放位置和名称
                fs.access(pathname,fs.F_OK,(err) => {
                    if(!err){   
                        let myDate=Date.now();
                        pathname=dirPath+'/'+myDate+name;
                        console.log(pathname);

                    }
                });
                //这里定时,是做异步串行,等上执行完后,再执行下面
                setTimeout(function(){
                    /*进行合并文件,先创建可写流,再把所有BOLB文件读出来,
                        流入可写流,生成文件
                        fs.createWriteStream创建可写流   
                        aname是存放所有生成bolb文件路径数组:
                        ['Uploads/img/3G.rar1','Uploads/img/3G.rar2',...]
                    */
                    let writeStream=fs.createWriteStream(pathname);
                    let aname=[];
                    for(let i=1;i<=total;i++){
                        let url=dirPath+'/'+name.split('.')[0]+'_'+i+'.'+name.split('.')[1];
                        aname.push(url);
                    }

                    //async.eachLimit进行同步处理
                    async.eachLimit(aname,1,function(item,callback){
                        //item 当前路径, callback为回调函数
                        fs.readFile(item,function(err,data){    
                           if(err)throw err;
                           //把数据写入流里
                            writeStream.write(data);
                            //删除生成临时bolb文件              
                            fs.unlink(item,function(){console.log('删除成功');})
                            callback();
                        });
                    },function(err){
                        if (err) throw err;
                        //后面文件写完,关闭可写流文件,文件已经成生完成
                        writeStream.end();



                        //返回给客服端,上传成功
                        let data=JSON.stringify({'code':0,"data": {
                            "source_store_id": uid,
                            "file_name": name
                        }});
                        res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 
                        res.end(data);//返回数据    
                    });
                },50);

            }else{//还没有上传文件,请继续上传
                let data=JSON.stringify({'code':1,'msg':'继续上传'});
                res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 
                res.end(data);//返回数据    
            }
         }catch(err){
             console.log(err)
         }
       
    });
   
}; 

2 Node后台切片,传输给另外服务器

关键点在于,Node后台的切片因为没有HTML中的file api, 无法利用File对象继承自Blob的slice方法,以进行切片,进一步去做合并。经过尝试可以利用Node提供的Buffer中的slice进行切片

大部分逻辑同上,故这里只贴出关键代码

客户端Node后台

 let distFile = fs.readFileSync(
          __dirname + "/../dataStorage/" + serviceItem.id + ".zip"
        );
        let largrFile = Buffer.from(distFile);
        let name = serviceItem.name+'.zip', //文件名
          size = largrFile.length, //总大小
          succeed = 0; //当前上传数
        let shardSize = 2 * 1024 * 1024, //以2MB为一个分片
          shardCount = Math.ceil(size / shardSize); //总片数

        /*生成上传分片文件顺充,通过async.eachLimit()进行同步上传
                    attr里面是[0,1,2,3...,最后一位]    
                */
        let attr = [];
        for (let i = 0; i < shardCount; ++i) {
          attr.push(i);
        }
        
        let rp=res;
        try{
        async.eachLimit(
          attr,
          1,
          async function (item, callback) {
            let i = item;
            let start = i * shardSize, //当前分片开始下标
              end = Math.min(size, start + shardSize); //结束下标

            let minFile = largrFile.slice(start, end);

            let obj = {};
            obj["data"] = minFile;
            obj["name"] = name;
            obj["total"] = shardCount;
            obj["index"] = i + 1;
            obj["size"] = size;
            obj["start"] = start;
            obj["end"] = end;
            // http://111.229.14.128:8899/largeBKend
            // http://localhost:8898/upload 测试
            await axios
              .post("api", obj, {
                timeout: 1000*60*60,
              })
              .then((axiosRes) => {
                ++succeed;
                /*返回code为0是成功上传,1是请继续上传*/
                if (axiosRes.data.code == 0) {
                    console.log(axiosRes.data.data);
                    console.log('大文件切上传完成,拿回数据索引,准备转发')
                    let ws=new WebSocket('ws://111.229.14.128:1708');
                    let msg={
                      msg:'Migration',
                      bk:true,
                      serviceDownloadId: axiosRes.data.data.source_store_id,
                      fromToken: req.query.fromToken,
                      targetToken: req.query.targetToken
                   }
                   ws.on('open',()=>{
                    ws.send(
                      JSON.stringify(msg)
                    )
                    ws.close()
                   })
                   ws.on('message',(data)=>{
                    if(data=='node offline'){
                      console.log('node offline')
                      ws.close()
                    }else{
                     console.log(data)

                    }
                  })

                } else if (axiosRes.data.code == 1) {
                  console.log(axiosRes.data.msg);
                  
                  let data=JSON.stringify({'code':1});
                  rp.end(data);//返回数据
                  
                }
                //生成当前进度百分比
                // _this.percentage=Math.round(succeed/shardCount*100);
                console.log(
                  "进度: " + Math.round((succeed / shardCount) * 100)
                );
            
             
                // callback()
              });
          },
          function (err) {
            if(err){
              console.log(err)
              rp.send({code:-1})
              return
            }
          }
        );
        }catch(err){
          console.log(err)
        }

服务端后台


    let fs=require('fs');
    let async = require('async');//异步模块
    
        // 切片的临时存储
         let dirPath=__dirname+"/../uploadFiles/tep/";
    
         let index=req.body['index'];//当前片数
         let total=req.body['total'];//总片数
         let name=req.body['name'];//文件名称
       
         let url= dirPath+'/'+name.split('.')[0]+'_'+index+'.'+name.split('.')[1];//临时bolb文件新名字
      
         try{
            if(index==total){//当最后一个分片上传成功,进行合并
                /*
                    检查文件是存在,如果存在,重新设置名称
                */
          
                    let bf=Buffer.from(req.body['data'])
                    fs.writeFileSync(url,bf)
                
                    let uid=uuid.v4()
                    fs.mkdirSync(__dirname+"/../uploadFiles/"+uid)
                    let pathname=__dirname+"/../uploadFiles/"+uid+'/'+name;//上传文件存放位置和名称
                    //这里定时,是做异步串行,等上执行完后,再执行下面
                    setTimeout(function(){
                    let writeStream=fs.createWriteStream(pathname);
                    let aname=[];
                    for(let i=1;i<=total;i++){
                        let url=dirPath+'/'+name.split('.')[0]+'_'+i+'.'+name.split('.')[1];
                        aname.push(url);
                    }
                    let bf=[]
                    //async.eachLimit进行同步处理
                    async.eachLimit(aname,1,function(item,callback){
                        //item 当前路径, callback为回调函数
                        fs.readFile(item,function(err,data){    
                           if(err)throw err;
                           //把数据写入流里,这里有两种方式
                           // 第一种是利用stream边读边写,这种方式相对于第二种对于内存更加友好
                            writeStream.write(data);
                            // 第二种是利用拼接buffer进行,但由于是将所有文件读取buffer并放在bf数组中进行拼接,可能由于bf数组过大导致内存溢出,所以舍弃
                            // bf.push(data)

                            //删除生成临时bolb文件              
                            fs.unlink(item,function(){console.log('删除成功');})
                            callback();
                        });
                    },function(err){
                        if (err) throw err;
                        //后面文件写完,关闭可写流文件,文件已经成生完成
                        // 这里同时有两种方式进行文件合并
                        //第一种,是关闭流,由于利用stream是边读边写的,对内存友好
                        writeStream.end();

                        // 第二种,利用Buffer的concat函数进行buffer拼接,这种方式可能会造成内存溢出,故舍弃
                        //  let re=Buffer.concat(bf)
                        //  fs.writeFileSync(pathname,re)

                       
                        //返回给客服端,上传成功
                        let data=JSON.stringify({'code':0,"data": {
                            "source_store_id": uid,
                            "file_name": name
                        }});
                        res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 
                        res.end(data);//返回数据    
                    });
                },50)
                
            }else{//还没有上传文件,请继续上传
                // bf.fill(req.body['data'])
                let bf=Buffer.from(req.body['data'])
                fs.writeFileSync(url,bf)


                let data=JSON.stringify({'code':1,'msg':'继续上传'});
                res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 
                res.end(data);//返回数据    
            }
         }catch(err){
             console.log(err)
         }
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值