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 相关文章推荐
JavaScript window.setTimeout() 的详细用法
Nov 04 Javascript
jquery attr 设定src中含有&amp;(宏)符号问题的解决方法
Jul 26 Javascript
H5基于iScroll实现下拉刷新和上拉加载更多
Jul 18 Javascript
使用Electron构建React+Webpack桌面应用的方法
Dec 15 Javascript
图文介绍Vue父组件向子组件传值
Feb 17 Javascript
浅析Vue中method与computed的区别
Mar 06 Javascript
Node.js实现注册邮箱激活功能的方法示例
Mar 23 Javascript
Vue引入jquery实现平滑滚动到指定位置
May 09 jQuery
在node中使用jwt签发与验证token的方法
Apr 03 Javascript
新手快速入门JavaScript装饰者模式与AOP
Jun 24 Javascript
vue实现登录拦截
Jun 29 Javascript
js实现简单扫雷
Nov 27 Javascript
JavaScript实现登录窗体
Vue + iView实现Excel上传功能的完整代码
基于JavaScript实现年月日三级联动
Vue vee-validate插件的简单使用
Jun 22 #Vue.js
基于JavaScript实现省市联动效果
JavaScript实现简单计时器
React实现动效弹窗组件
You might like
基于PHP array数组的教程详解
2013/06/05 PHP
windows的文件系统机制引发的PHP路径爆破问题分析
2014/07/28 PHP
php+ajax实现的点击浏览量加1
2015/04/16 PHP
PHP SPL标准库之接口(Interface)详解
2015/05/11 PHP
php数组指针操作详解
2017/02/14 PHP
PHP实现打包zip并下载功能
2018/06/12 PHP
jQuery实现动画效果的简单实例
2014/01/27 Javascript
原生js编写设为首页兼容ie、火狐和谷歌
2014/06/05 Javascript
jQuery选择器全集详解
2014/11/24 Javascript
js的[defer]和[async]属性
2014/11/24 Javascript
jQuery实现防止提交按钮被双击的方法
2015/03/24 Javascript
JavaScript获得当前网页来源页面(即上一页)的方法
2015/04/03 Javascript
九种原生js动画效果
2015/11/11 Javascript
微信小程序 WXML、WXSS 和JS介绍及详解
2016/10/08 Javascript
AngularJS使用ng-Cloak阻止初始化闪烁问题的方法
2016/11/03 Javascript
最通俗易懂的javascript变量提升详解
2017/08/05 Javascript
vue脚手架搭建项目的兼容性配置详解
2018/07/17 Javascript
jQuery实现朋友圈查看图片
2020/09/11 jQuery
Python 第一步 hello world
2009/09/25 Python
Python3使用SMTP发送带附件邮件
2020/06/16 Python
浅析python3中的os.path.dirname(__file__)的使用
2018/08/30 Python
python3转换code128条形码的方法
2019/04/17 Python
Django  ORM 练习题及答案
2019/07/19 Python
Python操作远程服务器 paramiko模块详细介绍
2019/08/07 Python
python3用PyPDF2解析pdf文件,用正则匹配数据方式
2020/05/12 Python
Pycharm 设置默认解释器路径和编码格式的操作
2021/02/05 Python
美国礼品卡商城: Gift Card Mall
2017/08/25 全球购物
英国女士和男士时尚服装网上购物:Top Labels Online
2018/03/25 全球购物
会计毕业生求职简历的自我评价
2013/10/20 职场文书
如何写好升职自荐信
2014/01/06 职场文书
男方父母婚礼答谢词
2014/01/25 职场文书
小学语文教学随笔
2015/08/14 职场文书
PHP中strval()函数实例用法
2021/06/07 PHP
nginx结合openssl实现https的方法
2021/07/25 Servers
分布式Redis Cluster集群搭建与Redis基本用法
2022/02/24 Redis
win10键盘驱动怎么修复?Win10键盘驱动修复小技巧
2022/04/06 数码科技