Node.js实现断点续传


Posted in Javascript onJune 23, 2021
目录
  • 方案分析
    • 切片
    • 断点续传
  • 具体解决流程
  • 逻辑分析
    • 前端
    • 服务端
  • 小结

 

方案分析

 

切片

  • 就是对上传视频进行切分,具体操作为:
  • File.slice(start,end):返回新的blob对象
    • 拷贝blob的起始字节
    • 拷贝blob的结束字节

 

断点续传

  • 每次切片上传之前,请求服务器接口,读取相同文件的已上传切片数
  • 上传的是新文件,服务端则返回0,否则返回已上传切片数

 

具体解决流程

该demo提供关键点思路及方法,其他功能如:文件限制,lastModifiedDate校验文件重复性,缓存文件定期清除等功能扩展都可以在此代码基础上添加。

html

<input class="video" type="file" />
<button type="submit" onclick="handleVideo(event, '.video', 'video')">
    提交
</button>

script

let count = 0; // 记录需要上传的文件下标
const handleVideo = async (event, name, url) => {
// 阻止浏览器默认表单事件
event.preventDefault();
let currentSize = document.querySelector("h2");
let files = document.querySelector(name).files;
// 默认切片数量
const sectionLength = 100;
// 首先请求接口,获取服务器是否存在此文件
// count为0则是第一次上传,count不为0则服务器存在此文件,返回已上传的切片数
count = await handleCancel(files[0]);

// 申明存放切片的数组对象
let fileCurrent = [];
// 循环file文件对象
for (const file of [...files]) {
  // 得出每个切片的大小
  let itemSize = Math.ceil(file.size / sectionLength);
  // 循环文件size,文件blob存入数组
  let current = 0;
  for (current; current < file.size; current += itemSize) {
    fileCurrent.push({ file: file.slice(current, current + itemSize) });
  }
  // axios模拟手动取消请求
  const CancelToken = axios.CancelToken;
  const source = CancelToken.source();
  // 当断点续传时,处理切片数量,已上传切片则不需要再次请求上传
  fileCurrent =
    count === 0 ? fileCurrent : fileCurrent.slice(count, sectionLength);
  // 循环切片请求接口
  for (const [index, item] of fileCurrent.entries()) {
    // 模拟请求暂停 || 网络断开
    if (index > 90) {
      source.cancel("取消请求");
    }
    // 存入文件相关信息
    // file为切片blob对象
    // filename为文件名
    // index为当前切片数
    // total为总切片数
    let formData = new FormData();
    formData.append("file", item.file);
    formData.append("filename", file.name);
    formData.append("total", sectionLength);
    formData.append("index", index + count + 1);

    await axios({
      url: `http://localhost:8080/${url}`,
      method: "POST",
      data: formData,
      cancelToken: source.token,
    })
      .then((response) => {
        // 返回数据显示进度
        currentSize.innerHTML = `进度${response.data.size}%`;
      })
      .catch((err) => {
        console.log(err);
      });
  }
}
};

// 请求接口,查询上传文件是否存在
// count为0表示不存在,count不为0则已上传对应切片数
const handleCancel = (file) => {
return axios({
  method: "post",
  url: "http://localhost:8080/getSize",
  headers: { "Content-Type": "application/json; charset = utf-8" },
  data: {
    fileName: file.name,
  },
})
  .then((res) => {
    return res.data.count;
  })
  .catch((err) => {
    console.log(err);
  });
};

node服务端

// 使用express构建服务器api
const express = require("express");
// 引入上传文件逻辑代码
const upload = require("./upload_file");
// 处理所有响应,设置跨域
app.all("*", (req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("Access-Control-Allow-Headers", "Content-Type, X-Requested-With ");
  res.header("X-Powered-By", " 3.2.1");
  res.header("Content-Type", "application/json;charset=utf-8");
  next();
});
const app = express();

app.use(bodyParser.json({ type: "application/*+json" }));
// 视频上传(查询当前切片数)
app.post("/getSize", upload.getSize);
// 视频上传接口
app.post("/video", upload.video);

// 开启本地端口侦听
app.listen(8080);

upload_file

// 文件上传模块
const formidable = require("formidable");
// 文件系统模块
const fs = require("fs");
// 系统路径模块
const path = require("path");

// 操作写入文件流
const handleStream = (item, writeStream) => {
  // 读取对应目录文件buffer
  const readFile = fs.readFileSync(item);
  // 将读取的buffer || chunk写入到stream中
  writeStream.write(readFile);
  // 写入完后,清除暂存的切片文件
  fs.unlink(item, () => {});
};

// 视频上传(切片)
module.exports.video = (req, res) => {
  // 创建解析对象
  const form = new formidable.IncomingForm();
  // 设置视频文件上传路径
  let dirPath = path.join(__dirname, "video");
  form.uploadDir = dirPath;
  // 是否保留上传文件名后缀
  form.keepExtensions = true;
  // err 错误对象 如果解析失败包含错误信息
  // fields 包含除了二进制以外的formData的key-value对象
  // file 对象类型 上传文件的信息
  form.parse(req, async (err, fields, file) => {
    // 获取上传文件blob对象
    let files = file.file;
    // 获取当前切片index
    let index = fields.index;
    // 获取总切片数
    let total = fields.total;
    // 获取文件名
    let filename = fields.filename;
    // 重写上传文件名,设置暂存目录
    let url =
      dirPath +
      "/" +
      filename.split(".")[0] +
      `_${index}.` +
      filename.split(".")[1];
    try {
      // 同步修改上传文件名
      fs.renameSync(files.path, url);
      console.log(url);
      // 异步处理
      setTimeout(() => {
        // 判断是否是最后一个切片上传完成,拼接写入全部视频
        if (index === total) {
          // 同步创建新目录,用以存放完整视频
          let newDir = __dirname + `/uploadFiles/${Date.now()}`;
          // 创建目录
          fs.mkdirSync(newDir);
          // 创建可写流,用以写入文件
          let writeStream = fs.createWriteStream(newDir + `/${filename}`);
          let fsList = [];
          // 取出所有切片文件,放入数组
          for (let i = 0; i < total; i++) {
            const fsUrl =
              dirPath +
              "/" +
              filename.split(".")[0] +
              `_${i + 1}.` +
              filename.split(".")[1];
            fsList.push(fsUrl);
          }
          // 循环切片文件数组,进行stream流的写入
          for (let item of fsList) {
            handleStream(item, writeStream);
          }
          // 全部写入,关闭stream写入流
          writeStream.end();
        }
      }, 100);
    } catch (e) {
      console.log(e);
    }
    res.send({
      code: 0,
      msg: "上传成功",
      size: index,
    });
  });
};

// 获取文件切片数
module.exports.getSize = (req, res) => {
  let count = 0;
  req.setEncoding("utf8");
  req.on("data", function (data) {
    let name = JSON.parse(data);
    let dirPath = path.join(__dirname, "video");
    // 计算已上传的切片文件个数
    let files = fs.readdirSync(dirPath);
    files.forEach((item, index) => {
      let url =
        name.fileName.split(".")[0] +
        `_${index + 1}.` +
        name.fileName.split(".")[1];
      if (files.includes(url)) {
        ++count;
      }
    });
    res.send({
      code: 0,
      msg: "请继续上传",
      count,
    });
  });
};

 

逻辑分析

 

前端

  • 首先请求上传查询文件是否第一次上传,或已存在对应的切片
    • 文件第一次上传,则切片从0开始
    • 文件已存在对应的切片,则从切片数开始请求上传
  • 循环切片数组,对每块切片文件进行上传
    • 其中使用了模拟手动暂停请求,当切片数大于90取消请求

 

服务端

  • 接收查询文件filename,查找临时存储的文件地址,判断是否存在对应上传文件
    • 从未上传过此文件,则返回0,切片数从0开始
    • 已上传过文件,则返回对应切片数
  • 接收上传文件切片,文件存入临时存储目录
    • 通过count和total判断切片是否上传完毕
    • 上传完毕,创建文件保存目录,并创建可写流,进行写入操作
    • 提取对应临时文件放入数组,循环文件目录数组,依次读取并写入文件buffer
    • 写入完毕,关闭可写流。

 

小结

以上代码涉及到具体的业务流程会有所更改或偏差,这只是其中一种具体实现的方式。
希望这篇文章能对大家有所帮助,如果有写的不对的地方也希望指点一二。

以上代码地址:github.com/Surprise-li…

以上就是Node.js实现断点续传的详细内容,更多关于Node.js 断点续传的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
js实现字符串转日期格式的方法
May 20 Javascript
JS实现关闭当前页而不弹出提示框的方法
Jun 22 Javascript
PHP+jquery+ajax实现分页
Dec 09 Javascript
jquery.masonry瀑布流效果
May 25 jQuery
利用jquery去掉时光轴头尾部线条的方法实例
Jun 16 jQuery
JS 验证密码 不能为空,必须含有数字、字母、特殊字符,长度在8-12位
Jun 21 Javascript
基于canvas粒子系统的构建详解
Aug 31 Javascript
javascript基于牛顿迭代法实现求浮点数的平方根【递归原理】
Sep 28 Javascript
Sublime Text新建.vue模板并高亮(图文教程)
Oct 26 Javascript
简单实现vue中的依赖收集与响应的方法
Feb 18 Javascript
微信小程序8种数据通信的方式小结
Feb 03 Javascript
Javascript中Math.max和Math.max.apply的区别和用法详解
Aug 24 Javascript
JavaScript实现登录窗体
Vue + iView实现Excel上传功能的完整代码
基于JavaScript实现年月日三级联动
Vue vee-validate插件的简单使用
Jun 22 #Vue.js
基于JavaScript实现省市联动效果
JavaScript实现简单计时器
React实现动效弹窗组件
You might like
php去掉文件前几行的方法
2015/07/29 PHP
php通过curl添加cookie伪造登陆抓取数据的方法
2016/04/02 PHP
php fseek函数读取大文件两种方法
2016/10/12 PHP
PHP CURL post数据报错 failed creating formpost data
2016/10/16 PHP
Yii2实现跨mysql数据库关联查询排序功能代码
2017/02/10 PHP
javascript数组组合成字符串的脚本
2021/01/06 Javascript
jQuery 动态酷效果实现总结
2009/12/27 Javascript
javascript动态改变img的src属性图片不显示的解决方法
2010/10/20 Javascript
超级简单的jquery操作表格方法
2014/12/15 Javascript
在浏览器中实现图片粘贴的jQuery插件-- pasteimg使用指南
2014/12/29 Javascript
跟我学习javascript的call(),apply(),bind()与回调
2015/11/16 Javascript
AngularJS ng-style中使用filter
2016/09/21 Javascript
JavaScript仿支付宝6位数字密码输入框
2016/12/29 Javascript
手把手教你把nodejs部署到linux上跑出hello world
2017/06/19 NodeJs
Angular X中使用ngrx的方法详解(附源码)
2017/07/10 Javascript
基于vue的短信验证码倒计时demo
2017/09/13 Javascript
NodeJs搭建本地服务器之使用手机访问的实例讲解
2018/05/12 NodeJs
Vue Promise的axios请求封装详解
2018/08/13 Javascript
基于Vue和Element-Ui搭建项目的方法
2019/09/06 Javascript
[01:58]DOTA2上海特级锦标赛现场采访:RTZ这个ID到底好不好
2016/03/25 DOTA
python 测试实现方法
2008/12/24 Python
Python的缺点和劣势分析
2019/11/19 Python
关于keras.layers.Conv1D的kernel_size参数使用介绍
2020/05/22 Python
Anaconda的安装与虚拟环境建立
2020/11/18 Python
python 使用csv模块读写csv格式文件的示例
2020/12/02 Python
Guess欧洲官网:美国服饰品牌
2019/08/06 全球购物
什么是GWT的Entry Point
2013/08/16 面试题
成品仓管员岗位职责
2013/12/11 职场文书
学生拾金不昧表扬信
2014/01/21 职场文书
2014购房个人委托书范本
2014/10/12 职场文书
接待员岗位职责
2015/02/13 职场文书
工作态度怎么写
2015/06/25 职场文书
2015年幼儿园国庆节活动总结
2015/07/30 职场文书
Nginx中使用Lua脚本与图片的缩略图处理的实现
2022/03/18 Servers
Android自定义ScrollView实现阻尼回弹
2022/04/01 Java/Android
python处理json数据文件
2022/04/11 Python