文件上传
如何做出亮点
- 上传文件格式验证
- 验证后缀,但是无法防范恶意改名文件
- 二进制流验证 (hexdunm) 一个可靠的文件校验
- 断点续传
- 对文件进行切片,计算 md5 然后按数组上传
- 秒传,计算 Md5 然后进行比较
- 大文件 使用 webwork 计算
- fiber 架构,利用空闲时间计算
- 对文件进行抽样数据再计算 md5
- 并发数控制+错误重试
- 并发 + tcp 冷启动
- 思考
- 碎片清理
- 文件碎片存储在多个机器上
- 文件碎片备份
- 兼容性更好的 requestIdleCallback
- 抽样 hash + 全量 hash 双重判断
- websocket 推送
- cdn
- …
不同事件上传 + 进度条
拖拽上传
drag.addEventListener("dragover", () => {});
drag.addEventListener("dragleave", () => {});
drag.addEventListener("drop", () => {});上传文件格式验证
- 验证后缀,但是无法防范恶意改名文件
- 二进制流验证 (hexdunm) 一个可靠的文件校验
断点续传
计算 hash 这种事都可以放入 Web Worker,不会阻塞渲染进程
- 整个文件进行 hash 计算,1.5 个 G 大概需要 20s
- 切片以后计算,再合并计算, 看并发数量
时间切片
秒传
结合布隆过滤器 的思想,以 2M 的切片为例,取首尾各一个切片,其他切片只取首中尾各两个字节后,合并后计算 hash,1.5G 的文件大概只需要 1s
async calculateHashSample() {
return new Promise(resolve => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
const file = this.container.file;
// 文件大小
const size = this.container.file.size;
let offset = 2 * 1024 * 1024;
let chunks = [file.slice(0, offset)];
// 前面100K
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;
}
// 拼接
reader.readAsArrayBuffer(new Blob(chunks));
reader.onload = e => {
spark.append(e.target.result);
resolve(spark.end());
};
});
}
并发数控制+错误重试
async sendRequest(forms, max=4) {
return new Promise(resolve => {
const len = forms.length;
let idx = 0;
let counter = 0;
const start = async ()=> {
// 有请求,有通道
while (idx < len && max > 0) {
max--; // 占用通道
console.log(idx, "start");
const form = forms[idx].form;
const index = forms[idx].index;
idx++
request({
url: '/upload',
data: form,
onProgress: this.createProgresshandler(this.chunks[index]),
requestList: this.requestList
}).then(() => {
max++; // 释放通道
counter++;
if (counter === len) {
resolve();
} else {
start();
}
});
}
}
start();
});
}
async uploadChunks(uploadedList = []) {
// 这里一起上传,碰见大文件就是灾难
// 没被hash计算打到,被一次性的tcp链接把浏览器稿挂了
// 异步并发控制策略,我记得这个也是头条一个面试题
// 比如并发量控制成4
const list = this.chunks
.filter(chunk => uploadedList.indexOf(chunk.hash) == -1)
.map(({ chunk, hash, index }, i) => {
const form = new FormData();
form.append("chunk", chunk);
form.append("hash", hash);
form.append("filename", this.container.file.name);
form.append("fileHash", this.container.hash);
return { form, index };
})
- .map(({ form, index }) =>
- request({
- url: "/upload",
- data: form,
- onProgress: this.createProgresshandler(this.chunks[index]),
- requestList: this.requestList
- })
- );
- // 直接全量并发
- await Promise.all(list);
// 控制并发
+ const ret = await this.sendRequest(list,4)
if (uploadedList.length + list.length === this.chunks.length) {
// 上传和已经存在之和 等于全部的再合并
await this.mergeRequest();
}
},
碎片清理
如果很多人传了一半就离开了,这些切片存在就没意义了,可以考虑定期清理,当然,我们可以使用node-schedule 来管理定时任务比如我们每天扫一次 target,如果文件的修改时间是一个月以前了,就直接删除把
[let start = function(UPLOAD_DIR){ // 每5秒 schedule.scheduleJob("*/5 * * * * *",function(){ console.log('开始扫描') scan(UPLOAD_DIR) }) }](<// 为了方便测试,我改成每5秒扫一次, 过期1钟的删除做演示
const fse = require('fs-extra')
const path = require('path')
const schedule = require('node-schedule')
// * * * * * *
// ┬ ┬ ┬ ┬ ┬ ┬
// │ │ │ │ │ │
// │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
// │ │ │ │ └───── month (1 - 12)
// │ │ │ └────────── day of month (1 - 31)
// │ │ └─────────────── hour (0 - 23)
// │ └──────────────────── minute (0 - 59)
// └───────────────────────── second (0 - 59, OPTIONAL)
let start = function(UPLOAD_DIR){
// 每5秒
schedule.scheduleJob("*/5 * * * * *",function(){
console.log('开始扫描')
scan(UPLOAD_DIR)
})
}
大文件切片下载
- 使用 http 的 Range 这个 header 就可以切片下载了 HTTP 如何处理大文件的传输?
- Content-length 整片下载通过这个知道进度条