关于Nodejs中的断点续传
最近一个项目要用到大文件的断点续传,多大文件呢,大的包可能会有 20G ,目前的做法是
前端取到文件后,按 2m 一个片段进行分片,然后逐个上传 后端收到完整的文件后放在一个隐藏的目录内,等最后的文件传完之后,逐一合并,并移动到指定的文件夹
实现是实现了,最大的问题不是上传,而是合并以及转移文件相当消耗时间
有没有更高级的做法呢?
关于Nodejs中的断点续传
是需要完全要自己实现么?你们项目有没有用对象存储?对象存储 sdk 一般都支持这个分片上传+合并操作的
https://aws.amazon.com/cn/blogs/compute/uploading-large-objects-to-amazon-s3-using-multipart-upload-and-transfer-acceleration/
移动如果同一硬盘不应该耗时啊
合并的方式不太对,应该直接创建完整大小的空文件,上传接口拿到的缓冲数据直接写到完整文件的指定位置,不需要合并操作
因为是企业内网网络(含广义上的内网)使用,不能使用第三方的云端存储。
开源的对象存储 minio 是否可以考虑使用搭建
为什么不能直接按全文件大小在指定位置建立空文件,然后 offset 写实际数据进去呢?设置个 cron 。超过 3 天没传完自动删除。
分成很多个小文件?说明你实现的断点续传不对,断点续传是一个大文件分成多个请求,每个请求只传自己分好的那一段数据,比如 20 个 G 分成 20 个请求,每个请求传 1G ,第一个请求传从 0 到 1G ,第二个请求从 1G 到 2G ,以依次类推,服务器也是一个文件,根据请求写入文件对应的位置。请求头会携带一些分段的信息,这个有标准的 header ,好像是 range ,可以搜一下
1 建立一个临时文件储存区。
2 上传文件方法分为
( 1 )新建临时文件,body 是文件的第一段,post 成功后将数据保存在临时文件区,文件名可以为一个随机 guid ,返回这个 id 。
( 2 )附加临时文件,body 是文件的第 N 段,将方法 1 返回的 id 作为 query 参数一并提供,post 成功后将新提供的数据附加在这个 id 指定的文件后。
3 在原“使用”文件的位置(例如新建文件的方法),增加临时文件 id 参数。将指定临时文件移动到永久存储区。
客户端流程:
1 打开一个文件。
使用“新建临时文件”方法,传送第一块,得到文件 ID ;使用“附加临时文件”方法,并提交文件 ID ,传送后续的块;使用原业务方法,提交文件 ID ,完成从临时文件到永久存储区的转移和使用。
to:
说的是同一个事,待验证是否可以按区块在预先创建的文件上直接写数据
to:
使用开源对象储存,也是一个不错的方式,之前没想到对象存储也有开源的,像这类东西有点大,没信心是否可以驾驭
#3 可以部署 minio
正解
建议检查代码的实现,理论上不会很耗时
为啥一定要按分片存储呢, 直接将缓冲不断写入单文件不好吗? 还是说用缓存文件替代缓冲,不管是处理缓冲区(Buffer)还是缓存文件,其实都需要每收到一次请求及时做写入目标文件处理啊。
因为要断点续传,直接写入文件,如何告诉客户端从哪里开始上传?后端精确的获得当前文件大小,然后告诉前端从这个位置开始重新上传?
领导允许用开源对象存储的话,可以参考一下 minio 的分片上传
做过类似断点续传,用的 Oss 分片+合并
用 syncthing 去传也行,可以自建
创建大小文件,http range 和你 server 进程 seek 写就行
#15 断点续传 和 断点下载是差不多的。一般的做法是 先建立一个 实际文件大小的 占位文件,还有一个 存储当前进度信息的 文本文件
#3 minio 是可以的,我们使用过,部署在内网的。
4G 左右的文件计算 MD 需要 13+S ,这个耗时前后端都不可避免,需要验证文件的完整性是有必要的。
至于后端小文件合并成大文件,可以使用一个线程合并,也可以使用多个线程分部分合并,然后继续往上合并。
后端也可以使用文件内存映射直接写入文件中对应的 fragment ,不把收到的部分写入小文件。
具体要分析是哪个部分慢,例如是计算 MD5 32 慢,看看是否改成 MD5 16 也能够满足需求。
有轮子就直接用。
minio 是 golang 实现的,很容易部署,我是用的 docker ,直接下载一个单文件应该也可以。
正好前段时间遇到类似的需求,我们采用的是 tus + minio
放硬盘里,人肉过去传输,最快。
请使用支持 S3 的相关存储(公有云对象存储或者私有云 minio),建议 op 好好重读下 http 的 RFC ,https://datatracker.ietf.org/doc/html/rfc7233#section-4.2 有标准的协议不用,你去造轮子
你说的是多线程下载,和断点续传有什么关系?
这是下载, 不是上传.
minio
去看看 BT 软件是如何实现就完事了,只是把一对多改成一对一。
原理一样的
我的做法是:
1.前端做好分片,为每一个分片生成序号,统计分片的个数。将这些数据传给后端,后端把这些数据记录下来,为这个文件生成一个全局 id ,返回给前端。
2.前端每次上传分片会连带文件 id+分片 id 一起传过来。分片文件会被上传到一个临时目录。每上传完成一个分片,后端会记录下来分片 id (分片序号)。如果是并发上传的话,要注意已上传分片 id 集合会有线程安全问题,不然会出现某个分片已上传但没记录的问题。
3.后端返回已上传分片集合。同时会有一个异步线程判断该文件 id 下的分片是否全都上传完毕。如果全都上传完成,调用文件系统 SDK 的合并文件方法(我用的是 minio ),合并完成之后,删除临时文件目录的分片。
4.与此同时,前端全部分片上传完成之后,循环调用获取文件合并状态接口。
完成~
#15
1. 前端生成一个文件 id ,连带文件 size 、offset 和二进制数据传给后端。
2. 后端生成一个 size 大小的空文件,根据 id 找到文件,再根据 offset 去寻找位置写入二进制数据。
3. 后端写完数据返回成功,前端 offset 更新继续传下一个二进制数据给后端,以此循环。
4. 如果断了,下次打开页面时,从后端获取到未完成的文件 id ,再执行 1-3 过程。
其实分片这东西完全是浏览器限制,不用 js,用个流式写入大文件直接边读边写
好家伙,你们但凡看过断点续传的原理,就说不出什么转移合并的话
透传 minio 的 uploadId 给前端,前端每次上传分片就带上 uploadId 和 partNumber 。服务器收到一片也不用写本地,直接透传到 minio ,我习惯这样。
但这样在弱网环境下会有问题,简单说个 流的 retry 需要自己写,这个是最麻烦的。如果是弱网还是建议先写 tmp 再异步上传
弱网环境或并发大场景下,流 reset 不好搞(最终还是需要缓存上传的数据)
复议 这才是断点续传的最初或者真实的做法。
OP 可以用这种方式避免重新组合文件。java 这边对应的 radomaccessfile…
另外为啥要组合?如果服务器不需要读取处理啥的,只是提供存储,直接存分片,下载的时候由前端拼也可以。
我不懂前端,你这 20G 的文件 前端下载时是否合并的了得打个问号
用 tus 协议
https://tus.io/
https://uppy.io/
这些不都是基操吗,检查下自己代码,怕不是有 bug
rsync 不行么?
内网搭建一台开源的对象存储
为啥要等都上传完了再合并,可以上传一点合并一点呀,顺序不对的就等着呗,合并完再移?不是直接在目标文件夹下直接合并吗?
这个倒是新东西,俺研究下看看,thx
— 因为是企业内网网络(含广义上的内网)使用,不能使用第三方的云端存储。
第三方和内不内网有什么关系吗,贵司平时不是用支持私有部署的服务吗
hi ,我还是没 get 到弱网环境下的问题。实际在上传时后端会维护一个 uploadId 下的已上传分片数组,客户端(我这边是 windows 客户端)会根据这个数组尝试重试机制。所以无论网络环境如何、是否是并发上传,只要客户端能正确处理这个数组就可以。
这个是发生在 server 端,server 端透传走客户端传的流(弱网是在 server 端 到 其它地方,比如对象存储)
thanks~
分块上传,并且要求按顺序上传,服务器顺序写入追加就行了
客户端按固定大小分块上传,直至文件结束
在Node.js中实现断点续传(Resumable Uploads)通常涉及处理大文件上传时的中断和恢复功能。这可以通过分片上传和记录已上传的分片信息来实现。以下是一个简化的示例,展示了如何在Node.js中使用Express和Multer库实现断点续传。
服务器端代码
首先,安装所需的包:
npm install express multer
然后,创建一个简单的Express服务器:
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
const { file, body } = req;
const { chunkIndex, totalChunks } = body;
const filePath = path.join(__dirname, file.path);
// 假设我们使用一个简单文件来记录上传状态
const stateFile = path.join(__dirname, 'upload_state.json');
// 更新上传状态(此处为简化处理,实际中需要更健壮的状态管理)
let state = JSON.parse(fs.readFileSync(stateFile, 'utf8')) || {};
state[chunkIndex] = true;
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
// 检查是否所有分片都已上传
if (Object.keys(state).length === totalChunks) {
// 合并分片(此处省略具体实现)
res.send('Upload complete');
} else {
res.send('Chunk received');
}
});
app.listen(3000, () => console.log('Server started on port 3000'));
此示例展示了如何接收分片并更新上传状态。实际应用中,你需要实现分片合并逻辑,并考虑并发上传、错误处理等复杂情况。