单文件上传 【断点续传】前端+后端代码实现逻辑

240 阅读1分钟

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #pro {
      height: 10px;
      width: 0;
      background-color: #f00;
    }
  </style>
</head>
<body>
  <h3>单文件上传 【断点续传】</h3>
  <input type="file" id="fileIpt">
  <h3>进度:</h3>
  <div id="pro"></div>
  <span id="protxt"></span>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.26.0/axios.min.js"></script>
<script>
  $(function(){
    $('#fileIpt').change(res => {
      const file = res.target.files[0];
      obj.uploadFile(file);
    })
  })
  var progressval = 0; // 进度
  var slipefile = []; // 切片文件数组
  var urladdress = ''; // 文件地址
  const obj = {
    async uploadFile(file) {
      slipefile = [];
      let chunks = await this.getFileChunks(file, Math.pow(1024, 2));
      // console.log(chunks);
      await chunks.forEach(chunk => {
        let formData = new FormData();
        formData.append('file', chunk.file);
        axios({
          url: 'http://192.168.88.214:3001/uploadsplit',
          method: 'post',
          data: formData,
          params: {
            hash: chunk.hash,
            name: chunk.name,
            type: chunk.type,
          }
        }).then(async res => {
          progressval = Number(((chunk.name + 1) / chunks.length * 100).toFixed(2));
          slipefile.push(res.data);
          document.querySelector('#pro').style.width = progressval + 'px';
          document.querySelector('#protxt').innerHTML = progressval + '%';
        }).catch(err => {
          // slipefile = { err };
        })
      });
      let { hash, type } = await this.getFileInfo(file);
      axios({
        url: 'http://192.168.88.214:3001/uploadmerge',
        method: 'get',
        params: { hash, type }
      }).then(result => {
        progressval = 100;
        slipefile = [result.data, ...slipefile];
        document.querySelector('#pro').style.width = progressval + 'px';
        document.querySelector('#protxt').innerHTML = progressval + '%';
        // urladdress = result.data
        console.log(result.data);
      })
    },
    getFileInfo(file) {
      return new Promise((resolve, reject) => {
        let fileReader = new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onload = (e) => {
          let buffer = e.target.result;
          let spark = new SparkMD5.ArrayBuffer();
          spark.append(buffer);
          let hash = spark.end();
          resolve({ hash, type: file.type });
        }
      })
    },
    /**
     * 文件切片
     * file 文件
     * size 单个切片大小
     */
    async getFileChunks(file, size) {
      let { hash, type } = await this.getFileInfo(file);
      let chunks = [];
      let count = Math.ceil(file.size / size);
      let index = 0;
      while (index < count) {
        chunks.push({ file: file.slice(index * size, (index + 1) * size), hash, name: index, type });
        index++;
      }
      return chunks;
    }
  }
</script>
</html>
  • 后端代码(nodejs)
const express = require('express');
const UploadRote = express.Router();
const path = require('path');
const fs = require('fs');
var multiparty = require('multiparty');
var mime = require('mime-types');
const multipartyUploadFile = (req) => {
  let { hash, name } = req.query;
  return new Promise((resolve, reject) => {
    let form = new multiparty.Form({});
    form.parse(req, (err, fields, files) => {
      if (err) return reject(err);
      let file = files.file[0];
      let dir = `${path.join(__dirname, '../')}upload/${hash}`;
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir)
      };
      let savePath = `${dir}/${name}`;
      fs.renameSync(file.path, savePath);
      file.realPath = savePath;
      return resolve({ fields, files });
    });
  })
}
// 切片上传
UploadRote.post('/uploadsplit', (req, res) => {
  let { hash, name, type } = req.query;
  let dir = `${path.join(__dirname, '../')}upload/${hash}`;
  // 文件是否已经上传过
  let realPath = `${dir}.${mime.extension(type)}`;
  if (fs.existsSync(realPath)) {
    res.status(200).json({ realPath, msg: '文件已存在,无需上传!' });
    return
  };
  // 切片路径 判断切片是否上传过
  let chunkPath = `${dir}/${name}`;
  if (fs.existsSync(chunkPath)) {
    res.status(200).json({ chunkPath, msg: '切片已存在,跳过此切片' });
  } else {
    multipartyUploadFile(req).then(value => {
      res.status(200).json(value);
    }).catch(reason => {
      res.status(500).json(reason);
    })
  };
});
// 合并文件
UploadRote.get('/uploadmerge', (req, res) => {
  let { hash, type } = req.query;
  let dir = `${path.join(__dirname, '../')}upload/${hash}`;
  // 文件是否已经上传过
  let realPath = `${dir}.${mime.extension(type)}`;
  if (fs.existsSync(realPath)) {
    res.status(200).json({ realPath, msg: '文件已存在,无需合并!' });
    return
  }
  let fileList = fs.readdirSync(dir);
  fileList.sort((a, b) => a - b).forEach(item => {
    fs.appendFileSync(`${dir}.${mime.extension(type)}`, fs.readFileSync(`${dir}/${item}`));
    fs.unlinkSync(`${dir}/${item}`);
  })
  fs.rmdirSync(dir);
  res.status(200).json({ path: `${dir}.${mime.extension(type)}`, msg: '合并成功!' });
});