文件断点续传(上传)以及秒传

前言:文件断点续传,指的是在进行上传或者下载时,将文件分割成几块,每一块都单独上传或下载,如果某一块由于网络原因或者其他因素,导致上传或下载失败,那么可以从已经上传或下载的部分继续进行上传下载未完成的部分,节省时间,提高效率。此外,也支持暂停上传或下载。
文件秒传,指的是当用户上传一个已被上传过(自己或他人上传到同一个文件服务器中)的文件时,文件能够直接跳过上传过程,达到秒传的效果。其中的原理就是,用户上传文件时,会同时发送文件的md5值,服务端会进行相应查找匹配,如果有相同的md5,则返回通知文件已上传过服务器,不必再次上传,实现秒传。
本文主要讲断点续传之上传和秒传的实现

在 HTML5 普及之前,上传基本是通过flash实现的,断点续传的实现也相对麻烦,本篇就不涉及啦,下面进入主题。
通过 HTML5 File API 进行文件分割,File 接口是基于 Blob 对象

Blob 对象表示不可变的类似文件对象的原始数据,包含两个属性 size (Blob对象中数据的大小)和 type (Blob对象包含数据的MIME类型)。还拥有一个方法 slice,返回一个新的 Blob对象,包含了源 Blob对象中指定范围内的数据。

对文件进行分割,使用的就是 File 继承于 Blob 对象的 slice 方法。

1
2
3
4
5
6
7
8
var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
var currentChunk = 0 // 当前文件块的编号
var chunkSize = 2 * 1024 * 1024 // 每一块文件块的大小,一般设为2~5MB
var start = currentChunk * chunkSize
var end = (start + chunkSize > fileSize) ? fileSize : (start + chunkSize)
blobSlice.call(file, start, end)

对文件分割完成后,则进行上传,上传时客户端和服务端都会保存当前文件块的编号。上传完成后,则进行下一块上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function uploadData () {
if (!fileSize) {
alert('请选择要上传的文件')
return
}
var xhr = new XMLHttpRequest()
var chunkData = sliceChunks() // 分割文件,返回文件块数据
var formData = new FormData()
fileIndex = currentChunk + 1 // 文件编号
if (fileIndex > chunks) return // 文件编号大于文件块总数时则终止上传
formData.append('fileName', file.name)
formData.append('fileSize', file.size)
formData.append('fileIndex', fileIndex)
formData.append('fileContent', chunkData)
// 文件块上传完成后,则继续上传下一块
xhr.upload.onload = function (e){
currentChunk++
if (fileIndex < chunks) {
uploadData ()
}
}
// 显示上传进度
xhr.upload.onprogress = function (e) {
uploadPrg.innerText = (fileIndex * 10000 / (chunks * 100)).toFixed(2) + '%'
fileIndex === chunks && (uploadBtn.value = '上传完成')
}
xhr.open("POST", '/upload')
xhr.send(formData)
}

以上主要是前端需要实现,再来看下服务端(采用node.js实现)是如何将文件块接收,并拼接成完整文件的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const http = require('http')
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
var multipart = require('connect-multiparty') //在处理模块中引入第三方解析模块
var multipartMiddleware = multipart()
var uploadPath = './upload'
var app = express()
var fileIndex
app.post('/upload', multipartMiddleware, (req, res, next) => {
let data = req.body
let fileData = req.files.fileContent
// 上传目录是否存在,不存在则创建
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath)
}
if (!fileIndex || fileIndex < data.fileIndex) {
fileIndex = parseInt(data.fileIndex)
let fileChunk = fs.readFileSync(fileData.path)
fs.appendFile('./upload/' + data.fileName, fileChunk)
} else {
fileIndex = ''
fs.renameSync(fileData.path, './upload/' + data.fileName)
}
res.writeHead(200, {'Content-Type': 'text/plain'})
res.end()
})

在服务端实现过程中,主要的难点是如何接收前端传输的参数,前端参数都是通过 FormData 传到服务端,服务端需要对参数进行解析,这里采用 connect-multiparty 库进行解析参数。参数接收完成后,则进行文件的写入操作。

秒传的实现就较为简单了,主要是上传第一块文件块时,会将文件的md5值传到服务端,然后进行查找匹配。难点在于如何获取文件的md5值。获取文件的md5主要是使用 spark-md5 库。

1
2
3
4
5
6
7
8
9
10
11
12
import sparkMD5 from 'spark-md5'
var fileReader = new FileReader()
var spark = new sparkMD5.ArrayBuffer()
// 读取文件内容
fileReader.readAsArrayBuffer(file)
fileReader.onload = function (e) {
// 计算md5
spark.append(e.target.result)
file.fileMD5 = spark.end()
}

此外,对于超大文件(1G以上)需要做下优化处理,因为浏览器读取文件流的能力有限,如果一次性读取超大文件的文件流(FileReader),那么浏览器占用内存会瞬间飙升,系统会变得卡顿,甚至直接浏览器崩溃。

优化的方案是:将文件进行切割分块,一块一块地读取到文件流中,然后计算MD5值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import sparkMD5 from 'spark-md5'
var chunkSize = 1024 * 1024 * 2, // 每一个文件块为2MB
chunks = Math.ceil(file.size / chunkSize), // 文件块总数
chunk = 0, // 文件块的序号
spark = new sparkMD5.ArrayBuffer(),
fileReader = new FileReader()
function loadNext () {
var start, end
start = chunk * chunkSize
end = Math.min(start + chunkSize, file.size)
// 分块读取文件流
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
fileReader.onload = (e) => {
spark.append(e.target.result)
}
fileReader.onloadend = () => {
fileReader.onload = fileReader.onloadend = null
if (++chunk < chunks) {
setTimeout(loadNext, 1)
} else {
setTimeout(() => {
file.fileMD5 = spark.end()
loadNex = spark = null
// 执行文件上传操作
uploadFile(file)
}, 50)
}
}
}
loadNext()

源码:https://github.com/Peterlhx/html5-resume 如果对你有用,欢迎star~

坚持原创技术分享,您的支持将鼓励我继续创作!