文件秒传以及断点续传

上一篇文章中我们已经了解到了前端的文件切片上传以及后端的切片合并的思路及原理,这一次我们来聊聊文件的秒传以及断点续传。过程只有核心代码,完整代码看文末哈。

秒传是如何实现的?

每个文件能通过算法来计算hash值,且相同的文件的hash是一样的。这样我们就能在上传文件之前,调用接口查询数据库是否有相同的hash的文件,如果有则直接返回上传成功。

前端计算hash,一般使用第三方库spark-md5 。安装一下

css 复制代码
npm i spark-md5 

核心代码

vue 复制代码
<template>
  <div v-loading.fullscreen="loading" class="container">
    <input type="file" @change="onFileChange" />
  <el-button type="primary" @click="upload" :loading="loading">上传</el-button>
  </div>
</template>

上一个文章是选项式API的写法,为了熟悉组合式API以及setup语法糖,所以这里更改了写法。在input标签监听change事件,然后在事件会调用计算hash值。FileReader是一个文件读取类,能够以多种方式读取文件,如readAsArrayBuffer readAsDataURL readAsText等等,读取是异步的,读取完成会执行onload回调。

vue 复制代码
import axios from 'axios';
import SparkMD5 from 'spark-md5'
import { ref } from 'vue';
import { ElMessage } from 'element-plus'

const file_hash = ref('')
const file = ref(null)
const filename = ref("")

const onFileChange = (event) => {
  file.value = event.target.files[0]; //获取文件对象
      filename.value = file.value.name
      const reader = new FileReader(); 
      const sparkk = new SparkMD5.ArrayBuffer() 
      reader.readAsArrayBuffer(file.value);
      reader.onload = (e) => {
        sparkk.append(e.target.result);//直接添加整个文件
        file_hash.value =sparkk.end(); //调用end方法,返回hash
      }
}

这时候我们拿到了文件的hash,文件名和文件对象。这时候我们写一个调接口的方法checkHaveFilecheck接口是为了上传之前查看服务器有无该文件或该文件hash

vue 复制代码
const checkHaveFile = async () => {
  const res =  await axios.get('http://127.0.0.1:3000/check', {
        params: {
          filename: filename.value,
          file_hash: file_hash.value
        }
      });
      console.log('res',res);      
      return res.data
}

从上面template中可知在点击上传按钮的时候,调用upload

vue 复制代码
const upload = async() => {
  let continueChunk = 0
  if (!file.value) {
    ElMessage({
      message: '请选择文件',
      type: 'error',
    })
    return;
  }
  const haveFileRes = await checkHaveFile();

  if (haveFileRes.code === 201) {
    ElMessage({
      message: '秒传成功',
      type: 'success',
    })
    return
  } else if (haveFileRes.code === 202) {
    // ElMessage('断点续传');
    continueChunk = haveFileRes.data
  }
  // 剩下的逻辑...
}

上一篇文章说过用nodejs来写接口,因为实现相对简单(其实是只会这个)。

先看一下我的node服务端的目录结构:

写这个check接口。现在写一下后端的check接口。可以看到引入了hashFns

js 复制代码
const hashFns = require('./utils/hashFns')

app.get('/check', async(req, res) => {
  const { filename, file_hash } = req.query;
  const hasFile = await hashFns.checkHash(file_hash) //查看服务器有没有该文件
  const hasChunk = await hashFns.hasChunk(file_hash) //查看是否存在切片,断点续传要用
  console.log('hasChunk',hasChunk);
  
  if(hasFile){
    res.send({
      code: 201,
      msg: '秒传成功',
    });
  }else if(hasChunk!==-1){
    res.send({
      code:202,
      msg:'断点续传',
      data:hasChunk
    })
  }else{
    res.send({
      code:200,
      msg:'文件不存在,继续上传',
    })
  }
})
js 复制代码
//./utils/hashFns.js
//读取json文件是否存在该hash
const checkHash = (hash, filePath = path.resolve(__dirname, 'db.json')) => {
  return new Promise((resolve, reject) => {
    // 读取JSON文件
    fs.readFile(filePath, 'utf8', (err, data) => {
      if (err) {
        console.error('Error reading file:', err);
        reject()
        return;
      }
      // 解析JSON数据
      let jsonData = JSON.parse(data);
      console.log(Object.keys(jsonData).includes(hash))
      if (jsonData[hash]) {
        console.log('true')
        resolve(true)
      } else {
        console.log('false')
        resolve(false)
      }
    });
  })
}

鉴于主要提供思路及快速实现,我用一个db.json文件模拟数据库

json 复制代码
//db.json
{
  "1da8e8fde4d9ab8e4047e8ca5493d420": "d:\\server\\uploadTest\\acceptFiles\\22.jpg",
  "868f746a5ebbc15aab4a6185a0b1803b": "d:\\server\\uploadTest\\acceptFiles\\33.jpg"
}

可以看到以上checkHash如果返回true,则说明db.json中找到了与前端上传的hash相同的文件,check接口会返回{code:201,msg: '秒传成功'}

尝试一下,选择一个文件上传

服务器已经接收到

再次上传相同文件

断点续传是如何实现的?

其实和秒传的原理差不多,都是通过hash来进行操作的。假如我要上传学习资料.mp4 文件,想要断点续传,那就先切片。假如分了10个切片,在上传了 5 个切片时,网络断了,服务器只收到了 5 个切片。这时候我要想重新上传剩下的切片,服务器需要一个这个文件hash作为标识,就比如我直接用这个hash当作目录名称了,那是不是我直接查找这个目录名为该hash的目录内有多少个切片并且返回浏览器,前端就知道我们要从哪里开始上传了。 同样是先通过check接口先查询,还记得node接口check是不是还调用了hashFns.hasChunk(file_hash),这个就是查询temp目录有无相同hash的目录名存在的。

javascript 复制代码
//./utils/hashFns.js
const hasChunk = (hash) => {
  return new Promise((resolve, reject) => {
    //查看是否存在 temp 文件夹
    try {
      // 检查目录是否存在
      fs.accessSync(path.resolve(__dirname, "../temp"), fs.constants.F_OK);
      console.log('Directory exists.');
    } catch (err) {
      if (err.code === 'ENOENT') {
        console.log('Directory does not exist.');
        fs.mkdirSync(path.resolve(__dirname, '../temp'), { recursive: true });
      } else {
        // 其他错误,如目录不可读取
        console.error('An error occurred:', err);
      }
    }
    //查看temp目录下是否有目录名与hash相同的目录
    fs.readdir(path.resolve(__dirname, "../temp"), (err, files) => {
      if (err) {
        reject()
      }
      console.log("__files", files)
      // resolve(files.indexOf(hash))
      if (files.indexOf(hash) === -1) {
        resolve(-1)
      } else {
        //如果有与该hash相同的文件,则读取该文件夹下的文件数,也就是切片数量
        resolve(new Promise((resolveChunk, rejectChunk) => {
          fs.readdir(path.resolve(__dirname, "../temp", hash), (err, chunkfiles) => {
            if (err) {
              rejectChunk()
            }
            resolveChunk(chunkfiles.length)
          })
        }))
      }
    })
  })
}

如果有则会返回切片数量,check接口会返回{ code:202, msg:'断点续传',data:hasChunk}data就是服务器已存在的切片数量。前端进行切片操作的时候,根据这个数量来设置开始切片的index就行了,当上传完成会调用/upload/complete接口,以下分别是上传和上传完成的接口。

js 复制代码
// 很显然,这里使用了个中间件,是存储文件的
app.post('/upload',upload.single('file'),(req, res) => {
  res.send({
    code: 200,
    msg: '上传成功',
  });
});
//当前端所有切片上传完成之后,会调用这个接口进行合并
app.post('/upload/complete', (req, res) => {
  const file_hash = req.body.file_hash;
  const filename = req.body.filename;
  console.log('file_hash',file_hash);
  try {
    combineChunks(ACCEPTDIR,path.resolve(__dirname, 'temp',file_hash),filename)
    .then(() => {
      hashFns.updatedHash(file_hash,path.resolve(ACCEPTDIR, filename) )
      res.json('文件上传完成');
    })
  }catch (error) {
    res.json('文件上传失败', err);
    fs.rm(path.resolve(__dirname,'temp', file_hash), { recursive: true }, (err) => {
      if (err) {
        // 处理错误
        console.error('临时文件夹删除失败',err);
      } else {
        // 删除成功
        console.log('临时文件夹删除成功.');
      }
    } );
    console.log('合并文件时发生错误', error);
  }
});

对于upload接口的中间件以及合并切片操作可以参考上篇文章或者下载完整demo代码(文末)。

我们来试一下,选择文件

点击上传然后快速按 F5 模拟上传中断。可以看到temp文件夹存在了几个切片且目录名是hash

再次选择相同文件,点击上传,check接口返回数据data:6,根据这个数来再次分片上传。

可以看到服务器已经上传成功。这个时候有个问题,上一次上传的切片是删不掉的(因为我已经改了,图片没有展示出来),因为上次上传中断了,mutler这个第三方包并没有关闭文件流,这个时候我们要监听一下aborted事件,在这个事件触发的时候关闭文件流。这样就大功告成了!

js 复制代码
const storage = multer.diskStorage({
  // 设置文件存储的位置,其中回调函数的第一个参数表示错误信息,第二个参数表示文件的存储路径
  destination: function (req, file, cb) {
    // 创建临时文件夹
    try {
      fs.mkdirSync(path.resolve(__dirname, 'temp',req.body.fileHash), { recursive: true });
    } catch (error) {
      console.log(' 创建临时文件夹失败',error);
    }
    cb(null, path.resolve(__dirname, 'temp',req.body.fileHash));
  },
  // 设置文件名,同上,第一个参数表示错误信息,第二个参数表示文件名
  filename:async function (req, file, cb) {
    req.on('aborted',(e)=>{
      file.stream.emit('end') //手动关闭流
    })
    await cb(null, file.fieldname + '_' + req.body.chunkIndex);
  }
});

完整demo代码gitee.com/cweile/uplo...

相关推荐
狂炫冰美式6 分钟前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw51 小时前
npm几个实用命令
前端·npm
!win !1 小时前
npm几个实用命令
前端·npm
代码狂想家1 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv2 小时前
优雅的React表单状态管理
前端
蓝瑟3 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv3 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱3 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder3 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端
清妍_3 小时前
一文详解 Taro / 小程序 IntersectionObserver 参数
前端