首页 > 其他分享 >大文件上传如何做断点续传?

大文件上传如何做断点续传?

时间:2023-09-20 11:37:08浏览次数:35  
标签:断点续传 const 文件 file var hash 上传

是什么
不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂

文件上传简单,文件变大就复杂

上传大文件时,以下几个变量会影响我们的用户体验

服务器处理数据的能力
请求超时
网络波动
上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等

为了解决上述问题,我们需要对大文件上传单独处理

这里涉及到分片上传及断点续传两个概念

#分片上传
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传

如下图

 

上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

大致流程如下:

将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
初始化一个分片上传任务,返回本次分片上传唯一标识;
按照一定的策略(串行或并行)发送各个分片数据块;
发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件
#断点续传
断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

服务器端返回,告知从哪开始
浏览器端自行处理
上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可

如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

#实现思路
整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕

 

下面的内容都是伪代码

读取文件内容:

handleFileChange(e){
const [file] = e.target.files
if(!file) return
this.file = file
}

核心代码

async uploadFile(){

if(!this.file){
return
}
// todos: 判断文件类别

// 1. 文件拆片
const chunks = this.createFileChunk(this.file)
// const hash = await this.calculateHashWorker()
// const hash1 = await this.calculateHashIdle()
// console.log('文件hash',hash)
// console.log('文件hash1',hash1)

// 2. 计算文件的hash值。这hash将用来唯一标识这个文件
const hash = await this.calculateHashSample()
this.hash = hash

// 3. 尝试上传
// 3.1 文件是否上传过? 如果上传过,就秒传成功
// 3.2 如果没有,是否有存在的切片?
const {data:{uploaded, uploadedList}} = await this.$http.post('/checkfile',{
hash:this.hash,
ext:this.file.name.split('.').pop()
})
if(uploaded){
// 秒传
return this.$message.success('秒传成功')
}
// 4. 上传
await this.uploadChunks(uploadedList, chunks)

},

createFileChunk

// 创建分片
createFileChunk(file,size=CHUNK_SIZE){
const chunks = []
let cur = 0
while(cur<this.file.size){
chunks.push({index:cur, file:this.file.slice(cur,cur+size)})
cur+=size
}
return chunks
},

uploadChunks

async uploadChunks(uploadedList=[], chunks){

this.chunks = chunks.map((chunk,index)=>{
// 切片的名字 hash+index
const name = hash +'-'+ index
return {
hash,
name,
index,
chunk:chunk.file,
// 设置进度条,已经上传的,设为100
progress:uploadedList.indexOf(name)>-1 ?100:0
}
})

const requests = this.chunks
.filter(chunk=>uploadedList.indexOf(chunk.name)==-1)
.map((chunk,index)=>{
// 转成promise
const form = new FormData()
form.append('chunk',chunk.chunk)
form.append('hash',chunk.hash)
form.append('name',chunk.name)
// form.append('index',chunk.index)
return {form, index:chunk.index,error:0}
})
.map(({form,index})=> this.$http.post('/uploadfile',form,{
onUploadProgress:progress=>{
// 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
this.chunks[index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
}
}))
// @todo 并发量控制
// 尝试申请tcp链接过多,也会造成卡顿
// 异步的并阿叔控制,
// await Promise.all(requests)

// await this.sendRequest(requests)
await Promise.all(requests)
await this.mergeRequest()

}

合并文件

async mergeRequest(){
const ret = await this.$http.post('/mergefile',{
ext:this.file.name.split('.').pop(),
size:CHUNK_SIZE,
hash:this.hash
})
},

可以使用md5实现文件的唯一性

import sparkMD5 from 'spark-md5'

// 计算当前文件的hash
async calculateHashSample(file){

// 布隆过滤器 判断一个数据存在与否
// 1个G的文件,抽样后5M以内
// hash一样,文件不一定一样
// hash不一样,文件一定不一样
return new Promise(resolve=>{
const spark = new sparkMD5.ArrayBuffer()
const reader = new FileReader()

const size = file.size
const offset = 2*1024*1024
// 第一个2M,最后一个区块数据全要
let chunks = [file.slice(0,offset)]

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
}
// 中间的,取前中后各2各字节
reader.readAsArrayBuffer(new Blob(chunks))
reader.onload = e=>{
spark.append(e.target.result)
this.hashProgress = 100
resolve(spark.end())
}
})
},

然后开始对文件进行分割

var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
//每10M切割一段,这里只做一个切割演示,实际切割需要循环切割,
var slice = e.target.result.slice(0, 10*1024*1024);
});

h5上传一个(一片)

const formdata = new FormData();
formdata.append('0', slice);
//这里是有一个坑的,部分设备无法获取文件名称,和文件类型,这个在最后给出解决方案
formdata.append('filename', file.filename);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
//xhr.responseText
});
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);

function updateProgress(event) {
if (event.lengthComputable) {
//进度条
}
}

这里给出常见的图片和视频的文件类型判断

function checkFileType(type, file, back) {
/**
* type png jpg mp4 ...
* file input.change=> this.files[0]
* back callback(boolean)
*/
var args = arguments;
if (args.length != 3) {
back(0);
}
var type = args[0]; // type = '(png|jpg)' , 'png'
var file = args[1];
var back = typeof args[2] == 'function' ? args[2] : function() {};
if (file.type == '') {
// 如果系统无法获取文件类型,则读取二进制流,对二进制进行解析文件类型
var imgType = [
'ff d8 ff', //jpg
'89 50 4e', //png

'0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
'0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4

'0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 0 66 74 79 70 6D 70 34 32', //m4v

'0 0 0 14 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 6D 6F 6F 76', //mov

'4F 67 67 53 0 02', //ogg
'1A 45 DF A3', //ogg

'52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)
];
var typeName = [
'jpg',
'png',
'mp4',
'mp4',
'mp4',
'mp4',
'mp4',
'm4v',
'm4v',
'mov',
'mov',
'mov',
'ogg',
'ogg',
'avi',
];
var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
var slice = e.target.result.slice(0, sliceSize);
reader = null;
if (slice && slice.byteLength == sliceSize) {
var view = new Uint8Array(slice);
var arr = [];
view.forEach(function(v) {
arr.push(v.toString(16));
});
view = null;
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
arr = arr.map(function(v) {
if (i > 3 && i < 8) {
return 'x';
}
return v;
});
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
back(false);
}

}
} else {
back(false);
}

});
} else {
var type = file.name.match(/\.(\w+)$/)[1];
back(type);
}
}

调用方法如下

checkFileType('(mov|mp4|avi)',file,function(fileType){
// fileType = mp4,
// 如果file的类型不在枚举之列,则返回false
});

上面上传文件的一步,可以改成:

formdata.append('filename', md5code+'.'+fileType);

有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断

后端主要做的内容为:根据前端传给后台的md5值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传

如果想要暂停切片的上传,可以使用XMLHttpRequest的 abort方法

#使用场景
大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度
网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见
#小结
当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如

切片上传失败怎么办
上传过程中刷新页面怎么办
如何进行并行上传
切片什么时候按数量切,什么时候按大小切
如何结合 Web Worker 处理大文件上传
如何实现秒传

参考文章:http://blog.ncmem.com/wordpress/2023/09/20/%e5%a4%a7%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e5%a6%82%e4%bd%95%e5%81%9a%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%ef%bc%9f/

欢迎入群一起讨论

 

 

标签:断点续传,const,文件,file,var,hash,上传
From: https://www.cnblogs.com/songsu/p/17716885.html

相关文章

  • odoo中在一个模块下修改另一个模块中的视图文件(新增一个字段)
     下面的代码,可以在一个模块中,修改另一个视图的内容,这里是新增一个字段,即:下图中的字段:my_field 固定部分:- <record id="view_order_form_inherit_my_module" model="ir.ui.view">:这是定义一个新的记录的开始标签。model="ir.ui.view"表示这个记录的模型是ir.ui.view,这是O......
  • Jmeter5.4参数化上传文件提示系统找不到指定的文件的解决方法
    问题:java.io.FileNotFoundException:D:\A_JFKJ\A_a项目资料\1_20230906国家教育考试指挥系统V2.10\测试数据\报名_编排_考场对应\${username}-报名.xlsx(系统找不到指定的文件。) 解决方法:在文件路径${}参数化内容前方加反斜杠“\”即可解决  ......
  • thinkphp上传文件失败的一次记录
    问题出现有用户反映上传图片报网络错误,问题转到开发之后怀疑可能是用户上传的图片比较大导致的错误,后面测试发现在上传8m左右大小的图片的时候就会报错,但是报的错误不是代码里面图片大小验证规则的错误,而是异常捕获的错误,让我很纳闷后来决定先让前端在页面提......
  • C++文件的读写
    文件读写函数库对于文件对象的操作,主要使用库:#include<fstream>类可以定义三种类对象:ifstream定义的对象只能读文件ofstream定义的对象只能写文件iofstream定义对象既能读文件,也能写文件类定义的对象中open()方法的第二个参数文件模式(filemode)有多种属性:in:......
  • 26_linux 文件编程
    linux文件编程#include<stdio.h>#include<string.h>#include<fcntl.h>intmain(intargc,charconst*argv[]){intfd,len;char*buf="HelloWorld\n",Out[32];fd=open("a.txt",O_CREAT|O_TRUNC|O_RDWR,0600......
  • pycharm 无法加载文件activate.ps1的原因分析及解决方法
    这篇文章主要介绍了pycharm报错提示:无法加载文件\venv\Scripts\activate.ps1,因为在此系统上禁止运行脚本,解决方法终端输入get-executionpolicy,回车返回Restricted即可,需要的朋友可以参考下 pycharm报错提示:无法加载文件\venv\Scripts\activate.ps1,因为在此系统上禁止运行脚本......
  • Http Fetch+StreamSaver.js在内存有限的设备下载大文件
    目前前端没有很好的api支持流式的文件的分片下载。如果直接把整个文件保存到Blob对象中再保存,有可能出现很多不可以预期的问题,可能会因为达到浏览器的Blob对象上限而下载失败。也有机会因为客户端内存太低而导致OOM。那如果我们有额外的文件服务器的话,可以选择把文件先导出到文件......
  • web前端Tips:断点续传如何实现?
    在Web前端中实现断点续传功能的一种常见方式是使用HTTPRange请求和文件分片上传。以下是一个简单的断点续传实现的步骤:前端将要上传的文件分成多个固定大小的片段(chunk),例如每个片段的大小为1MB。当用户选择上传文件时,前端发送一个初始请求到服务器,询问服务器当前已上传的文件......
  • delphi 操作INI文件
    转载自: delphi读写INI文件_delphi写数据到ini_苏生米沿的博客-CSDN博客Delphi提供了读写INI文件的方法,Delphi操作INI文件最为简洁,这是因为Delphi提供了一个TInifile类,使我们可以非常灵活的处理INI文件。  一、INI文件的结构:;注释[小节名]关键字=值...INI文件允许有多个小节,......
  • macOS 运行xxx.command文件提示”无法执行,因为您没有正确的访问权限“解决方法
    使用苹果mac电脑运行.command文件时,是否遇到弹出”无法执行,因为您没有正确的访问权限“的窗口?遇到这种问题怎么解决呢?这里小编为大家带来了详细的解决方法,一起来看看吧!解决方法:方法一:打开终端工具,输入以下命令:sudosh注意后面有空格然后再把.command文件直接拖入终端按回车键即可......