大文件处理(上传,下载)思考

article/2025/8/18 20:30:01

文件处理一直都是前端人的心头病,如何控制好文件大小,文件太大上传不了,文件下载时间太长,tcp直接给断开了😱😱😱等

效果

为了方便大家有意义的学习,这里就先放效果图,如果不满足直接返回就行,不浪费大家的时间。

文件上传

big-file-upload.gif

文件上传实现,分片上传,暂停上传,恢复上传,文件合并等

文件下载

big-file-download.gif

为了方便测试,我上传了1个1g的大文件拿来下载,前端用的是流的方式来保存文件的,具体的可以看这个api TransformStream

正文

本项目的地址是: https://github.com/cll123456/deal-big-file 需要的自提

上传

请带着以下问题来阅读下面的文章

  1. 如何计算文件的hash,怎么做计算hash是最快的
  2. 文件分片的方式有哪些
  3. 如何控制分片上传的http请求(控制并发),大文件的碎片太多,直接把网络打垮
  4. 如何暂停上传
  5. 如何恢复上传等

计算文件hash

在计算文件hash的方式,主要有以下几种: 分片全量计算hash抽样计算hash
在这两种方式上,分别又可以使用web-work和浏览器空闲(requestIdleCallback)来实现.

  • web-work有不明白的可以看这里: https://juejin.cn/post/7091068088975622175
  • requestIdleCallback 有不明白的可以看这里: https://juejin.cn/post/7069597252473815053

接下来咋们来计算文件的hash,计算文件的hash需要使用 spark-md5这个库,

全量计算文件hash

export async function calcHashSync(file: File) {// 对文件进行分片,每一块文件都是分为2MB,这里可以自己来控制const size = 2 * 1024 * 1024;let chunks: any[] = [];let cur = 0;while (cur < file.size) {chunks.push({ file: file.slice(cur, cur + size) });cur += size;}// 可以拿到当前计算到第几块文件的进度let hashProgress = 0return new Promise(resolve => {const spark = new SparkMD5.ArrayBuffer();let count = 0;const loadNext = (index: number) => {const reader = new FileReader();reader.readAsArrayBuffer(chunks[index].file);reader.onload = e => {// 累加器 不能依赖index,count++;// 增量计算md5spark.append(e.target?.result as ArrayBuffer);if (count === chunks.length) {// 通知主线程,计算结束hashProgress = 100;resolve({ hashValue: spark.end(), progress: hashProgress });} else {// 每个区块计算结束,通知进度即可hashProgress += 100 / chunks.length// 计算下一个loadNext(count);}};};// 启动loadNext(0);});
}

全量计算文件hash,在文件小的时候计算是很快的,但是在文件大的情况下,计算文件的hash就会非常慢,并且影响主进程哦🙄🙄🙄

抽样计算文件hash

抽样就是取文件的一部分来继续,原理如下:
image.png

/*** 抽样计算hash值 大概是1G文件花费1S的时间* * 采用抽样hash的方式来计算hash* 我们在计算hash的时候,将超大文件以2M进行分割获得到另一个chunks数组,* 第一个元素(chunks[0])和最后一个元素(chunks[-1])我们全要了* 其他的元素(chunks[1,2,3,4....])我们再次进行一个分割,这个时候的分割是一个超小的大小比如2kb,我们取* 每一个元素的头部,尾部,中间的2kb。*  最终将它们组成一个新的文件,我们全量计算这个新的文件的hash值。* @param file {File}* @returns */
export async function calcHashSample(file: File) {return new Promise(resolve => {const spark = new SparkMD5.ArrayBuffer();const reader = new FileReader();// 文件大小const size = file.size;let offset = 2 * 1024 * 1024;let chunks = [file.slice(0, offset)];// 前面2mb的数据let cur = offset;while (cur < size) {// 最后一块全部加进来if (cur + offset >= size) {chunks.push(file.slice(cur, cur + offset));} else {// 中间的 前中后去两个字节const mid = cur + offset / 2;const end = cur + offset;chunks.push(file.slice(cur, cur + 2));chunks.push(file.slice(mid, mid + 2));chunks.push(file.slice(end - 2, end));}// 前取两个字节cur += offset;}// 拼接reader.readAsArrayBuffer(new Blob(chunks));// 最后100Kreader.onload = e => {spark.append(e.target?.result as ArrayBuffer);resolve({ hashValue: spark.end(), progress: 100 });};});
}

这个设计是不是发现挺灵活的,真是个人才哇

在这两个的基础上,咋们还可以分别使用web-worker和requestIdleCallback来实现,源代码在hereヾ(≧▽≦*)o

这里把我电脑配置说一下,公司给我分的电脑配置比较lower, 8g内存的老机器。计算(3.3g文件的)hash的结果如下:

image.png

结果很显然,全量无论怎么弄,都是比抽样的更慢。

文件分片的方式

这里可能大家会说,文件分片方式不就是等分吗,其实还可以根据网速上传的速度来实时调整分片的大小哦!

const handleUpload1 = async (file:File) => {if (!file) return;const fileSize = file.sizelet offset = 2 * 1024 * 1024let cur = 0let count = 0// 每一刻的大小需要保存起来,方便后台合并const chunksSize = [0, 2 * 1024 * 1024]const obj = await calcHashSample(file) as { hashValue: string };fileHash.value = obj.hashValue;//todo 判断文件是否存在存在则不需要上传,也就是秒传while (cur < fileSize) {const chunk = file.slice(cur, cur + offset)cur += offsetconst chunkName = fileHash.value + "-" + count;const form = new FormData();form.append("chunk", chunk);form.append("hash", chunkName);form.append("filename", file.name);form.append("fileHash", fileHash.value);form.append("size", chunk.size.toString());let start = new Date().getTime()// todo 上传单个碎片const now = new Date().getTime()const time = ((now - start) / 1000).toFixed(4)let rate = Number(time) / 10// 速率有最大和最小 可以考虑更平滑的过滤 比如1/tan if (rate < 0.5) rate = 0.5if (rate > 2) rate = 2offset = parseInt((offset / rate).toString())chunksSize.push(offset)count++}//todo 可以发送合并操作了}

image.png

🥉🥉🥉ATTENTION!!! 如果是这样上传的文件碎片,如果中途断开是无法续传的(每一刻的网速都是不一样的),除非每一次上传都把 chunksSize(分片的数组)保存起来哦

控制http请求(控制并发)

控制http的请求咋们可以换一种想法,是不是就是控制异步任务呢?

/*** 异步控制池 - 异步控制器* @param concurrency 最大并发次数* @param iterable  异步控制的函数的参数* @param iteratorFn 异步控制的函数*/
export async function* asyncPool<IN, OUT>(concurrency: number, iterable: ReadonlyArray<IN>, iteratorFn: (item: IN, iterable?: ReadonlyArray<IN>) => Promise<OUT>): AsyncIterableIterator<OUT> {
// 传教set来保存promiseconst executing = new Set<Promise<IN>>();// 消费函数async function consume() {const [promise, value] = await Promise.race(executing) as unknown as [Promise<IN>, OUT];executing.delete(promise);return value;}// 遍历参数变量for (const item of iterable) {const promise = (async () => await iteratorFn(item, iterable))().then(value => [promise, value]) as Promise<IN>;executing.add(promise);// 超出最大限制,需要等待if (executing.size >= concurrency) {yield await consume();}}// 存在的时候继续消费promisewhile (executing.size) {yield await consume();}
}

暂停请求

暂停请求,其实也很简单,在原生的XMLHttpRequest 里面有一个方法是 xhr?.abort(),在发送请求的同时,在发送请求的时候,咋们用一个数组给他装起来,然后就可以自己直接调用abort方法了。

在封装request的时候,咋们要求传入一个requestList就好:

export function request({url,method = "post",data,onProgress = e => e,headers = {},requestList
}: IRequest) {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.upload.onprogress = onProgress// 发送请求xhr.open(method, baseUrl + url);// 放入其他的参数Object.keys(headers).forEach(key =>xhr.setRequestHeader(key, headers[key]));xhr.send(data);xhr.onreadystatechange = e => {// 请求是成功的if (xhr.readyState === 4) {if (xhr.status === 200) {if (requestList) {// 成功后删除列表const i = requestList.findIndex(req => req === xhr)requestList.splice(i, 1)}// 获取服务响应的结构const resp = JSON.parse(xhr.response);// 这个code是后台规定的,200是正确的响应,500是异常if (resp.code === 200) {// 成功操作resolve({data: (e.target as any)?.response});} else {reject('报错了 大哥')}} else if (xhr.status === 500) {reject('报错了 大哥')}}};// 存入请求requestList?.push(xhr)});
}

有了请求数组后,那么咋们想暂时直接遍历请求数组,调用 abort方法

恢复上传

恢复上传是判断有哪些碎片上已经存在的,存在的就不需要上传了,不存在的继续上传。所以咋们要一个接口,verify 传入文件的hash,文件名称,判断文件是否存在或者说是上传了多少。

/*** 验证文件是否存在* @param req * @param res */async handleVerify(req: http.IncomingMessage, res: http.ServerResponse) {// 解析post请求数据const data = await resolvePost(req) as { filename: string, hash: string }const { filename, hash } = data// 获取文件后缀名称const ext = extractExt(filename)const filePath = path.resolve(this.UPLOAD_DIR, `${hash}${ext}`)// 文件是否存在let uploaded = falselet uploadedList: string[] = []if (fse.existsSync(filePath)) {uploaded = true} else {// 文件没有完全上传完毕,但是可能存在部分切片上传完毕了uploadedList = await getUploadedList(path.resolve(this.UPLOAD_DIR, hash))}res.end(JSON.stringify({code: 200,uploaded,uploadedList // 过滤诡异的隐藏文件}))}

注意,这里还需要在每一次验证的时候需要去删除片段的最后几块文件,防止最后几块文件是不完全上传的残杂.

合并文件

合并文件很好理解,就是把所有的碎片进行合并,但是有一个地方需要注意的是,咋们不能把所有的文件都读到内存中进行合并,而是使用流的方式来进行合并,边读边写入文件。写入文件的时候需要保证顺序,不然文件可能就会损坏了。
这一部分代码会比较多,感兴趣的同学可以看源码

文件下载

对于文件下载的话,后端其实很简单,就是返回一个流就行,如下:

/*** 文件下载* @param req  * @param res */async handleDownload(req: http.IncomingMessage, res: http.ServerResponse) {// 解析get请求参数const resp: UrlWithParsedQuery = await resolveGet(req)// 获取文件名称const filePath = path.resolve(this.UPLOAD_DIR, resp.query.filename as string)// 判断文件是否存在if (fse.existsSync(filePath)) {// 创建流来读取文件并下载const stream = fse.createReadStream(filePath)// 写入文件stream.pipe(res)}}

对于前端的话,咋们需要使用一个库,就是 streamsaver,这个库调用了 TransformStream api来实现浏览器中把文件用流的方式保存在本地的。有了这个后,那就非常简单的使用啦😄😄😃

const downloadFile = async () => {// StreamSaver// 下载的路径const url = 'http://localhost:4001/download?filename=b0d9a1481fc2b815eb7dbf78f2146855.zip'// 创建一个文件写入流const fileStream = streamSaver.createWriteStream('b0d9a1481fc2b815eb7dbf78f2146855.zip')// 发送请求下载fetch(url).then(res => {const readableStream = res.body// more optimizedif (window.WritableStream && readableStream?.pipeTo) {return readableStream.pipeTo(fileStream).then(() => console.log('done writing'))}const writer = fileStream.getWriter()const reader = res.body?.getReader()const pump: any = () => reader?.read().then(res => res.done? writer.close(): writer.write(res.value).then(pump))pump()})
}

http://chatgpt.dhexx.cn/article/ADFwe7tA.shtml

相关文章

python拆分大文件(大文件分割)

python拆分大文件 前言实现过程实验结果 前言 在工作中常常会遇见一些大文件&#xff0c;由于内容太多&#xff0c;使用比如记事本、notePad等软件也打不开&#xff0c;无法查看内容&#xff0c;最好是将整个文件进行拆分&#xff0c;分开处理&#xff0c;现在处理的文件原本是…

如何快速地向服务器传大文件,大文件如何快速传输

在这个互联网时代&#xff0c;信息更新速度逐渐加快。用户在进行文件传输时&#xff0c;一定是希望既稳定又快速的&#xff0c;并且还能够保证安全。但是通常来讲&#xff0c;FTP文件传输并不能同时实现这三点的&#xff0c;特别是上传大文件时&#xff0c;FTP上传文件速度明显…

Linux - 怎么实现大文件传输

一 前言 博文《PageCache》中介绍了 PageCache 的优缺点&#xff0c;其实在处理大文件中 PageCache 作用反而没有那么好。所以本文介绍 Linux 是怎么处理大文件的。 二 起因 首先看一下一个 read() 系统调用流程发生了什么&#xff0c;如下图&#xff1a; 当调用 read 方法时…

如何快速传输大文件:4 种大文件传输有效的方法

文件大小正在爆炸式增长&#xff0c;随之而来的挑战是如何仍然以快速、安全的方式发送。从这个意义上说&#xff0c;弄清楚如何快速传输大文件似乎是一项几乎不可能完成的任务。随着工作流程不断适应数字化&#xff0c;这对于自由职业者、业余视频编辑、后期制作公司和广播公司…

win 10计算机查找大文件,教你如何在Win10系统中查找大文件?

Win10系统如何查找大文件&#xff1f;Win10系统内置有搜索功能&#xff0c;可以帮助用户快速找到所需文件&#xff0c;一般我们都是输入名称进行查找文件的。当然也有其他的搜索方式&#xff0c;比如按照文件大小搜索&#xff0c;相信大家比较少见吧。那么在Win10系统中该如何查…

如何进行大文件传输?

本文首发微信公众号&#xff1a;码上观世界 网络文件传输的应用场景很多&#xff0c;如网络聊天的点对点传输、文件同步网盘的上传与下载、文件上传到分布式文件存储器等&#xff0c;其传输速度主要受限于网络带宽、存储器大小、CPU处理速度以及磁盘读写速度&#xff0c;尤其是…

大文件分片上传

前言 前端进行文件大小分割 &#xff0c;按10M分割进行分片上传&#xff0c;使用的项目还是前面文档介绍的poi同一个项目 另一篇poi导出文章,使用的同一个项目 poi的使用和工具类&#xff08;一&#xff09; 开发 1、maven依赖 <!--文件分片上传使用到依赖 start --&g…

HTTP传输大文件

一 概述 早期网络传输的文件非常小&#xff0c;只是一些几K大小的文本和图片&#xff0c;随着网络技术的发展&#xff0c;传输的不仅有几M的图片&#xff0c;还有可以达到几G和几十G的视频。 在这些大文件传输的情况下&#xff0c;100M的光纤或者4G移动网络都会因为网络压力导致…

使用python读取大文件

读取文件时&#xff0c;如果文件过大&#xff0c;则一次读取全部内容到内存&#xff0c;容易造成内存不足&#xff0c;所以要对大文件进行批量的读取内容。 python读取大文件通常两种方法&#xff1a;第一种是利用yield生成器读取&#xff1b;第二种是&#xff1a;利用open()自…

前端必学 - 大文件上传如何实现

前端必学 - 大文件上传如何实现 写在前面问题分析开始操作一、文件如何切片二、得到原文件的hash值三、文件上传四、文件合并 技术点总结【重要】一、上传文件&#xff1f;二、显示进度三、暂停上传四、Hash有优化空间吗&#xff1f;五、限制请求个数六、拥塞控制&#xff0c;动…

Linux如何快速生成大文件

微信搜索&#xff1a;“二十同学” 公众号&#xff0c;欢迎关注一条不一样的成长之路 dd命令 dd if/dev/zero offile bs1M count20000 会生成一个20G的file 文件&#xff0c;文件内容为全0&#xff08;因从/dev/zero中读取&#xff0c;/dev/zero为0源&#xff09;。 此命令可…

java 处理大文件

目的&#xff1a; 前几天在开发过程中遇到一个需求: 读取一个大约5G的csv文件内容&#xff0c;将其转化为对象然后存储到redis中, 想着直接开大内存直接load 进入到内存中就行了&#xff0c;结果可想而知,5G的文件 &#xff0c;Xmx 开到10G都没有解决&#xff0c;直接out of Me…

5、Linux:如何将大文件切割成多份小文件

最近&#xff0c;在做数据文件的导入操作时&#xff0c;发现有些文本文件太大了&#xff0c;需要将这样的大文件切分成多个小文件进行操作。那么&#xff0c;Linux 中如何将大文件切割成许多的小文件呢&#xff1f;在此记录一下。 Linux 提供了 split 命令可以轻松实现大文件的…

大文件传输有哪些方式可用?大文件传输有哪些方式?

大文件传输有哪些方式可用&#xff1f;大文件传输有哪些方式&#xff1f;互联网时代&#xff0c;速度决定效率。在企业生产过程中需要进行信息数据交换、搬运。这时就需要进行大文件传输。方方面面的行业都要涉及到大文件传输。例如影视行业需要每天进行视频素材的传输&#xf…

简道云-第5章-流程

title: 简道云-第5章-流程 date: 2022-06-13 22:21:29 tags: 简道云 categories: 简道云 简道云-第5章-流程 背景介绍 简道云三个基本项目表单、流程以及仪表。关于它们的介绍可以参照官方文档表单 vs 流程表单 vs 仪表盘。 「流程表单」&#xff1a;填报数据&#xff0c;并带…

阿里云【达摩院特别版·趣味视觉AI训练营】笔记2

阿里云【趣味视觉AI训练营】笔记2 一、笔记说明二、正文2.1 人体分割实验2.2 图像人脸融合实验 三、转载说明 一、笔记说明 本博客专栏《阿里云【达摩院特别版趣味视觉AI训练营】》的所有文章均为趣味视觉AI训练营的学习笔记&#xff0c;当前【达摩院特别版趣味视觉AI训练营】…

笔记本简单使用eNSP的云连接外网

文章目录 前言一、连接拓扑图二、配置cloud 三、配置pc测试是否能连接外网 前言 很多时候ping不通的原因不是网卡问题&#xff0c;而是配置没有设置好 一、连接拓扑图 二、配置cloud 绑定信息为UDP然后点击增加 绑定信息 笔记本电脑可以选择WiFi-ip&#xff0c;有本地连接可以…

头歌-信息安全技术-用Python实现自己的区块链、支持以太坊的云笔记服务器端开发、编写并测试用于保存云笔记的智能合约、支持以太坊的云笔记小程序开发基础

头歌-信息安全技术-用Python实现自己的区块链、支持以太坊的云笔记服务器端开发、编写并测试用于保存云笔记的智能合约、支持以太坊的云笔记小程序开发基础 一、用Python实现自己的区块链1、任务描述2、评测步骤(1)打开终端&#xff0c;输入两行代码即可评测通过 二、支持以太坊…

华为云HCS解决方案笔记HUAWEI CLOUD Stack【面试篇】

目录 HCS方案 一、定义 1、特点 2、优点 二、云服务 1、云管理 2、存储服务 3、网络服务 4、计算服务 5、安全服务 6、灾备服务 7、容器服务 三、应用场景 四、HCS功能层 五、OpenStack网络平面规划 六、ManageOne运维面 1、首页 2、集中监控 3、资源拓扑 …

关于玄武集团MOS云平台的使用笔记

对于该平台感兴趣的可以自己下载开发文档看一下&#xff0c;附上地址: https://download.csdn.net/download/qq_39380192/11182359 1、根据开发手册&#xff0c;MOS云平台给用户提供了关于各种通信服务的接口&#xff0c;用户可以通过调用相关的接口来实现一下几点功能&#x…