1、整体思路

  1. 将文件切成多个小的文件;
  2. 将切片并行上传;
  3. 所有切片上传完成后,服务器端进行切片合成;
  4. 当分片上传失败,可以在重新上传时进行判断,只上传上次失败的部分;
  5. 当切片合成为完整的文件,通知客户端上传成功;
  6. 已经传到服务器的完整文件,则不需要重新上传到服务器,实现,秒传功能;

2、实现步骤

2.1 文件切片加密

利用MD5 , MD5 是文件的唯一标识,可以利用文件的 MD5 查询文件的上传状态;

读取进度条进度,生成MD5:
image.png

实现结果:
image.png

实现代码如下:

  1. const md5File = (file) => {
  2. return new Promise((resolve, reject) => {
  3. // 文件截取
  4. let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
  5. chunkSize = file?.size / 100,
  6. chunks = 100,
  7. currentChunk = 0,
  8. spark = new SparkMD5.ArrayBuffer(),
  9. fileReader = new FileReader();
  10. fileReader.onload = function (e) {
  11. console.log('read chunk nr', currentChunk + 1, 'of', chunks);
  12. spark.append(e.target.result);
  13. currentChunk += 1;
  14. if (currentChunk < chunks) {
  15. loadNext();
  16. } else {
  17. let result = spark.end()
  18. resolve(result)
  19. }
  20. };
  21. fileReader.onerror = function () {
  22. message.error('文件读取错误')
  23. };
  24. const loadNext = () => {
  25. const start = currentChunk * chunkSize,
  26. end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
  27. // 文件切片
  28. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  29. // 检查进度条
  30. dispatch({ type: 'check', checkPercent: currentChunk + 1 })
  31. }
  32. loadNext();
  33. })
  34. }

2.2 查询上传文件状态

利用当前md5去查询服务器创建的md5文件夹是否存在,如果存在则返回该目录下的所有分片;
image.png

前端只需要拿MD5和文件名去请求后端,这里就不在列出来;

node端代码逻辑:

  1. app.get('/check/file', (req, resp) => {
  2. let query = req.query
  3. let fileName = query.fileName
  4. let fileMd5Value = query.fileMd5Value
  5. // 获取文件Chunk列表
  6. getChunkList(
  7. path.join(uploadDir, fileName),
  8. path.join(uploadDir, fileMd5Value),
  9. data => {
  10. resp.send(data)
  11. }
  12. )
  13. })
  14. // 获取文件Chunk列表
  15. async function getChunkList(filePath, folderPath, callback) {
  16. let isFileExit = await isExist(filePath)
  17. let result = {}
  18. // 如果文件已在存在, 不用再继续上传, 真接秒传
  19. if (isFileExit) {
  20. result = {
  21. stat: 1,
  22. file: {
  23. isExist: true,
  24. name: filePath
  25. },
  26. desc: 'file is exist'
  27. }
  28. } else {
  29. let isFolderExist = await isExist(folderPath)
  30. // 如果文件夹(md5值后的文件)存在, 就获取已经上传的块
  31. let fileList = []
  32. if (isFolderExist) {
  33. fileList = await listDir(folderPath)
  34. }
  35. result = {
  36. stat: 1,
  37. chunkList: fileList,
  38. desc: 'folder list'
  39. }
  40. }
  41. callback(result)
  42. }

2.3 秒传

如果上传的当前文件已经存在服务器目录,则秒传;

服务器端代码已给出,前端根据返回的接口做判断;

  1. if (data?.file) {
  2. message.success('文件已秒传')
  3. return
  4. }

实现效果:
image.png

2.4 上传分片、断点续传

检查本地切片和服务器对应的切片,如果没有当前切片则上传,实现断点续传;

同步并发上传所有的切片,维护上传进度条状态;

前端代码:

  1. /**
  2. * 上传chunk
  3. * @param {*} fileMd5Value
  4. * @param {*} chunkList
  5. */
  6. async function checkAndUploadChunk(file, fileMd5Value, chunkList) {
  7. let chunks = Math.ceil(file.size / chunkSize)
  8. const requestList = []
  9. for (let i = 0; i < chunks; i++) {
  10. let exit = chunkList.indexOf(i + "") > -1
  11. // 如果不存在,则上传
  12. if (!exit) {
  13. requestList.push(upload({ i, file, fileMd5Value, chunks }))
  14. }
  15. }
  16. // 并发上传
  17. if (requestList?.length) {
  18. await Promise.all(requestList)
  19. }
  20. }
  21. // 上传chunk
  22. function upload({ i, file, fileMd5Value, chunks }) {
  23. current = 0
  24. //构造一个表单,FormData是HTML5新增的
  25. let end = (i + 1) * chunkSize >= file.size ? file.size : (i + 1) * chunkSize
  26. let form = new FormData()
  27. form.append("data", file.slice(i * chunkSize, end)) //file对象的slice方法用于切出文件的一部分
  28. form.append("total", chunks) //总片数
  29. form.append("index", i) //当前是第几片
  30. form.append("fileMd5Value", fileMd5Value)
  31. return axios({
  32. method: 'post',
  33. url: BaseUrl + "/upload",
  34. data: form
  35. }).then(({ data }) => {
  36. if (data.stat) {
  37. current = current + 1
  38. const uploadPercent = Math.ceil((current / chunks) * 100)
  39. dispatch({ type: 'upload', uploadPercent })
  40. }
  41. })
  42. }

Node端代码:

  1. app.all('/upload', (req, resp) => {
  2. const form = new formidable.IncomingForm({
  3. uploadDir: 'nodeServer/tmp'
  4. })
  5. form.parse(req, function(err, fields, file) {
  6. let index = fields.index
  7. let fileMd5Value = fields.fileMd5Value
  8. let folder = path.resolve(__dirname, 'nodeServer/uploads', fileMd5Value)
  9. folderIsExit(folder).then(val => {
  10. let destFile = path.resolve(folder, fields.index)
  11. copyFile(file.data.path, destFile).then(
  12. successLog => {
  13. resp.send({
  14. stat: 1,
  15. desc: index
  16. })
  17. },
  18. errorLog => {
  19. resp.send({
  20. stat: 0,
  21. desc: 'Error'
  22. })
  23. }
  24. )
  25. })
  26. })

实现效果:
image.png
存储形式:
image.png

2.5 合成分片还原完整文件

当所有的分片上传完成,前端通知服务器端分片上传完成,准备合成;

前端代码:

  1. /**
  2. * 所有的分片上传完成,准备合成
  3. * @param {*} file
  4. * @param {*} fileMd5Value
  5. */
  6. function notifyServer(file, fileMd5Value) {
  7. let url = BaseUrl + '/merge?md5=' + fileMd5Value + "&fileName=" + file.name + "&size=" + file.size
  8. axios.get(url).then(({ data }) => {
  9. if (data.stat) {
  10. message.success('上传成功')
  11. } else {
  12. message.error('上传失败')
  13. }
  14. })
  15. }

Node端代码:

  1. // 合成
  2. app.all('/merge', (req, resp) => {
  3. let query = req.query
  4. let md5 = query.md5
  5. let fileName = query.fileName
  6. console.log(md5, fileName)
  7. mergeFiles(path.join(uploadDir, md5), uploadDir, fileName)
  8. resp.send({
  9. stat: 1
  10. })
  11. })
  12. // 合并文件
  13. async function mergeFiles(srcDir, targetDir, newFileName) {
  14. let fileArr = await listDir(srcDir)
  15. fileArr.sort((x,y) => {
  16. return x-y;
  17. })
  18. // 把文件名加上文件夹的前缀
  19. for (let i = 0; i < fileArr.length; i++) {
  20. fileArr[i] = srcDir + '/' + fileArr[i]
  21. }
  22. concat(fileArr, path.join(targetDir, newFileName), () => {
  23. console.log('合成成功!')
  24. })
  25. }

请求实现:

image.png

合成文件效果:
image.png

3、总结

  1. 将文件切片,并发上传切片,切片合成完整文件,实现分片上传;
  2. 使用MD5标识文件夹,得到唯一标识;
  3. 分片上传前通过文件 MD5 查询已上传切片列表,上传时只上传未上传过的切片,实现断点续传;
  4. 检查当前上传文件,如果已存在服务器,则不需要再次上传,实现秒传;

4、代码地址

https://github.com/linhexs/file-upload.git