• Html + Express 实现大文件分片上传、断点续传、秒传


    在日常的网页开发中,文件上传是一项常见操作。通过文件上传技术,用户可以将本地文件方便地传输到Web服务器上。这种功能在许多场景下都是必不可少的,比如上传文件到网盘或上传用户头像等。

    然而,当需要上传大型文件时,可能会遇到以下问题:

    1. 长时间上传:由于文件大小较大,上传过程可能会耗费较长时间。

    2. 上传中断重新上传:如果在上传过程中出现意外情况导致上传中断,用户需要重新开始整个上传过程,这会增加用户的不便。

    3. 服务端限制:通常,服务端会对上传的文件大小进行限制,这可能导致无法上传大型文件。

    为了解决这些问题,可以采用分片上传的方式:

    分片上传即将大文件分割成小块,然后分块上传到服务器。通过分片上传,可以实现以下优势:

    快速上传:由于每个小块的大小相对较小,上传时间大大缩短。

    断点续传:如果上传过程中出现中断,只需重新上传中断的部分,而不需要重新上传整个文件,提高了用户体验。

    避免大小限制:分片上传可以避免由于文件大小限制而无法上传大文件的问题。

    通过采用分片上传技术,可以提升用户体验,加快大文件上传速度,并确保上传过程的稳定性和可靠性。

    原理:


    分片上传的概念类似于将一个大文件分割成多个小块,然后分别上传这些小块到服务器上。
    首先,将待上传的大文件划分为固定大小的小块,比如每块大小为1MB。然后逐个上传这些小块到服务器。在上传过程中,可以同时处理多个小块的上传,也可以按顺序逐一上传小块。每个小块上传完成后,服务器会妥善保存这些小块,并记录它们的顺序和位置信息。
    当所有小块都上传完成后,服务器会按照预先记录的顺序和位置信息,将这些小块组合成完整的大文件。最终,整个大文件就成功地被分片上传并合并完成了。这种分片上传的方式能够有效地提升大文件上传的效率和稳定性,确保文件上传过程更加可靠和高效。

    前端代码

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    6. <title>Documenttitle>
    7. <script src="https://code.jquery.com/jquery-3.6.0.min.js">script>
    8. <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js">script>
    9. <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js">script>
    10. head>
    11. <body>
    12. <input type="file" />
    13. <script>
    14. const CHUNK_SIZE = 1024 * 1024
    15. let hashName = ''
    16. let fileName = ''
    17. $('input').change(async (e) => {
    18. const file = e.target.files[0]
    19. const chunks = shardingChunks(file) // 分片
    20. fileName = file.name
    21. hashName = await shardingHash(file) // 获取文件hash值
    22. const { data: { existFile, existChunks } } = await axios.post('http://localhost:3000/uploader/verify', { fileHash: hashName, fileName });
    23. if (existFile) return; // 如果该hash值 && file.name 存在说明该文件已经在服务器上了
    24. uploader(chunks, existChunks)
    25. })
    26. // 分片
    27. const shardingChunks = (file) => {
    28. let start = 0
    29. const chunks = []
    30. while (start < file.size) {
    31. chunks.push(file.slice(start, start + CHUNK_SIZE))
    32. start += CHUNK_SIZE
    33. }
    34. return chunks
    35. }
    36. // 获取文件hash值
    37. const shardingHash = (file) => {
    38. return new Promise((resolve) => {
    39. const fileReader = new FileReader()
    40. fileReader.readAsArrayBuffer(file)
    41. fileReader.onload = (e) => {
    42. const spark = new SparkMD5.ArrayBuffer()
    43. spark.append(e.target.result)
    44. resolve(spark.end())
    45. }
    46. })
    47. }
    48. // 分片上传
    49. const uploader = async (chunks, existChunks) => {
    50. const chunksArr = chunks.map((chunk, index) => ({
    51. fileHash: hashName,
    52. chunkHash: hashName + '-' + index,
    53. chunk
    54. }))
    55. const formDatas = chunksArr.map(item => {
    56. const formData = new FormData();
    57. formData.append("fileHash", item.fileHash);
    58. formData.append("chunkHash", item.chunkHash);
    59. formData.append("chunk", item.chunk);
    60. return formData;
    61. })
    62. let flagArr = []
    63. formDatas.forEach(async (item) => {
    64. const res = await axios.post('http://localhost:3000/uploader/upload', item, {
    65. headers: {
    66. 'Content-Type': 'multipart/form-data'
    67. }
    68. })
    69. flagArr.push(res.data.success)
    70. if (flagArr.length == formDatas.length && flagArr.every(item => item == true)) {
    71. mergeFile() // 合并文件
    72. flagArr = []
    73. }
    74. })
    75. }
    76. const mergeFile = async () => {
    77. const res = await axios.post('http://localhost:3000/uploader/merge',
    78. {
    79. fileHash: hashName,
    80. fileName: fileName
    81. })
    82. if (res.data.success) return alert('上传成功')
    83. }
    84. script>
    85. body>
    86. html>

    后端代码(Node)

    1. const express = require("express");
    2. const cors = require("cors");
    3. const bodyParser = require("body-parser");
    4. const fse = require("fs-extra");
    5. const path = require("path");
    6. const multipart = require("connect-multiparty");
    7. const multipartMiddleware = multipart();
    8. const app = express();
    9. app.use(cors());
    10. app.use(bodyParser.json());
    11. // 所有上传的文件存放在该目录下
    12. const UPLOADS_DIR = path.resolve("uploads");
    13. /**
    14. * 上传
    15. */
    16. app.post("/upload", multipartMiddleware, (req, res) => {
    17. const { fileHash, chunkHash } = req.body;
    18. // 如果临时文件夹(用于保存分片)不存在,则创建
    19. const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
    20. if (!fse.existsSync(chunkDir)) {
    21. fse.mkdirSync(chunkDir);
    22. }
    23. // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
    24. const chunkPath = path.resolve(chunkDir, chunkHash);
    25. if (!fse.existsSync(chunkPath)) {
    26. fse.moveSync(req.files.chunk.path, chunkPath);
    27. }
    28. res.send({
    29. success: true,
    30. msg: "上传成功",
    31. });
    32. });
    33. /**
    34. * 合并
    35. */
    36. app.post("/merge", async (req, res) => {
    37. const { fileHash, fileName } = req.body;
    38. // 最终合并的文件路径
    39. const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
    40. // 临时文件夹路径
    41. const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
    42. // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
    43. const chunkPaths = fse.readdirSync(chunkDir);
    44. // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
    45. chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    46. // 遍历文件(分片)数组,将分片追加到文件中
    47. const pool = chunkPaths.map(
    48. (chunkName) =>
    49. new Promise((resolve) => {
    50. const chunkPath = path.resolve(chunkDir, chunkName);
    51. // 将分片追加到文件中
    52. fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
    53. // 删除分片
    54. fse.unlinkSync(chunkPath);
    55. resolve();
    56. })
    57. );
    58. await Promise.all(pool);
    59. // 等待所有分片追加到文件后,删除临时文件夹
    60. fse.removeSync(chunkDir);
    61. res.send({
    62. success: true,
    63. msg: "合并成功",
    64. });
    65. });
    66. /**
    67. * 校验
    68. */
    69. app.post("/verify", (req, res) => {
    70. const { fileHash, fileName } = req.body;
    71. // 判断服务器上是否存在该hash值的文件
    72. const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
    73. const existFile = fse.existsSync(filePath);
    74. // 获取已经上传到服务器的文件分片
    75. const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
    76. const existChunks = [];
    77. if (fse.existsSync(chunkDir)) {
    78. existChunks.push(...fse.readdirSync(chunkDir));
    79. }
    80. res.send({
    81. success: true,
    82. msg: "校验文件",
    83. data: {
    84. existFile,
    85. existChunks,
    86. },
    87. });
    88. });
    89. const server = app.listen(3000, () => {
    90. console.log(`Example app listening on port ${server.address().port}`);
    91. });

    效果图

  • 相关阅读:
    【方向盘】认为:开发者已无理由再用Java EE
    Lecture 11 Deadlocks (死锁)
    怎样在应用中实现自助报表功能?
    Bootstrap的CSS类积累学习
    Opencv | 边缘检测 & 轮廓信息
    PyTorch多GPU训练时同步梯度是mean还是sum?
    Pointnet/Pointnet++点云数据集处理并训练
    js节流和防抖
    【第2章 Node.js基础】2.1 JavaScript基本语法
    JMeter 调试取样器(Debug Sampler)简介
  • 原文地址:https://blog.csdn.net/m0_71349739/article/details/138715142