利用WebUploader+layui+expressjs+mongodb实现秒传和断点续传功能

 xiezixing @ 2019-01-26 12:10:20

最近需要编写云转码的总控系统,在总控系统上面必然是要开放用户上传的功能,所以设想中就必须需要和百度网盘一样的秒传和断点续传功能,而且需要做成API调用,方便以后制作成桌面应用,我还想使用electron制作跨平台的网盘系统,当然这都是后话,我们来看看如何利用百度出品的Webuploader+layui+expressjs+mongodb实现秒传和断点续传功能。

秒传和断点续传功能介绍
在上传视频的时候,实时计算视频文件的MD5码,然后调用api,服务器中判断此MD5是否存在于mongodb中,如果存在则返回true,如果不存在则返回false,前端根据返回的情况来判断是否需要直接跳过此文件的上传,这就是秒传功能的实现。
但是,如果视频非常大,在上传的过程中由于网络原因或者其他原因就非常容易失败,所以在当前上传方案中最佳的方案就是分片上传,在上传之前就将视频分片成几M一个分片上传,再配合后端返回的分片MD5数组,比对数组中是否存在分片的MD5码,如果存在则跳过此分片的上传,如果不存在,则上传分片,所以在上传过程中,如果上传到一半失败了,没关系,重新上传的时候,会进行md5码比对跳过上传过的切片,只上传未上传切片,这就是断点续传功能。

第一步、前端UI设计

我们利用layui的按钮样式和进度条样式建立一个简单的页面。

<div class="layui-container main">
  <div class="layui-row">
    <div class="layui-col-md6 layui-col-md-offset3">
      <h1 class="title">上传电影</h1>
      <div class="layui-btn-group" id="actions"><span id="picker" style="font-size:14px;float:left;">添加视频</span>
        <button class="layui-btn" id="start">开始上传</button>
      </div>
      <div class="table" id="previews"></div>
    </div>
  </div>
</div>

当然还要在页面中加载layui的css和js,还有webuploader的css和js,这里就不赘述了。
非常简单的样式,前端展示如下:
界面UI

第二步、编写webuploader的上传逻辑

var $btn = $('#start')
    var state = 'pending'
    var filemd5
    var chunkmd5
    var md5s = []
    WebUploader.Uploader.register({
      'before-send-file': 'preupload'
    }, {
      preupload: function(file) {
        var me = this,
        owner = this.owner,
        deferred = WebUploader.Deferred();
        $( '#'+file.id ).find('p.state').text('检验md5码中...');
        owner.md5File(file.source)
            .fail(function() {
              deferred.reject();
            })
            .then(function(md5) {
              filemd5 = md5
              $.ajax('/api/checkfilemd5', {
                dataType: 'json',
                method: 'post',
                data: {
                  md5: md5
                },
                success: function(response) {
                  console.log(response)
                  if (response.exist) {
                    owner.skipFile(file);
                    $( '#'+file.id ).find('p.state').text('秒传成功');
                  } else {
                    md5s = response.chunkmd5
                  }
                  deferred.resolve();
                }
              })
            })
        return deferred.promise();
      }
    });
    WebUploader.Uploader.register({
      'before-send': 'prechunkupload'
    }, {
      prechunkupload: function(block) {
        var me = this,
        owner = this.owner,
        deferred = WebUploader.Deferred();
        var blob = block.blob;
        owner.md5File(blob)
            .fail(function() {
              deferred.reject();
            })
            .then(function(md5) {
              chunkmd5 = md5
              if($.inArray(chunkmd5, md5s)>-1) {
                deferred.reject();
              }
              deferred.resolve();
            })
        return deferred.promise();
      }
    });
    var uploader = WebUploader.create({
      swf: '/javascripts/Uploader.swf',
      server: '/upload',
      pick: '#picker',
      chunked: true,
      chunkSize: "4000000",
      threads: 1
    });
    uploader.on( 'fileQueued', function( file ) {
        $("#previews").append( '<div id="' + file.id + '" class="item">' +
            '<h4 class="info">' + file.name + '</h4>' +
            '<p class="state">等待上传...</p>' +
        '</div>' );
    });
    $btn.on( 'click', function() {
        if ( state === 'uploading' ) {
            uploader.stop();
        } else {
            uploader.upload();
        }
    });
    // 文件上传过程中创建进度条实时显示。
    uploader.on( 'uploadProgress', function( file, percentage ) {
        var $li = $( '#'+file.id ),
            $percent = $li.find('.layui-progress .layui-progress-bar');
        if ( !$percent.length ) {
            $percent = $('<div class="layui-progress progress-striped active">' +
              '<div class="layui-progress-bar" role="progressbar" style="width: 0%">' +
              '</div>' +
            '</div>').appendTo( $li ).find('.layui-progress-bar');
        }

        $li.find('p.state').text('上传中');

        $percent.css( 'width', percentage * 100 + '%' );
    });
    uploader.on( 'uploadSuccess', function( file ) {
        $( '#'+file.id ).find('p.state').text('已上传');
    });
    uploader.on('uploadBeforeSend', function(block ,data) {
        var file = block.file;
        data.filemd5 = filemd5
        data.chunkmd5 = chunkmd5
    });
    uploader.on( 'uploadError', function( file ) {
        $( '#'+file.id ).find('p.state').text('上传出错');
    });

    uploader.on( 'uploadComplete', function( file ) {
        $( '#'+file.id ).find('.progress').fadeOut();
    });
    uploader.on( 'all', function( type ) {
        if ( type === 'startUpload' ) {
            state = 'uploading';
        } else if ( type === 'stopUpload' ) {
            state = 'paused';
        } else if ( type === 'uploadFinished' ) {
            state = 'done';
        }

        if ( state === 'uploading' ) {
            $btn.text('停止上传');
        } else {
            $btn.text('开始上传');
        }
    });

等会我们会详细介绍,这里先直接贴上代码。

第三步、expressjs上传逻辑

如上一步代码所示,后台处理逻辑的url为/upload,我们来看看处理逻辑。

  var file = req.file
  var body = req.body
  var des = "./movies/"
  var filename = file.originalname
  var filearr = filename.split(".")
  filearr.pop()
  var path = filearr.join(".")
  var tmppath = des + path
  var exitst = fs.existsSync(tmppath)
  if (!exitst) {
    fs.mkdirSync(tmppath)
  }
  var newfilename = filename + body.chunk
  fs.renameSync(file.path, tmppath + "/" + newfilename)
  Md5.updateOne(
    { md5: body.filemd5 },
    { $push: { chunkmd5: body.chunkmd5 } },
    function(err) {
      if (err) {
        console.log(err)
      }
    }
  )
  if (body.chunk * 1 + 1 == body.chunks * 1) {
    var files = fs.readdirSync(tmppath)
    for (var i = 0; i < files.length; i++) {
      fs.appendFileSync(
        file.path + "",
        fs.readFileSync(tmppath + "/" + filename + i)
      )
      fs.unlinkSync(tmppath + "/" + filename + i)
    }
    fs.rmdirSync(tmppath)
    var movieObj = {
      status: "waiting",
      originalname: file.originalname,
      path: file.path,
      size: body.size,
      md5: body.filemd5
    }
    var movie = new Movie(movieObj)
    movie.save(function(err, movie) {
      if (err) {
        console.log(err)
      }
    })
  }
  return res.json({ success: 1 })

逻辑上因为是分片上传,所以这里就是分片的处理过程,这个逻辑会在接受到分片的文件的时候,将文件重命名加上切片的索引,然后保存在一个临时的文件夹里边,等最后一个切片上传完毕的时候就会进行合并,合并成一个完整的文件。

第四步、mongod的设计

Movies:
status: String,
size: String,
category: String,
originalname: String,
path: String,
md5: String,
createAt: {
    type: Date
  }

Md5:
  md5: String,
  chunkmd5: [String],
  createAt: {
    type: Date
  }

movies表储存源文件名和路径,文件的md5码,而md5表储存源文件的md5码,和对应分片的md5数组。

第五步、检验秒传API逻辑

 var md5 = req.body.md5
  const movie = await Movie.findOne({ md5: md5 })
  const themd5 = await Md5.findOne({ md5: md5 })
  if (movie) {
    return res.json({ exist: true })
  } else {
    if (themd5) {
      return res.json({
        exist: false,
        chunkmd5: themd5.chunkmd5
      })
    } else {
      await Md5.create({ md5: md5, chunkmd5: [] })
      return res.json({
        exist: false,
        chunkmd5: []
      })
    }
  }

在前端上传之前,会将整个文件的MD5码传输到后台进行验证,上面就是验证逻辑,查找Md5表和视频表,如果视频表存在则返回存在,如果不存在的话会判断md5表是否存在,md5表存在则存在切片,md5表不存在则不存在切片并且会创建一个表数据。

第六步、切片上传前检验

    WebUploader.Uploader.register({
      'before-send': 'prechunkupload'
    }, {
      prechunkupload: function(block) {
        var me = this,
        owner = this.owner,
        deferred = WebUploader.Deferred();
        var blob = block.blob;
        owner.md5File(blob)
            .fail(function() {
              deferred.reject();
            })
            .then(function(md5) {
              chunkmd5 = md5
              if($.inArray(chunkmd5, md5s)>-1) {
                deferred.reject();
              }
              deferred.resolve();
            })
        return deferred.promise();
      }
    });

这个前端逻辑,就是在上传切片之前处理的逻辑,根据前面检验源文件整个MD5的api返回的切片md5码数组进行检验,如果数组中存在则跳过,不存在则上传。

大致上就这样,实现上还是比较简单,运行截图:
选择文件
md5码检验中
上传中
已完成

最新回复:

xiezixing @ 5 个月前:

@f2a4lj24:
怎么下载啊??????

下载什么?

f
 f2a4lj24 @ 2019-02-24 10:21:35

怎么下载啊??????

回复
 xiezixing @ 2019-02-24 12:38:23

@f2a4lj24:
怎么下载啊??????

下载什么?

回复