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日期转换 时间戳转日期格式
Nov 05 Javascript
jquery对象和DOM对象的区别介绍
Aug 09 Javascript
js控制frameSet示例
Sep 10 Javascript
jquery插件开发之实现md5插件
Mar 17 Javascript
javascript实现点击商品列表checkbox实时统计金额的方法
May 15 Javascript
javascript实现输出指定行数正方形图案的方法
Aug 03 Javascript
Bootstrap开发实战之第一次接触Bootstrap
Jun 02 Javascript
JavaScript动态添加css样式和script标签
Jul 19 Javascript
微信小程序 数据交互与渲染实例详解
Jan 21 Javascript
基于Bootstrap分页的实例讲解(必看篇)
Jul 04 Javascript
微信小程序 确认框的实现(附代码)
Jul 23 Javascript
js实现计算器功能
Aug 10 Javascript
JavaScript实现登录窗体
Vue + iView实现Excel上传功能的完整代码
基于JavaScript实现年月日三级联动
Vue vee-validate插件的简单使用
Jun 22 #Vue.js
基于JavaScript实现省市联动效果
JavaScript实现简单计时器
React实现动效弹窗组件
You might like
PHP 高手之路(一)
2006/10/09 PHP
php中使用临时表查询数据的一个例子
2013/02/03 PHP
php计算title标题相似比的方法
2015/07/29 PHP
php注册审核重点解析(数据访问)
2017/05/23 PHP
Laravel关联模型中过滤结果为空的结果集(has和with区别)
2018/10/18 PHP
Javascript基础 函数“重载” 详细介绍
2013/10/25 Javascript
javascript中文本框中输入法切换的问题
2013/12/10 Javascript
jQuery使用andSelf()来包含之前的选择集
2014/05/19 Javascript
JQuery做的一个简单的点灯游戏分享
2014/07/16 Javascript
JavaScript DOM基础
2015/04/13 Javascript
javascript设计模式之对象工厂函数与构造函数详解
2015/07/30 Javascript
轻松实现jquery手风琴效果
2016/01/14 Javascript
jQuery的框架介绍
2016/05/11 Javascript
JS异步加载的三种实现方式
2017/03/16 Javascript
微信小程序实现元素渐入渐出动画效果封装方法
2019/05/18 Javascript
vue router总结 $router和$route及router与 router与route区别
2019/07/05 Javascript
Vue实现多标签选择器
2019/11/28 Javascript
vue+axios 拦截器实现统一token的案例
2020/09/11 Javascript
如何在vue 中引入使用jquery
2020/11/10 jQuery
[02:16]深扒TI7聊天轮盘语音出处2
2017/05/11 DOTA
基于python编写的微博应用
2014/10/17 Python
在Django框架中伪造捕捉到的URLconf值的方法
2015/07/18 Python
Django 限制访问频率的思路详解
2019/12/24 Python
详解django中Template语言
2020/02/22 Python
keras CNN卷积核可视化,热度图教程
2020/06/22 Python
虚拟环境及venv和virtualenv的区别说明
2021/02/05 Python
英国建筑用品在线:Building Supplies Online(BSO)
2018/04/30 全球购物
如何强制垃圾回收
2015/10/06 面试题
如何将一个描述日期或日期/时间的字符串转换为一个Date对象
2015/10/13 面试题
摄影助理岗位职责
2014/02/07 职场文书
硕士研究生求职自荐信范文
2014/03/11 职场文书
心理学专业求职信
2014/06/16 职场文书
计划生育工作总结2015
2015/04/03 职场文书
学雷锋主题班会教案
2015/08/13 职场文书
教师岗位说明书
2015/09/30 职场文书
完美处理python与anaconda环境变量的冲突问题
2021/04/07 Python