记录--经常被cue大文件上传,忍不住试一下

这篇具有很好参考价值的文章主要介绍了记录--经常被cue大文件上传,忍不住试一下。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

记录--经常被cue大文件上传,忍不住试一下

大文件上传主要步骤:

  1. 获取文件对象,切分文件
  2. 根据文件切片,计算文件唯一hash值
  3. 上传文件切片,服务端保存起来
  4. 合并文件切片,前端发送合并请求,服务端将文件切片合并为原始文件
  5. 秒传,对于已经存在的分片,可以前端发个请求获取已经上传的文件切片信息,前端判断已经上传的切片不再发送切片上传请求;或者后端验证已经存在的切片,直接返回成功结果,后端不再重复写入保存
  6. 暂停上传,使用axios的取消请求
  7. 继续上传,跟秒传逻辑一样,先发个请求验证,已经上传的切片不再重复发请求,将没有上传的切片继续上传

技术栈:

包管理工具:

  • pnpm

前端:

  • vue 3.3.11
  • vite
  • axios
  • spark-md5:根据文件内容生成唯一hash值

后端:

  • node
  • koa
  • @koa/router
  • koa-body 解析请求体,包括json、form-data等
  • @koa/cors:解决跨域

1、文件分片

先用vite搭一个vue3项目 pnpm create vite

首先拿到上传的文件,通过 <input type="file"/>change事件拿到File文件对象,File继承自Blob,可以调用Blob的实例方法,然后用slice方法做分割;

这篇文章介绍了 JS中的二进制对象:Blob、File、ArrayBuffer,及转换处理:FileReader、URL.createObjectURL

// App.vue
<script setup>
import { ref } from "vue";
import { createChunks } from "./utils";

// 保存切片
const fileChunks = ref([]);

function handleFileChange(e) {
  // 获取文件对象
  const file = e.target.files[0];
  if (!file) {
    return;
  }
  fileChunks.value = createChunks(file);
  console.log(fileChunks.value);
}
</script>

<template>
  <input type="file" @change="handleFileChange" />
  <button>上传</button>
</template>

// utils.js

// 默认每个切片3MB
const CHUNK_SIZE = 3 * 1024 * 1024;

export function createChunks(file, size = CHUNK_SIZE) {
  const chunks = [];
  for (let i = 0; i < file.size; i += size) {
    chunks.push(file.slice(i, i + size));
  }
  return chunks;
}

记录--经常被cue大文件上传,忍不住试一下

2、计算hash

上传文件给服务器,要区分一下不同文件,对于服务端已经存在的文件切片,前端不需要重复上传,服务器不需要重复处理,节约性能。要做到区分不同文件,就需要给每个文件一个唯一标识,用 spark-md5 这个库来根据文件内容生成唯一hash值,安装 pnpm add spark-md5

// App.vue
<script setup>
import { ref } from "vue";
import { createChunks, calculateFileHash } from "./utils";

const fileChunks = ref([]);

async function handleFileChange(e) {
  const file = e.target.files[0];
  if (!file) {
    return;
  }
  fileChunks.value = createChunks(file);
  const sT = Date.now();
  const hash = await calculateFileHash(fileChunks.value);
  console.log(Date.now() - sT); //测试一下计算hash耗时
}

</script>
// utils.js
import SparkMD5 from "spark-md5";

export function calculateFileHash(chunkList) {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    // FileReader读取文件内容
    const reader = new FileReader();
    reader.readAsArrayBuffer(new Blob(chunkList));
    // 读取成功回调
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
}

上面calculateFileHash这个函数计算hash使用文件所有切片内容,如果文件很大,将会非常耗时,测试了一个526MB的文件,需要6813ms左右,为了保证所有切片都参与计算,也不至于太耗时,采取下面这种方式:

  • 第一个和最后一个切片全部计算
  • 其他切片取前、中、后两个字节参与计算

这种方式可能会损失一点准确性,如果计算出来的hash变了,就重新上传呗

// utils.js
const CHUNK_SIZE = 3 * 1024 * 1024;

export function calculateFileHash(chunkList) {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const reader = new FileReader();
    // 抽取chunk
    const chunks = [];
    for (let i = 0; i < chunkList.length; i++) {
      const chunk = chunkList[i];
      if (i === 0 || i === chunkList.length - 1) {
        chunks.push(chunk);
      } else {
        chunks.push(chunk.slice(0, 2));
        chunks.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2));
        chunks.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE));
      }
    }
    reader.readAsArrayBuffer(new Blob(chunks));
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
}

再次传同一个文件测试,只需要975ms左右

3、上传切片

前端逻辑:

这里要考虑一个问题,如果一个大文件切成了几十上百个切片,这时如果同时发送,浏览器负担很重,浏览器默认允许同时建立 6 个 TCP 持久连接,也就是说同一个域名同时能支持6个http请求,多余的会排队。这里就需要控制一下并发请求数量,设置为同时发送6个

// App.vue
<script setup>
import {
  createChunks,
  calculateFileHash,
  createFormData,
  concurrentChunksUpload,
} from "./utils";

async function uploadChunks() {
  const hash = await calculateFileHash(fileChunks.value);
  // 利用计算的文件hash构造formData
  const dataList = createFormData(fileChunks.value, hash);
  // 切片上传请求
  await concurrentChunksUpload(dataList);
}
</script

<template>
  <input type="file" @change="handleFileChange" />
  <button @click="uploadChunks()">上传</button>
</template>
// utils.js

const CHUNK_SIZE = 10 * 1024 * 1024;
const BASE_URL = "http://localhost:2024";

// 根据切片的数量组装相同数量的formData
export function createFormData(fileChunks, hash) {
  return fileChunks
    .map((chunk, index) => ({
      fileHash: hash,
      chunkHash: `${hash}-${index}`,
      chunk,
    }))
    .map(({ fileHash, chunkHash, chunk }) => {
      const formData = new FormData();
      formData.append("fileHash", fileHash);
      formData.append("chunkHash", chunkHash);
      formData.append(`chunk-${chunkHash}`, chunk);
      return formData;
    });
}

// 默认最大同时发送6个请求
export function concurrentChunksUpload(dataList, max = 6) {
  return new Promise((resolve) => {
    if (dataList.length === 0) {
      resolve([]);
      return;
    }
    const dataLength = dataList.length;
    // 保存所有成功结果
    const results = [];
    // 下一个请求
    let next = 0;
    // 请求完成数量
    let finished = 0;

    async function _request() {
      // next达到dataList个数,就停止
      if (next === dataLength) {
        return;
      }
      const i = next;
      next++;

      const formData = dataList[i];
      const url = `${BASE_URL}/upload-chunks`;
      try {
        const res = await axios.post(url, formData);
        results[i] = res.data;
        finished++;
        // 所有切片上传成功返回
        if (finished === dataLength) {
          resolve(results);
        }
        _request();
      } catch (err) {
        console.log(err);
      }
    }
    // 最大并发数如果大于formData个数,取最小数
    const minTimes = Math.min(max, dataLength);
    for (let i = 0; i < minTimes; i++) {
      _request();
    }
  });
}

后端逻辑:

浏览器跨域问题及几种常见解决方案:CORS,JSONP,Node代理,Nginx反向代理 ,分析如何解决浏览器跨域

const path = require("path");
const fs = require("fs");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const cors = require("@koa/cors");
const { koaBody } = require("koa-body");

const app = new Koa();
const router = new KoaRouter();
// 保存切片目录
const chunksDir = path.resolve(__dirname, "../chunks");

//cors解决跨域
app.use(cors()); 
app.use(router.routes()).use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动"));

// 中间件:处理multipart/form-data,切片写入磁盘
const uploadKoaBody = koaBody({
  multipart: true,
  formidable: {
    // 设置保存切片的文件夹
    uploadDir: chunksDir,
    // 在保存到磁盘前回调
    onFileBegin(name, file) {
      if (!fs.existsSync(chunksDir)) {
        fs.mkdirSync(chunksDir);
      }
      // 切片重命名
      file.filepath = `${chunksDir}/${name}`;
    },
  },
});

// 上传chunks切片接口
router.post("/upload-chunks", uploadKoaBody, (ctx) => {
  ctx.body = { code: 200, msg: "文件上传成功" };
});

4、合并切片

前端逻辑:

当所有切片上传成功,发送合并请求

// App.vue
<script setup>
import {
  createChunks,
  calculateFileHash,
  createFormData,
  concurrentChunksUpload,
  mergeChunks
} from "./utils";

async function uploadChunks() {
  const hash = await calculateFileHash(fileChunks.value);
  // 利用计算的文件hash构造formData
  const dataList = createFormData(fileChunks.value, hash);
  // 切片上传请求
  await concurrentChunksUpload(dataList);
  // 等所有chunks发送完毕,发送合并请求
  mergeChunks(originFile.value.name);
}
</script

<template>
  <input type="file" @change="handleFileChange" />
  <button @click="uploadChunks()">上传</button>
</template>

// utils.js

export function mergeChunks(filename) {
  return axios.post(BASE_URL + "/merge-chunks", { filename, size: CHUNK_SIZE });
}

后端逻辑:

  • fs.readdirSync(path[, options]):同步读取给定目录的内容,返回一个数组,其中包含目录中的所有文件名或对象
  • fs.existsSync(path):判断路径是否存在
  • fs.mkdirSync(path[, options]):同步地创建目录
  • fs.createWriteStream(path[, options]):创建文件可写流
  • fs.createReadStream(path[, options]):创建文件可读流
// 合并chunks接口
router.post("/merge-chunks", koaBody(), async (ctx) => {
  const { filename, size } = ctx.request.body;
  await mergeChunks(filename, size);
  ctx.body = { code: 200, msg: "合并成功" };
});

// 合并 chunks
async function mergeChunks(filename, size) {
  // 读取chunks目录中的文件名
  const chunksName = fs.readdirSync(chunksDir);
  if (!chunksName.length) return;
  // 保证切片合并顺序
  chunksName.sort((a, b) => a.split("-")[2] - b.split("-")[2]);
  // 提前创建要写入的static目录
  const fileDir = path.resolve(__dirname, "../static");
  if (!fs.existsSync(fileDir)) {
    fs.mkdirSync(fileDir);
  }
  // 最后写入的文件路径
  const filePath = path.resolve(fileDir, filename);
  const pipeStreams = chunksName.map((chunkName, index) => {
    const chunkPath = path.resolve(chunksDir, chunkName);
    // 创建写入流
    const writeStream = fs.createWriteStream(filePath, { start: index * size });
    return createPipeStream(chunkPath, writeStream);
  });
  await Promise.all(pipeStreams);
  // 全部写完,删除chunks切片目录
  fs.rmdirSync(chunksDir);
}

// 创建管道流写入
function createPipeStream(chunkPath, writeStream) {
  return new Promise((resolve) => {
    const readStream = fs.createReadStream(chunkPath);
    readStream.pipe(writeStream);
    readStream.on("end", () => {
      // 写完一个chunk,就删除
      fs.unlinkSync(chunkPath);
      resolve();
    });
  });
}

5、秒传文件

对于已经上传的文件,服务端这边可以判断,直接返回成功结果,不做重复保存的处理,节省时间;也可以前端先发一个请求获取已经上传的文件切片,就不再重复发送切片上传请求

服务端逻辑加一个中间件做判断:

// 中间件,已经存在的切片,直接返回成功结果
async function verifyChunks(ctx, next) {
  // 前端把切片hash放到请求路径上带过来
  const chunkName = ctx.request.querystring.split("=")[1];
  const chunkPath = path.resolve(chunksDir, chunkName);
  if (fs.existsSync(chunkPath)) {
    ctx.body = { code: 200, msg: "文件已上传" };
  } else {
    await next();
  }
}

// 上传chunks切片接口
router.post("/upload-chunks", verifyChunks, uploadKoaBody, (ctx) => {
  ctx.body = { code: 200, msg: "文件上传成功" };
});
前端这边修改一下请求路径,带个参数过去
export function concurrentChunksUpload(dataList, max = 6) {
  return new Promise((resolve) => {
      //...
      const formData = dataList[i];
      const chunkName = `chunk-${formData.get("chunkHash")}`;
      const url = `${BASE_URL}/upload-chunks?chunkName=${chunkName}`;
     //...
  });
}

6、暂停上传

前端逻辑

axios中可以使用同一个 cancel token 取消多个请求

<script setup>
import axios from "axios";

const CancelToken = axios.CancelToken;
let axiosSource = CancelToken.source();

function pauseUpload() {
  axiosSource.cancel?.();
}

async function uploadChunks(existentChunks = []) {
  const hash = await calculateFileHash(fileChunks.value);
  const dataList = createFormData(fileChunks.value, hash, existentChunks);
  await concurrentChunksUpload(axiosSource.token, dataList);
  // 等所有chunks发送完毕,发送合并请求
  mergeChunks(originFile.value.name);
}
</script>

<template>
  <input type="file" @change="handleFileChange" />
  <button @click="uploadChunks()">上传</button>
  <button @click="pauseUpload">暂停</button>
</template>

 

// utils.js

export function concurrentChunksUpload(sourceToken, dataList, max = 6) {
  return new Promise((resolve) => {
        //...
        const res = await axios.post(url, formData, {
          cancelToken: sourceToken,
        });
       //...
  });
}

7、继续上传

前端逻辑

要调用CancelToken.source()重新生成一个suource,发请求获取已经上传的chunks,过滤一下,不再重复发送,前面的秒传是在服务端判断的,也可以按这个逻辑来,已经上传的不重复发请求

<script setup>
import { getExistentChunks } from "./utils";

 async function continueUpload() {
  const { data } = await getExistentChunks();
  uploadChunks(data);
}

// existentChunks 默认空数组
async function uploadChunks(existentChunks = []) {
  const hash = await calculateFileHash(fileChunks.value);
  // existentChunks传入过滤已经上传的切片
  const dataList = createFormData(fileChunks.value, hash, existentChunks);
  // 重新生成source
  axiosSource = CancelToken.source();
  await concurrentChunksUpload(axiosSource.token, dataList);
  // 等所有chunks发送完毕,发送合并请求
  mergeChunks(originFile.value.name);
}

</script>

<template>
  <input type="file" @change="handleFileChange" />
  <button @click="uploadChunks()">上传</button>
  <button @click="pauseUpload">暂停</button>
  <button @click="continueUpload">继续</button>
</template>

 

// utils.js
export function createFormData(fileChunks, hash, existentChunks) {
  const existentChunksName = existentChunks
    // 如果切片有损坏,切片大小可能就不等于CHUNK_SIZE,重新传
    // 最后一张切片大小大概率是不等的
    .filter((item) => item.size === CHUNK_SIZE)
    .map((item) => item.filename);

  return fileChunks
    .map((chunk, index) => ({
      fileHash: hash,
      chunkHash: `${hash}-${index}`,
      chunk,
    }))
    .filter(({ chunkHash }) => {
      // 同时过滤掉已经上传的切片
      return !existentChunksName.includes(`chunk-${chunkHash}`);
    })
    .map(({ fileHash, chunkHash, chunk }) => {
      const formData = new FormData();
      formData.append("fileHash", fileHash);
      formData.append("chunkHash", chunkHash);
      formData.append(`chunk-${chunkHash}`, chunk);
      return formData;
    });
}


export function getExistentChunks() {
  return axios.post(BASE_URL + "/existent-chunks");
}

后端逻辑

// 获取已经上传的切片接口
router.post("/existent-chunks", (ctx) => {
  if (!fs.existsSync(chunksDir)) {
    ctx.body = [];
    return;
  }
  ctx.body = fs.readdirSync(chunksDir).map((filename) => {
    return {
      // 切片名:chunk-tue234wdhfjksd211tyf3234-1
      filename,
      // 切片大小
      size: fs.statSync(`${chunksDir}/${filename}`).size,
    };
  });
});

最后

  • 多次尝试一个23MB的pdf和一个536MB的mp4,重复几次点暂停和继续,最后都可以打开
  • 如有问题,请不吝指教,学习一下

本文转载于:

https://juejin.cn/post/7317704519160528923

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录--经常被cue大文件上传,忍不住试一下文章来源地址https://www.toymoban.com/news/detail-762500.html

到了这里,关于记录--经常被cue大文件上传,忍不住试一下的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 记录一下,win11,单击zip文件后文件管理器闪退

    新买的笔记本电脑,正版win11家庭版,压缩软件安装的是winrar,安装了其他一堆软件后,发现文件管理器经常莫名闪退。多次尝试之后,发现是只要是单击zip文件后就会文件管理器闪退,然后恢复桌面。 1. 百度了“win11 zip 闪退”,出来的解决方案,试了,无果; 2. 必应了同

    2024年01月23日
    浏览(15)
  • 记录一下,C#运行nodejs调用js文件提示报错:Error: node:internal/modules/cjs/loader:1080

    个人记录一下,C#运行nodejs调用js文件提示报错: 报错提示信息: Error: node:internal/modules/cjs/loader:1080 throw err;   ^  Error: Cannot find module \\\'F:鎴戠殑....................” .....................下面还有很多报错内容 还有英文提示模块未找到的提示。 我另一个文件运行没报错,运行正常有

    2024年02月11日
    浏览(16)
  • 【学习记录21】Vue+ElementUI el-upload多文件上传,一次请求上传多个文件!

    【学习记录21】Vue+ElementUI el-upload多文件上传,一次请求上传多个文件!

    前情回顾说点废话。。。 1、项目当中遇到需要上传多个图片,一次选取多个图片。但是吧el-upload默认只能一个一个传,每次上传成功还的自己去push,一个一个去判断。 2、关键是后台给的接口,要一次性接收一堆,无奈之下只能去网上搜索,大佬们都是给的代码片段无法直

    2024年02月12日
    浏览(50)
  • 【Web】CTFSHOW 文件上传刷题记录(全)

    【Web】CTFSHOW 文件上传刷题记录(全)

    期末考完终于可以好好学ctf了,先把这些该回顾的回顾完,直接rushjava! 目录 web151 web152 web153 web154-155 web156-159 web160 web161 web162-163 web164 web165 web166 web167 web168 web169-170  如果直接上传php文件就会弹窗 直接禁js按钮就不能上传文件了  一种方法是改js代码(png=php) 然后直接上传即可

    2024年01月17日
    浏览(24)
  • uniapp微信小程序 选择聊天记录文件上传

    uniapp微信小程序 选择聊天记录文件上传

    目录 精简版总结 示例 容易踩的坑 1、页面刷新问题 2、extension问题 单文件 多个文件 PS:files和filePath/name只能二选一组 此处用xlsx文件作实例,选择聊天记录中的xlsx文件上传到指定接口中。 因为某些微信版本extension可能不生效,或者又想要对提交的文件名做校验,建议参考我

    2024年02月09日
    浏览(211)
  • 【前端学习记录】vue中使用el-upload组件时,上传文件进度条没有实时更新

    问题背景 今天在项目中遇到一个问题,使用el-upload组件时,上传文件进度条没有实时更新,需要手动点击一下才会更新。 原理及可尝试方案 el-upload 组件默认的进度条是通过 Ajax 请求上传文件,并且进度条通过监听 xhr.upload 的 progress 事件来实时更新。但是,有些浏览器在处

    2024年02月01日
    浏览(15)
  • ruoyi-nbcio-plus基于vue3的flowable为了适配文件上传改造VForm3的代码记录

    ruoyi-nbcio-plus基于vue3的flowable为了适配文件上传改造VForm3的代码记录

    更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio 演示地址:RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统  gitee源代码地址 后端代码: https://gitee.com/nbacheng/nbcio-boot 前端代码:https://gitee.com/nbacheng/nbcio-vu

    2024年04月28日
    浏览(9)
  • Qt:记录一下好看的配色

    Qt:记录一下好看的配色

    2024年02月13日
    浏览(11)
  • 使用了百度OCR,记录一下

    由于识别ocr有的频率不高,图片无保密性需求,也不想太大的库, 就决定还是用下api算了,试用了几家,决定用百度的ocr包,相对简单。 遇到的问题里面下列基本有提到:例如获取ID,KEY;例如安装库; 参考帖子:python+百度OCR的使用方法(踩坑+测试程序)_no module named \\\'ai

    2024年02月06日
    浏览(16)
  • XSS攻击是怎么回事?记录一下

    XSS攻击是怎么回事?记录一下

    title: XSS攻击 date: 2023-08-27 19:15:57 tags: [XSS, 网络安全] categories: 网络安全 今天学习了一个网络攻击的手段,XSS攻击技术,大家自建网站的朋友,记得看看是否有此漏洞。 🎈 XSS 攻击 全称跨站脚本攻击 Cross Site Scripting 。 为了与重叠样式表 CSS 进行区分,所以换了另一个缩写名称

    2024年02月11日
    浏览(12)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包