讲断点续传前,咱们先讲讲大文件上传。大文件上传,可能会出现,上传时间过长,接口限制了文件大小。所以,大文件直接上传,也很不友好,一般采用分片上传的方式去上传。而blob
提供了slice
方法, file
继承了blob
自然也能使用slice
去进行分片处理。
处理流程:
- 前端对大文件进行分片,分片名采用文件
hash后续会讲
加下标
- 为了防止完全占用
tcp
资源,我这里是采用4个4个上传 - 在最后,再发送一个合并请求
- 服务端根据请求,对前面的分片内容进行合并成一个整体文件,然后删除分片
const handleUpload = () => {
chunks = [];
const file: File = files.current[dataIndex];
// 获取对应文件file
let start = 0, end = BIG_FILE_SIZE;
// const BIG_FILE_SIZE = 1024 * 1024 * 2;
while(true) {
const part: Blob = file.slice(start, end);
if (part.size) {
chunks.push(part);
} else {
break;
}
start += BIG_FILE_SIZE;
end += BIG_FILE_SIZE;
};
// worker!.postMessage({ chunkList: chunks });
// 利用webworker获取hash,后面会讲
return;
};
const partUpload = async (start: number) => {
const uploadArr = [];
let restReq = MAX_FILE_TCP;
// MAX_FILE_TCP = 4;同时发起4个连接
let index = start;
while (restReq) {
if (index >= chunkCount) {
// chunkCount是chunk的length,即多少个片段
break;
};
// const blobPart = `${hash}-${index}`;
// if (hashData[hash] && hashData[hash][blobPart])
// {
// index++;
// continue;
// };
// 注释部分是,断点续传部分代码,可跳过
const formData = new FormData();
formData.append('file', chunks[index]);
formData.append('xx', xx);
const uploadFunc = () => {
return new Promise(async (res) => {
const { code } = await uploadPart(formData);
res(code);
});
};
uploadArr.push(uploadFunc);
index++;
restReq--;
};
const result = await Promise.all(uploadArr.map(v => v()));
result.map((v) => {
if (v === 0) {
console.log('上传片段成功');
} else {
throw new Error('上传失败');
}
return null;
});
if (index < chunkCount) {
partUpload(index);
} else {
const params = {
// sth.
};
const {code} = await partMerge(params);
// 发送合并请求
// todo code sth.
}
};
服务端的话,我这边是把文件根据对应的hash
和下标进行命名,即static/hash/hash-i
。利用fs.rename
去修改文件&路径, 通过pipe
合并文件。
router.post('/upload_part', (req, res) => {
try {
const { hash, index } = req.body;
const { file } = req;
const sourcePath = path.join(__dirname, file.path);
const destPath = path.join(__dirname, `xxxx/${hash}/${hash}-${index}`);
fs.renameSync(sourcePath, destPath);
return res.json({code: 0});
} catch(e) {
return res.json({code: 1, msg: e});
}
});
router.post('/merge_part', (req, res) => {
try {
const destPath = 'xxx/yyy/zzz/a.png';
const writePath = fs.createWriteStream(destPath);
// 最终合并结果存储在哪
const fileMerge = (i: number) => {
const blobPath = `xxx/part/${hash}/${hash}-${i}`;
const blobFile = fs.createReadStream(blobPath);
blobFile.on("end", async (err) => {
if (err) {
return res.json({code: 1, msg: err});
};
fs.unlink(blobPath);
// 删除片段
if (i + 1 < chunkCount) {
fileMerge(i + 1);
} else {
fs.rmdirSync(`xxx/part/${hash}`);
// 删除文件夹
// 数据库操作 todo
return res.json({ code: 0 });
}
});
blobFile.pipe(writeStream, { end: false });
};
fileMerge(0);
} catch(e) {
return res.json({code: 1, msg: e});
}
});
断点续传
大文件分片上传,如果客户端发生异常中断了上传过程,那么下次重新上传的时候,比较友好的做法应该是跳过那些已经上传的片段。
那么问题也就是,怎么跳过那些文件?刚才前面的代码,也显示了,其实就是通过${hash}-${i}
设置文件名,然后保存已经上传成功的片段数据,在文件分片上传的时候,跳过已经上传过的片段,就是断点续传辽。
对于这类数据存储,一般都是两个方法:
- 前端存储
- 服务端存储
前端存储的话,一般就是用localStorage
,不太推荐使用。因为用户如果清了缓存,或者换了设备登陆,就无法生效。
服务端的话,就返回对应的已成功上传的片段名。因为对应userId
下的文件hash-i
,是唯一的。node
这里就采用readdirSync/readdir
去读取文件名辽。
前端这里就是前面提到过的部分。
const blobPart = `${hash}-${index}`;
if (hashData[hash] && hashData[hash][blobPart])
{
// hashData是服务端传回来的数据,判断片段是否存在,存在就跳过
// 具体可以看看前面
index++;
continue;
};
标签:yyds,code,const,web,index,断点续传,res,hash,上传
From: https://blog.51cto.com/u_11365839/6215432