大文件分片上传、分片进度以及整体进度、断点续传(一)

大文件分片上传

效果展示
请添加图片描述


前端

思路

前端的思路:将大文件切分成多个小文件,然后并发给后端。

页面构建

先在页面上写几个组件用来获取文件。

<body><input type="file" id="file" /><button id="uploadButton">点击上传</button>
</body>

功能函数:生成切片

切分文件的核心函数是 slice,没错,就是这么的神奇啊

我们把切好的 chunk 放到数组里,等待下一步的包装处理

/*** 默认切片大小 10 MB*/
const SIZE = 10 * 1024 * 1024;/*** 功能:生成切片*/
function handleCreateChunk(file, size = SIZE) {const fileChunkList = [];let cur = 0;while (cur < file.size) {fileChunkList.push({file: file.slice(cur, cur + size),});cur += size;}return fileChunkList;
}

功能函数:请求逻辑

在这里简单封装一下 XMLHttpRequest

/*** 功能:封装请求* @param {*} param0* @returns*/
function request({ url, method = 'post', data, header = {}, requestList }) {return new Promise((resolve, reject) => {let xhr = new XMLHttpRequest();xhr.open(method, url);Object.keys(header).forEach((item) => {xhr.setRequestHeader(item, header[item]);});xhr.onload = function (e) {resolve({data: e.target.response,});};xhr.send(data);});
}

功能函数:上传切片

/*** 功能: 上传切片* 包装好 FormData 之后通过 Promise.all() 并发所有切片*/
async function uploadChunks(hanldleData, fileName) {const requestList = hanldleData.map(({ chunk, hash }) => {const formData = new FormData();formData.append('chunk', chunk);formData.append('hash', hash);formData.append('filename', fileName);return formData;}).map((formData) => {request({// url: 'http://localhost:3001/upload',url: 'upload',data: formData,});});await Promise.all(requestList);
}/*** 功能:触发上传
*/
document.getElementById('uploadButton').onclick = async function () {// 切片const file = document.getElementById('file').files[0];console.log(file);const fileName = file.name;const fileChunkList = handleCreateChunk(file);// 包装const hanldleData = fileChunkList.map(({ file }, index) => {return {chunk: file,hash: `${fileName}_${index}`,};});await uploadChunks(hanldleData, fileName);
};

可以在请求中看到有很多个请求并发的上传

在这里插入图片描述

后端

后端的思路是:

  1. 把 Node 暂存的 chunk 文件转移到我想处理的地方(也可以直接处理,看你的)
  2. 创建写入流,把各个 chunk 合并,前端会给你每个 chunk 的大小,还有 hash 值来定位每个 chunk 的位置

获取 chunk 切片文件

先把上传的接口写好,

const Koa = require('koa');
const Views = require('koa-views');
const Router = require('koa-router');
const Static = require('koa-static');
const { koaBody } = require('koa-body');
const fs = require('fs');
const fse = require('fs-extra');const app = new Koa();
const router = new Router();
app.use(Views(__dirname));
app.use(Static(__dirname));
app.use(koaBody({multipart: true,formidable: {maxFields: 1000 * 1024 * 1024,},})
);router.get('/', async (ctx) => {await ctx.render('index.html');
});/*** 功能:上传接口* - 从 ctx.request.body 中获取 hash 以及 filename* - 从 ctx.request.files 中拿到分片数据* - 然后再把 node 帮我们临时存放的 chunk 文件的 filepath 拿到,之后移动到我们想要存放的路径下* - filepath 和 hash 是一一对应的关系*/
router.post('/upload', async (ctx) => {const { hash, filename } = ctx.request.body;const { filepath } = ctx.request.files?.chunk;const chunkPath = `${__dirname}/chunkPath/${filename}`;if (!fse.existsSync(chunkPath)) {await fse.mkdirs(chunkPath);}await fse.move(filepath, `${chunkPath}/${hash}`);ctx.body = {code: 1,};
});app.use(router.routes());
app.listen(3000, () => {console.log(`server start: http://localhost:3000`);
});

写完这些就可以拿到 chunk
在这里插入图片描述

合并接口

先写一个接口,用来拿到 hash文件名

/*** 功能: merge 接口* - hasMergeChunk 变量是上面用来记录的* - mergePath 定义一下合并后的文件的路径*/
router.post('/merge', async (ctx) => {// console.log(ctx.request.body);const { fileName, size } = ctx.request.body;hasMergeChunk = {};const mergePath = `${__dirname}/merge/${fileName}`;if (!fse.existsSync(`${__dirname}/merge`)) {fse.mkdirSync(`${__dirname}/merge`);}await mergeChunk(mergePath, fileName, size);ctx.body = {data: '成功',};
});

合并分片的功能函数

然后开始合并

/*** 功能:合并 Chunk* - 1. chunkDir: 是 chunks 文件们所在的文件夹的路径* - 2. chunkPaths: 是个 Array,数组中包含所有的 chunk 的 path* - 3. 因为 每个 chunk 的 path 命名是通过 hash 组成的,所以我们先排序一下,* - 算是为 createWriteStream 中的 start 做准备* - 4. 为每个 chunk 的 path 创建写入流,写到 mergePath 这个路径下。因为已经* - 排序了,所以 start 就是每个文件的 index * eachChunkSize* @param {*} mergePath* @param {*} name* @param {*} eachChunkSize*/
async function mergeChunk(mergePath, name, eachChunkSize) {const chunkDir = `${__dirname}/chunkPath/${name}`;const chunkPaths = await fse.readdir(chunkDir);chunkPaths.sort((a, b) => a.split('_')[1] - b.split('_')[1]);await Promise.all(chunkPaths.map((chunk, index) => {const eachChunkPath = `${chunkDir}/${chunk}`;const writeStream = fse.createWriteStream(mergePath, {start: index * eachChunkSize,});return pipeStream(eachChunkPath, writeStream);}));console.log('合并完成');fse.rmdirSync(chunkDir);console.log(`删除 ${chunkDir} 文件夹`);
}

接着就是写入流

/*** 功能:创建 pipe 写文件流* - 1. [首先了解一下什么是输入输出流](https://www.jmjc.tech/less/111)* - 2. hasMergeChunk 变量用于记录一下那些已经合并完成了,也可以写成数组,都行。* - 3. 可以检测输出流的 end 事件,表示我这个 chunk 已经流完了,然后写一下善后逻辑。* @param {*} path* @param {*} writeStream* @returns*/
let hasMergeChunk = {};
function pipeStream(path, writeStream) {return new Promise((resolve) => {const readStream = fse.createReadStream(path); // 输出流readStream.pipe(writeStream); // 输出通过管道流向输入readStream.on('end', () => {hasMergeChunk[path] = 'finish';fse.unlinkSync(path); // 删除此文件resolve();console.log(`合并 No.${path.split('_')[1]}, 已经合并${Object.keys(hasMergeChunk).length}`);});});
}

至此一个基本的逻辑上传就做好了!


Q & A

发送片段之后的合并可能出现错误

这个情况分析了一下是前端的锅啊,前端的 await Promise.all() 并不能保证后端的文件流都写完了。

在这里插入图片描述

完整代码

前端

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="request.js"></script></head><body><input type="file" id="file" /><button id="uploadButton">点击上传</button><button id="mergeButton">点击合并</button></body><script>/*** 默认切片大小 10 MB*/const SIZE = 10 * 1024 * 1024;/*** 功能:生成切片*/function handleCreateChunk(file, size = SIZE) {const fileChunkList = [];let cur = 0;while (cur < file.size) {fileChunkList.push({file: file.slice(cur, cur + size),});cur += size;}return fileChunkList;}/*** 功能: 上传切片* - 注意 map 里别忘了写 return*/async function uploadChunks(hanldleData, fileName) {const requestList = hanldleData.map(({ chunk, hash }) => {const formData = new FormData();formData.append('chunk', chunk);formData.append('hash', hash);formData.append('filename', fileName);return formData;}).map((formData) => {return request({url: 'upload',data: formData,});});await Promise.all(requestList).then((res) => {console.log('所有上传结束', res);});console.log('发送合并请求');await request({url: 'merge',headers: {'content-type': 'application/json',},data: JSON.stringify({size: SIZE,fileName,}),});}document.getElementById('uploadButton').onclick = async function () {// 切片const file = document.getElementById('file').files[0];const fileName = file.name;const fileChunkList = handleCreateChunk(file);// 包装const hanldleData = fileChunkList.map(({ file }, index) => {return {chunk: file,hash: `${fileName}_${index}`,};});await uploadChunks(hanldleData, fileName);};// document.getElementById('mergeButton').onclick = async function () {//   await request({//     url: 'merge',//     headers: {//       'content-type': 'application/json',//     },//     data: JSON.stringify({//       size: SIZE,//       fileName: '116 Mb.mkv',//     }),//   });// };</script>
</html>

后端

const Koa = require('koa');
const Views = require('koa-views');
const Router = require('koa-router');
const Static = require('koa-static');
const { koaBody } = require('koa-body');
const fse = require('fs-extra');const app = new Koa();
const router = new Router();
app.use(Views(__dirname));
app.use(Static(__dirname));
app.use(koaBody({multipart: true,formidable: {maxFields: 1000 * 1024 * 1024,},})
);router.get('/', async (ctx) => {await ctx.render('index.html');
});/*** 功能:上传接口* - 从 ctx.request.body 中获取 hash 以及 filename* - 从 ctx.request.files 中拿到分片数据* - 然后再把 node 帮我们临时存放的 chunk 文件的 filepath 拿到,之后移动到我们想要存放的路径下* - filepath 和 hash 是一一对应的关系*/
router.post('/upload', async (ctx) => {const { hash, filename } = ctx.request.body;const { filepath } = ctx.request.files?.chunk;const chunkPath = `${__dirname}/chunkPath/${filename}`;if (!fse.existsSync(chunkPath)) {await fse.mkdirs(chunkPath);}await fse.move(filepath, `${chunkPath}/${hash}`);ctx.body = {code: 1,};
});/*** 功能:创建 pipe 写文件流* - 1. [首先了解一下什么是输入输出流](https://www.jmjc.tech/less/111)* - 2. hasMergeChunk 变量用于记录一下那些已经合并完成了,也可以写成数组,都行。* - 3. 可以检测输出流的 end 事件,表示我这个 chunk 已经流完了,然后写一下善后逻辑。* @param {*} path* @param {*} writeStream* @returns*/
let hasMergeChunk = {};
function pipeStream(path, writeStream) {return new Promise((resolve) => {const readStream = fse.createReadStream(path); // 输出流readStream.pipe(writeStream); // 输出通过管道流向输入readStream.on('end', () => {hasMergeChunk[path] = 'finish';fse.unlinkSync(path); // 删除此文件resolve();console.log(`合并 No.${path.split('_')[1]}, 已经合并${Object.keys(hasMergeChunk).length}`);});});
}/*** 功能:合并 Chunk* - 1. chunkDir: 是 chunks 文件们所在的文件夹的路径* - 2. chunkPaths: 是个 Array,数组中包含所有的 chunk 的 path* - 3. 因为 每个 chunk 的 path 命名是通过 hash 组成的,所以我们先排序一下,* - 算是为 createWriteStream 中的 start 做准备* - 4. 为每个 chunk 的 path 创建写入流,写到 mergePath 这个路径下。因为已经* - 排序了,所以 start 就是每个文件的 index * eachChunkSize* - 5. 每个写入流都用 Promise 包装了一下,然后用 await Promise.all() 等待处理完* @param {*} mergePath* @param {*} name* @param {*} eachChunkSize*/
async function mergeChunk(mergePath, name, eachChunkSize) {const chunkDir = `${__dirname}/chunkPath/${name}`;const chunkPaths = await fse.readdir(chunkDir);chunkPaths.sort((a, b) => a.split('_')[1] - b.split('_')[1]);await Promise.all(chunkPaths.map((chunk, index) => {const eachChunkPath = `${chunkDir}/${chunk}`;// 创建输入流,并为每个 chunk 定好位置const writeStream = fse.createWriteStream(mergePath, {start: index * eachChunkSize,});return pipeStream(eachChunkPath, writeStream);}));console.log('合并完成');fse.rmdirSync(chunkDir);console.log(`删除 ${chunkDir} 文件夹`);
}/*** 功能: merge 接口* - hasMergeChunk 变量是上面用来记录的* - mergePath 定义一下合并后的文件的路径*/
router.post('/merge', async (ctx) => {// console.log(ctx.request.body);const { fileName, size } = ctx.request.body;hasMergeChunk = {};const mergePath = `${__dirname}/merge/${fileName}`;if (!fse.existsSync(`${__dirname}/merge`)) {fse.mkdirSync(`${__dirname}/merge`);}await mergeChunk(mergePath, fileName, size);ctx.body = {data: '成功',};
});app.use(router.routes());
app.listen(3000, () => {console.log(`server start: http://localhost:3000`);
});

request.js 的封装

/*** 功能:封装请求* @param {*} param0* @returns*/
function request({ url, method = 'post', data, headers = {}, requestList }) {return new Promise((resolve, reject) => {let xhr = new XMLHttpRequest();xhr.open(method, url);Object.keys(headers).forEach((item) => {xhr.setRequestHeader(item, headers[item]);});xhr.onloadend = function (e) {resolve({data: e.target.response,});};xhr.send(data);});
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/192400.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录-刷题第十五天

二叉树层序遍历 题目链接&#xff1a;102. 二叉树的层序遍历 思路&#xff1a;利用队列来存储遍历的节点&#xff0c;同时要定义size来保存当前层的节点个数。 时间复杂度O(n) 层序遍历的一般写法&#xff0c;通过一个 while 循环控制从上向下一层层遍历&#xff0c;for 循…

动态规划学习——回文串

目录 一&#xff0c;回文子串 1.题目 2.题目接口 3&#xff0c;解题代码及其思路 解题代码&#xff1a; 二&#xff0c; 分割回文串II 1&#xff0c;题目 2&#xff0c;题目接口 3&#xff0c;解题思路及其代码 一&#xff0c;回文子串 1.题目 给你一个字符串 s &…

模板初阶(2):函数模板的匹配原则,类模板的实例化

一、函数模板的匹配原则 int Add(const int& x, const int& y) {return x y; }template <class T> T Add(const T& x, const T& y) {return x y; }int main() {int a1 1, a2 2;Add(a1, a2);double d1 1.1, d2 2.2;Add(d1, d2);return 0; }一个非模…

Unity 加载本地图片的方法

Unity加载本地图片有不少方法&#xff0c;一般使用以下这些&#xff1a; 1、使用System.IO下的File.ReadAllBytes方法&#xff1a; //方法一void LoadTextureFromFile1(string filePath){// 创建一个Texture2DTexture2D texture new Texture2D(1, 1);// 加载图片数据byte[] i…

【搭建网站】搭建一个自己的网站

【搭建网站】搭建一个自己的网站 传送门&#xff1a;搭建一个自己的网站&#xff1f;看这个就够了&#xff01; P1&#xff0c;建站准备 P2&#xff0c;创建站点

汇编:call与ret/retf指令

一、call指令 ​​​​​​​ 1.1 依据位移进行转移&#xff1a;call 标号 1.2 实现段间转移&#xff1a;call far ptr 标号 1.3 转移地址在寄存器中&#xff1a;call 16位寄存器 1.4 转移地址在内存中 1.4.1 call word ptr 内存单元地址 1.4.2 call dword ptr 内存单元地址…

E/Acc有效加速主义与EA有效利他主义

隐形人工智能初创公司 Extropic 的创始人、前谷歌工程师 Guillaume Verdon 领导了席卷硅谷的“有效加速主义&#xff08;E/Acc&#xff09;”运动。 他在被人肉以后发布声明&#xff1a; 不幸地通过语音取证和网络侦查&#xff0c;被记者人肉搜索了我的身份和秘密初创公司。当…

NVMe Over Fabrics with iRDMA总结 - 2

5.0 Configure NVMe over Fabrics Target (Storage Server) 配置 NVMe over Fabrics 目标机(存储服务器) 5.1 Install NVMe over Fabrics Tools安装 NVMe over Fabrics 工具 5.1.1 Install nvme-cli安装 nvme-cli Download nvme-cli from: 从以下网址下载 nvme-cli:https:…

基于springboot2 mysql Mybatis拦截器 实现 数据库脱敏

在 Spring Boot 2 中使用 MySQL 和 MyBatis 实现数据库脱敏&#xff0c;你可以考虑借助 MyBatis 的拦截器机制来实现。下面是一个简单的示例&#xff0c;演示如何在查询数据时进行脱敏处理。 假设有一个实体类 User&#xff0c;其中包含需要脱敏的字段&#xff0c;比如 name 和…

ZooKeeper 如何保证数据一致性?

在分布式场景中&#xff0c;ZooKeeper 的应用非常广泛&#xff0c;比如数据发布和订阅、命名服务、配置中心、注册中心、分布式锁等。 ZooKeeper 提供了一个类似于 Linux 文件系统的数据模型&#xff0c;和基于 Watcher 机制的分布式事件通知&#xff0c;这些特性都依赖 ZooKee…

【开源】基于JAVA语言的桃花峪滑雪场租赁系统

项目编号&#xff1a; S 036 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S036&#xff0c;文末获取源码。} 项目编号&#xff1a;S036&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 游客服务2.2 雪场管理 三、数据库设…

Redis数据存储:高效、灵活、实时

目录 引言 1. Redis概述 1.1 什么是Redis&#xff1f; 1.2 Redis的数据结构 1.3 Redis的持久化机制 2. Redis的使用场景 2.1 缓存 2.2 会话存储 2.3 发布/订阅系统 2.4 计数器和排行榜 3. Redis最佳实践 3.1 数据模型设计 3.2 键的命名规范 3.3 事务和原子操作 3…

国产AI边缘计算盒子,双核心A55丨2.5Tops算力

边缘计算盒子 双核心A55丨2.5Tops算力 ● 2.5TopsINT8算力&#xff0c;支持INT8/INT4/FP16多精度混合量化。 ● 4路以上1080p30fps视频编解码&#xff0c;IVE模块独立提供图像基础算子加速。 ● 支持Caffe、ONNX/PyTorch深度学习框架&#xff0c;提供resnet50、yolov5等AI算…

西南科技大学模拟电子技术实验四(集成运算放大器的线性应用)预习报告

一、计算/设计过程 说明:本实验是验证性实验,计算预测验证结果。是设计性实验一定要从系统指标计算出元件参数过程,越详细越好。用公式输入法完成相关公式内容,不得贴手写图片。(注意:从抽象公式直接得出结果,不得分,页数可根据内容调整) 反相比例运算电路(1)实验…

【开源视频联动物联网平台】j2mod 库写一个Modbus RTU 服务器

J2Mod是一个Java编写的Modbus通信库&#xff0c;可以用于实现Modbus RTU服务器。以下是一个简单的示例&#xff0c;演示如何使用J2Mod库创建一个Modbus RTU服务器&#xff1a; 添加J2Mod库依赖项&#xff1a; 首先&#xff0c;确保在项目中包含J2Mod库。你可以将J2Mod库添加到…

C++包装类

C包装类是为了统一解决一些调用问题&#xff0c;在C中&#xff1a;“函数指针回调、仿函数、lambda表达式”都属于可调用对象&#xff0c;他们都可以达到函数函数的行为&#xff0c;达到回调函数和事件处理的功能。 但他们各有优缺点&#xff1a; 函数指针&#xff1a;符合C/…

QT 中 QDateTime::currentDateTime() 输出格式备查

基础 QDateTime::currentDateTime() //当前的日期和时间。 QDateTime::toString() //以特定的格式输出时间&#xff0c;格式 yyyy: 年份&#xff08;4位数&#xff09; MM: 月份&#xff08;两位数&#xff0c;07表示七月&#xff09; dd: 日期&#xff08;两位数&#xff0c…

合成相机模型【图形学】

相机在计算机图形学中有两个方面的考虑&#xff1a;相机的位置和相机的形状。 要了解后者&#xff0c;我们需要了解相机的工作原理。 NSDT工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - R…

如何在Rocky Linux中安装nmon

一、环境基础 [rootlocalhost nmon16d]# cat /etc/redhat-release Rocky Linux release 9.2 (Blue Onyx) [rootlocalhost nmon16d]# uname -r 5.14.0-284.11.1.el9_2.x86_64 [rootlocalhost nmon16d]# 二、安装步骤 在Rocky Linux和AlmaLinux等基于RHEL 的发行版上&#xff…

编程实战:类C语法的编译型脚本解释器(四)

系列入口&#xff1a; 编程实战&#xff1a;类C语法的编译型脚本解释器&#xff08;系列&#xff09;-CSDN博客 本文介绍总入口和使用方法。 一、总入口 class IScript{private:CScript* m_p;string m_msg;public:IScript() :m_p(nullptr) {}string GetMessage()const;//连接bo…