校验文件格式
大部分判断文件上传的方法主要是还是第一种,然而,扩展名完全是可以随便修改的。
1.判断文件的后缀名,是否符合。这种校验方法,不准确。
2.不管是图片、视频等文件都是而二进制形式存储的,每类文件的头信息是不同的,所有我们可以通过头信息来判断是哪种类型文件
通过input的事件,传递进来的文件,构造函数是File,File也继承了Blob的属性方法。
比如我们要判断gif格式,只需要判断头6位就行了
- 首先调用file.slice(0,6)。返回指定范围内的数据,是个blob对象
async blobToString(blob) {
return new Promise(resolve => {
const reader = new FileReader()
reader.onload = function() {
// 1.将结果转为unicode编码,2.转成16进制,再转大写
let res = reader.result.split('')
.map(i => i.charCodeAt())
.map(j => j.toString(16).toUpperCase())
.join(' ')
// 返回转换后的结果
resolve(res)
}
// 调用读取文件方法
reader.readAsBinaryString(blob)
})
},
创建一个FileReader实例去读取这个blob对象的内容,此时的内容是 "GIF89a "。
先将字符分割开,charCodeAt:返回字符的Unicode 编码,返回值是 0 - 65535 之间的整数
转成16进制,再转大写,最后以空格转成字符串。最终比较结果是否是 "47 49 46 38 37 61" 或者 res === "47 49 46 38 39 61"
上传文件
普通的上传文件,就是将file添加到formDate中,传给后端就ok了。 但文件要考虑的问题:
- 要上传的文件过大
- 文件上传过程中,网络错误导致上传失败怎么办
- 秒传等问题
文件过大就要用到切片上传,就是将文件分割成许多小块,每个小块单独上传,这样就需要知道上传了哪些切片。
如何知道服务器上有没有上传过该文件? 该文件的切片上传了多少?
这就需要一个唯一标识,就是计算文件的md5值,只要文件内容不变,md5值就不会变
计算文件md5
首先先将文件切片,就是定义一个size,用file.slice(0, size)将文件切成大小相同的切片,最后一个可能不同。
计算md5值,是非常耗时的。
new Worker
创建一个worker实例,注册传递、接收的两个监听函数。
传进去切片数组,worker每计算完一个切片或者计算完所有切片。就发给主线程发信号。这个信号有md5计算的进度,前端可用来做进度条
requestIdleCallback
react16的Filber架构就是用的这个,利用浏览器的空闲时间计算。
就是在window.requestIdleCallback传进去一个执行函数,当空闲时间大于1ms就执行这个计算函数
抽样hash计算
思路就是设定一个size,假如是2M。
开头用一个size的长度去切片file,就是slice(0, size)
往后每一个size的长度切片分别从前中后2个子节。大概就是slice(start, start + 2)、slice(midd, midd + 2)、slice(end - 2, end)
余下的不足一个size的作为一个切片
然后把计算md5值,其实这个计算的是:前后切片、中间的一部门切片的抽样的md5值。
计算结果肯定和以上两种计算的不一样,但是这种方法不管多大的文件,大概都是当做4M左右文件的md5值,适合那种超大的文件
这种计算是损失掉一部分精度,换来的高效。如果文件的其中某几个子节改变了,计算的md5值可能不一样。当然了,这种情况极少,可以忽略了...
切片上传图片
经过以上步骤,切片、文件hash都有了
- 将每个切片的标记一个index、name,index是切片的顺序,name是hash + index。用作唯一标识。
- 创建一个数组,数组每一项是上传切片的请求。也就是说每个成员都是一个Promise对象
- Promise.all(promises),并发执行。这里存在一个问题,就是虽然是并发执行n多个请求,但只是同时建立了n多个Tcp连接而已,并不是同时上传。会造成性能问题,解决反感就是下边的控制并发
- 当切片上传完,再请求后端merge接口,传递文件名、hash值等
async mergeRequest() {
let data = {
ext: this.file.name.split(".").pop(),
size: this.chunkSize,
hash: this.hash
}
let res = await this.$http.post("/mergeUploadedSliceFile", data)
if (res && res.success) {
this.$message({
message: res.message,
type: 'success'
});
}
},
- 后端接口接收到后,大概就是创建一个写的流,根据切片的index讲读的流依次导进去。删掉删掉所有的切片。
async mergeFile(filePath, filehash, size) {
// 切片所在的文件夹
let chunkdDir = path.resolve(this.config.UPLOAD_DIR, filehash)
// 给所有切片排序
try {
let chunks = await fse.readdir(chunkdDir)
chunks.sort((a, b) => a.split('-')[1] - b.split('-')[1])
chunks = chunks.map(cp => path.resolve(chunkdDir, cp))
await this.mergeChunks(chunks, filePath, size)
} catch(err) {
console.log("err", err);
}
}
async mergeChunks(files, dest, size) {
const pipStream = (filePath, writeStream) => new Promise(resolve => {
const readStream = fse.createReadStream(filePath)
readStream.on('end', () => {
fse.unlinkSync(filePath)
resolve()
})
readStream.pipe(writeStream)
})
// 创建一个写的流,依次将读到的流导进去
await Promise.all(
files.map((file, index) => {
// size 必须是整数
let option = {
start: index * size,
end: (index + 1) * size
}
pipStream(file, fse.createWriteStream(dest, option))
})
)
}
断点续传、秒传
发一个checked接口请求,带上文件的md5值,验证文件是否存在。若不存在,也有可能存在切片的文件包
- 如果存在就是直接提示秒传成功。
- 不存在,后端会回传一个切片的数组。 前端根据切片数组,修改各个切片进度条状态,过滤掉已存在的切片。只上传未上传的切片,后边上传、merge的操作和一样
控制并发上传
解决以上的Promise.all一次建立多个tcp连接的问题。比如limit是3,调用三次执行函数。
由于循环的原因,三次执行的开始点就不一样了。每次将弹出一个切片,当前切片上传完以后,再执行下一个切片。永远都是三次并发执行
return new Promise((resolve, reject) => {
let cur = 0
let limit = this.limit
let len = requets.lengthz
const startUpload = async () => {
let task = requets.shift();
if (task) {
let res = await this.$http.post('/uploadSliceFile', task.formData, {
onUploadProgress: e => {
this.chunks[task.index]['progress'] = Number(
((e.loaded / e.total) * 100).toFixed(2),
)
},
})
if (cur === (len - 1)) {
resolve()
} else {
cur ++
startUpload()
}
}
}
while(limit>0) {
startUpload()
limit --
}
})
报错重试上传
假设限制每个切片最多上传3次。
每个切片的添加一个err属性为0,用于记录当前切片上传失败的次数,在以上并发处理的外层,加一层try catch。当有切片返回接口报错的时候
- 当没有超过3次时,err+1,将当前任务添加到切片数组的开头,重新执行,同时更新进度条
- 已经3次了,直接reject
本文使用 mdnice 排版