大文件上传【切片上传】和 【断点续传】

前端:Vue3

效果:

HTML相关代码:

<template>
  <!-- 大文件切片上传和断点续传 -->
  <div class="item">
    <h3>大文件切片上传和断点续传</h3>
    <section class="upload_box" id="upload4">
      <input type="file" class="upload_inp" ref="input" @change="changeInput" />
      <div class="upload_button_box">
        <button class="upload_button select" :class="{ loading: isLoading }" @click="changeFile">
          上传文件
        </button>
      </div>
      <div class="upload_progress" v-show="isShow">
        <div class="progress" ref="progress"></div>
      </div>
      <span class="percent" ref="percent" v-show="isShow">0%</span>
    </section>
  </div>
</template>

scss样式相关代码:

<style lang="scss" scoped>
.item {
  // 文件上传盒子
  .upload_box {
    position: relative;
    padding: 10px;
    box-sizing: border-box;
    //width: 400px;
    min-height: 100px;
    border: 3px dashed rgb(218, 232, 233);

    // input
    .upload_inp {
      display: none; // 隐藏原生的input,太丑了
    }

    // 选择文件盒子
    .upload_button_box {

      // 选择文件、上传文件到服务器
      .upload_button {
        position: relative;
        margin-right: 10px;
        min-width: 80px;
        height: 30px;
        padding: 0 10px;
        line-height: 30px;
        border: none;
        cursor: pointer;
        background-color: #ddd;
        overflow: hidden;
        /*将伪类隐藏出来*/

        &::before,
        &::after {
          position: absolute;
          left: 0;
          z-index: 999;
          transition: top 0.3s; // 过渡
          width: 100%;
          height: 100%;
          padding-left: 25px;
          box-sizing: border-box;
          text-align: left;
        }

        &::before {
          top: -30px;
          content: "";
          background: #eee;
          color: #999;
        }

        &::after {
          top: 30px;
          content: "loading...";
          background: #eee url("../assets/images/loading.gif") no-repeat 5px center;
          color: #999;
        }
      }

      //选择文件按钮
      .select {
        background: #409eff;
        color: #fff;
      }

      //上传文件到服务器按钮
      .upload {
        background: #67c23a;
        color: #fff;
      }

      // 动态类:loading
      .loading {
        cursor: inherit; // 文件上传中,鼠标恢复默认

        &::before,
        &::after {
          top: 0;
        }
      }

      // 动态类:disable
      .disable {
        background: #eee;
        color: #999;
        cursor: inherit;
      }
    }

    // 进度条盒子
    .upload_progress {
      width: 90%;
      display: inline-block;
      position: relative;
      margin-top: 10px;
      height: 3px;
      background: #777777;
      border-radius: 10px;


      .progress {
        position: absolute;
        top: 0;
        left: 0;
        z-index: 999;
        font-size: 12px;
        color: #fff;
        height: 100%;
        background: #67c23a;
        text-align: right;
        border-radius: 10px;
        box-sizing: border-box;
        transition: width 0.1s;
      }
    }

    .percent {
      display: inline-block;
      font-size: 12px;
      color: #67c23a;
      margin-left: 5px;
    }

  }
}
</style>

 javascript相关代码:

<script setup>
import { ref } from "vue";
import $axios from "../utils/axios";
import SparkMD5, { hash } from "spark-md5";
const input = ref(null); // 获取input元素
const progress = ref(null); // 获取进度条元素
const percent = ref(null) // 获取百分比
let _file = ref(null); // 文件源
let isShow = ref(false); // 隐藏提示
let isLoading = ref(false); // 禁用文件上传到服务器按钮
let index = ref(0)
let count = ref(0)

// 选择文件按钮
function changeFile() {
  // 判断选择文件按钮 或者 文件上传到服务器按钮 是否为禁用
  if (isLoading.value) return;
  // 实际上是手动触发原生的input
  input.value.click();
}

// 4.切片合并
async function mergeSlice(HASH, count) {
  // 管控进度条
  index.value++;
  // 设置进度条的宽
  progress.value.style.width = `${((index.value / count) * 100).toFixed(0)}%`;
  // 设置进度条100%
  percent.value.innerHTML = `${((index.value / count) * 100).toFixed(0)}%`;
  console.log('!!!', index.value, HASH, count)
  // 当所有切片都上传成功,再发送请求合并所有的切片
  if (index.value == count) {
    try {
      let data = await $axios.post('/upload_merge', { HASH, count }, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      });
      console.log(data)
      if (+data.code === 0) {
        alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);

        return;
      }
      throw data.codeText;
    } catch (err) {
      alert('切片合并失败,请您稍后再试~~');

    }
  };

}


// 3.把每一个切片都上传到服务器上
function uploadChunks(chunks, HASH) {
  chunks.forEach(async (chunk) => {
    let fm = new FormData;
    fm.append('file', chunk.file);
    fm.append('filename', chunk.filename);
    // 发送请求上传所有切片
    try {
      const data = await $axios.post('/upload_chunk', fm)
      if (+data.code === 0) {
        console.log(data)
        // 发送请求合并所有切片
        mergeSlice(HASH, count.value);
      }
      // if(+data.code === 2) { // 文件已存在
      //   // 设置进度条的宽
      //   progress.value.style.width = `100%`;
      //   // 设置进度条100%
      //   percent.value.innerHTML = `100%`;
      //   console.log(data)
      // }
    } catch (error) {
      // 错误处理
      console.log(error)
    } finally {

    }

  });


}

// 2.实现文件切片处理 「固定数量 & 固定大小」
function fileSlice(file, HASH, suffix, size) {
  let max = 1024 * 10 * size // 切片大小
  count.value = Math.ceil(file.size / size) // 切片数量
  let chunks = []
  let index = 0
  // 如果切片数量 大于20
  if (count.value > 20) {
    max = file.size / 20; // 重新计算最大切片大小
    count.value = 20;  // 重新计算最大切片数量
  }
  while (index < count.value) {
    chunks.push({
      // 切片
      file: file.slice(index * max, (index + 1) * max),
      // 切片名
      filename: `${HASH}_${index + 1}.${suffix}`
    });
    index++;
  }

  return { chunks }
}

// 1.将文件读取成ArrayBuffer 
function changeBuffer(file) {
  return new Promise((resolve, reject) => {
    let fileReader = new FileReader()
    // 将数据读取到:数组缓冲区
    fileReader.readAsArrayBuffer(file)
    // 取完成后通过 onload 事件处理函数来处理读取到的二进制数据
    fileReader.onload = (ev) => {
      // 拿到数组缓冲区数据
      let buffer = ev.target.result
      let spark = new SparkMD5.ArrayBuffer()
      spark.append(buffer)
      // 难道文件的hash值,相同内容的文件hash值是一样的
      let HASH = spark.end();
      // 文件后缀名 例如:mp4
      let suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
      resolve({
        buffer,  // 数组缓冲区数据
        HASH,    // hash值
        suffix,  // 文件后缀名
        filename: `${HASH}.${suffix}` // 文件名
      })
    }

  })
}

// 监听用户选择文件的操作(注意:文件改变触发)
async function changeInput(e) {
  // 拿到文件对象,注意:是数组
  _file.value = e.target.files[0];
  if (!_file.value) return;
  isShow.value = true
  isLoading.value = true

  // 1.获取文件的HASH
  const { HASH, suffix } = await changeBuffer(_file.value);
  console.log(HASH, suffix)

  // 2.实现文件切片处理
  const { chunks } = fileSlice(_file.value, HASH, suffix, 100)
  console.log(chunks)

  // 3.上传切片前,查看文件是否已经上传过
  try {
    const data = await $axios.get(`/has_uploaded?HASH=${HASH}`)
    console.log(data)
    if (+data.code === 2) {
      // 设置进度条的宽
      progress.value.style.width = `100%`;
      // 设置进度条100%
      percent.value.innerHTML = `100%`;
      return
    }
  } catch (error) {

  } finally{
    // isShow.value = true
    isLoading.value = false
  }

  // 4.把每一个切片都上传到服务器上
  uploadChunks(chunks, HASH)
}

后端:node.js

const express = require('express'),
  fs = require('fs'),
  paths = require('path'),
  bodyParser = require('body-parser'),
  // 插件1:处理上传文件(解析form-data文件)
  multiparty = require('multiparty'),
  // 处理客户端文件hash名字
  SparkMD5 = require('spark-md5')
const https = require('https')
const http = require('http')



// 创建app 
const app = express(),
  PORT = 8889,        // 端口
  HOST = 'http://127.0.0.1',  // 协议
  HOSTNAME = `${HOST}:${PORT}`; // 拼接协议、端口

  // 静态资源
  app.use('/upload',express.static('upload'))

// 监听
app.listen(PORT, () => {
  console.log(`端口:${PORT},链接:${HOSTNAME}`)
})


// 创建文件并写入到指定的目录 & 返回客户端结果
// 参数:(响应头,要写入的路径,文件内容,文件名)
const writeFile_formdata = function writeFile(res, path, file, filename) {
  return new Promise((resolve, reject) => {
    // 文件流
    try {
      // 创建可读流,并读取其中的是数据,【file.path 是客服端的文件路径】
      let readStream = fs.createReadStream(file.path),
        // 创建可写流,【file.path 是需要写入到服务器的文件路径】
        writeStream = fs.createWriteStream(path);
      // 当有新的数据可供读取时,readStream会自动推入writeStream中
      readStream.pipe(writeStream);

      readStream.on('end', () => {
        resolve();
        // unlinkSync()函数同步删除文件
        fs.unlinkSync(file.path);
        res.send({
          code: 0,
          codeText: '上传成功',
          originalFilename: filename,
          servicePath: path.replace(__dirname, HOSTNAME)
        });
      });
    } catch (err) {
      reject(err);
      res.send({
        code: 1,
        codeText: err
      });  
    }
  });
};


// 判断文件是否已经上传过
app.get('/has_uploaded',async (req,res) => {
  const {HASH} = req.query
  // 合并好的文件路径
  let mergePath =  `${uploadDir}/${HASH}.mp4`
  // 查看文件是否存在
  let isExists = await exists(mergePath);
  console.log(mergePath,isExists)
  if(isExists) {
    res.send({
      code: 2,
      codeText: '文件已存在',
      servicePath: mergePath.replace(__dirname, HOSTNAME)
    });
    return 
  }
  res.send({
    code: 1,
    codeText: '文件不存在,开始上传切片',
  });

})

// 大文件切片上传 & 合并切片
const merge = function merge(HASH, count) {
  return new Promise(async (resolve, reject) => {
    // 找到需要合并的切片目录
    let path = `${uploadDir}/${HASH}`
    let fileList = [] // 切片名称数组
    let suffix  // 后缀
    let isExists // 是否存在

    // 判断是否存在切片目录
    isExists = await exists(path);
    // 不存在目录
    if (!isExists) {
      reject('根据HASH值查找目录,找不到路径!');
      return;
    }
    // 读取目录
    fileList = fs.readdirSync(path);
    // 目录是否为空
    if (fileList.length < count) {
      reject('切片还没有上传!');
      return;
    }
    // 切片排序并遍历
    fileList.sort((a, b) => {
      let reg = /_(\d+)/;
      return reg.exec(a)[1] - reg.exec(b)[1];
    }).forEach(item => {
      !suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;
      // 追加切片数据
      fs.appendFileSync(`${uploadDir}/${HASH}.${suffix}`, fs.readFileSync(`${path}/${item}`));
      // 删除切片
      fs.unlinkSync(`${path}/${item}`);
    });
    // 删除切片目录
    fs.rmdirSync(path);
    resolve({
      path: `${uploadDir}/${HASH}.${suffix}`,
      filename: `${HASH}.${suffix}`
    });
  });
};

// 切片存储
app.post('/upload_chunk', async (req, res) => {
  try {
    let { fields, files } = await multiparty_upload(req);
    // 文件对象
    let file = (files.file && files.file[0]) || {}
    // 文件名称
    let filename = (fields.filename && fields.filename[0]) || ""
    let path = ''
    // 创建存放切片的临时目录(HASH命名)
    let [, HASH] = /^([^_]+)_(\d+)/.exec(filename);
    // 目录路径
    path = `${uploadDir}/${HASH}`;
    // 判断目录是否存在,不存在就创建此目录
    !fs.existsSync(path) ? fs.mkdirSync(path) : null;
    // 把切片存储到临时目录中
    path = `${uploadDir}/${HASH}/${filename}`;
    // 不存在就写入
    writeFile_formdata(res, path, file, filename);
  } catch (err) {
    res.send({
      code: 1,
      codeText: err
    });
  }
  
});

// 当所有切片都上传成功,我们合并切片
app.post('/upload_merge', async (req, res) => {
  let {HASH,count} = req.body;
  try {
    let {filename,path} = await merge(HASH, count);
    res.send({
      code: 0,
      codeText: '合并成功',
      originalFilename: filename,
      servicePath: path.replace(__dirname, HOSTNAME)
    });
  } catch (err) {
    res.send({
      code: 1,
      codeText: err
    });
  }
});



  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值