⼀个上传组件,需要具备的功能:
-
- 需要校验⽂件格式
-
- 可以上传任何⽂件,包括超⼤的视频⽂件(切⽚)
-
- 上传期间断⽹后,再次联⽹可以继续上传(断点续传)
-
- 要有进度条提示
-
- 已经上传过同⼀个⽂件后,直接上传完成(秒传)
前后端分⼯:
前端:
-
- ⽂件格式校验
-
- 文件hash、⽂件切⽚、md5计算
-
- 发起检查请求,把当前⽂件的hash发送给服务端,检查是否有相同hash的⽂件
-
- 上传进度计算
-
- 上传完成后通知后端合并切⽚
后端:
-
- 检查接收到的hash是否有相同的⽂件,并通知前端当前hash是否有未完成的上传
-
- 接收切⽚
-
- 合并所有切⽚
校验⽂件格式
通过文件后缀名判断
//⽂件路径
var path = "file://upload/example.png";
var index= path.lastIndexOf(".");
var ext = filePath.substr(index+1);
console.log(ext); //png
注意:该⽅式有个弊端,可以随便篡改⽂件的后缀名,⽐如:example.png,可以通过修改其后缀名
example.png -> example.mp4 ,这样即可绕过限制进⾏上传。
通过查看⽂件的⼆进制数据来识别其真实的⽂件类型,因为计算机识别⽂件类型时,并不是真的通过⽂件的后缀名来识别的,⽽是通过 “魔数”(Magic Number)来区分,对于某⼀些类型的⽂件,起始的⼏个字节内容都是固定的,根据这⼏个字节的内容就可以判断⽂件的类型。借助⼗六进制编辑器,可以查看⼀下图⽚的⼆进制数据,我们还是以example.png为例
PNG 类型的图⽚前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A
readBuffer(file, start = 0, end = 2) {
// 获取⽂件的⼆进制数据,因为我们只需要校验前⼏个字节即可,所以并不需要获取整个⽂件的数据
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
let uint8Array = new Uint8Array(reader.result); // 先转成Unicode
// 转成 16进制字符串
let result = Array.prototype.map.call(uint8Array, (x) => ('00' + x.toString(16)).slice(-2).toUpperCase()).join(' ');
resolve(result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file.slice(start, end));
});
}
判断是否为PNG
async isPNG(file){
const png = '89 50 4E 47 0D 0A 1A 0A';
const exname = await readBuffer(file,0,8);
return png === exname;
}
hash生成
通过 spark-md5来计算⽂件内容的hash值。
getHash(file,size = 1024 * 1024 * 2){
return new Promise((resolve,reject)=> {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let file = this.files[0];
let chunkSize = size; // Read in chunks of 2MB
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
fileReader.onload = function (e) {
console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
console.log('finished loading');
console.info('computed hash', spark.end()); // Compute hash
resolve(spark.end())
}
};
fileReader.onerror = reject;
function loadNext() {
var start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
})
文件上传,断点续传、上传进度、秒传
在拿到切⽚和md5后,我们⾸先去服务器查询⼀下,是否已经存在当前⽂件。
-
如果已存在,并且已经是上传成功的⽂件,则直接返回前端上传成功,即可实现"秒传"。
-
如果已存在,并且有⼀部分切⽚上传失败,则返回给前端已经上传成功的切⽚name,前端拿到后,根据返回的切⽚,计算出未上传成功的剩余切⽚,然后把剩余的切⽚继续上传,即可实现"断点续传"。
-
如果不存在,则开始上传,这⾥需要注意的是,在并发上传切⽚时,需要控制并发量,避免⼀次性上传过多切⽚,导致崩溃
上传chunk
function upload({ chunkList, chunk, fileMd5, i }, err=2) {
let form = new FormData()
form.append("data", chunk) //切⽚流
form.append("total", chunkList.length) //总⽚数
form.append("index", i) //当前是第⼏⽚
form.append("fileMd5Value", fileMd5Value)
return axios({
method: 'post',
url: '/upload',
data: form
}).then(({ data }) => {
if (data.stat) {
current = current + 1
// 获取到上传的进度
const uploadPercent = Math.ceil((current / chunkList.length) * 100)
}
}).catch(err=> {
// code 判断 超时重传
err && upload({ chunkList, chunk, fileMd5, i }, err--)
})
}
由于计算hash,分片上传这些操作比较消耗性能,可以使用web work
// upload.js
// 创建⼀个worker对象
const worker = new worker('worker.js')
// 向⼦线程发送消息,并传⼊⽂件对象和切⽚⼤⼩,开始计算分割切⽚
worker.postMessage(file,ChunkSize)
// ⼦线程计算完成后,会将切⽚返回主线程
worker.onmessage = (chunks) => {
...
}