Upload large files in pieces and resume uploads from breakpoints (with front-end and back-end demos)

Order

Uploading large files, resuming uploads at breakpoints, and uploading in seconds, as technical points of high-frequency investigation, most people know about it but don’t know why. Let’s explore it from the perspective of front-end and back-end together.
I believe that as long as you spend a little time to understand it seriously, you will find that you have spent some time. . .

Thought diagram

Front-end core code fragment

Slice files according to the specified size
 createChunk(file, size = 512 * 1024) {<!-- -->
      const chunkList = [];
      let cur = 0;
      while (cur < file. size) {<!-- -->
        // Use the slice method to slice
        chunkList. push({<!-- --> file: file. slice(cur, cur + size) });
        cur += size;
      }
      return chunkList;
    }
The file uses js-md5 to hash
 handleFileChange(e) {<!-- -->
      let fileReader = new FileReader();
      fileReader.readAsDataURL(e.target.files[0]);
      fileReader.onload = function (e2) {<!-- -->
          const hexHash = md5(e2.target.result) + '.' + that.fileObj.file.name.split('.').pop();
      };
    }

After the

slice is converted into a form object, it is configured as a request list.
const requestList = noUploadChunks.map(({<!-- --> file, fileName, index, chunkName }) => {<!-- -->
          const formData = new FormData();
          formData.append("file", file);
          formData.append("fileName", fileName);
          formData.append("chunkName", chunkName);
          return {<!-- --> formData, index };
        })
        .map(({<!-- --> formData, index }) =>
          axiosRequest({<!-- -->
            url: "http://localhost:3000/upload",
            data: formData
          })
        )

Full front-end code

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <el-button @click="uploadChunks"> Upload </el-button>
    <!-- <el-button @click="pauseUpload"> Pause </el-button> -->
    <div style="width: 300px">
          total progress:
          <el-progress :percentage="tempPercent"></el-progress>
          Slicing progress:
          <div v-for="(item,index) in fileObj.chunkList" :key="index">
            <span>{<!-- -->{<!-- --> item.chunkName }}:</span>
            <el-progress :percentage="item.percent"></el-progress>
          </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";
import md5 from 'js-md5'
 const CancelToken = axios. CancelToken;
 let source = CancelToken. source();

function axiosRequest({<!-- -->
  url,
  method = "post",
  data,
  headers = {<!-- -->},
  onUploadProgress = (e) => e, // progress callback
}) {<!-- -->
  return new Promise((resolve, reject) => {<!-- -->
    axios[method](url, data, {<!-- -->
      headers,
      onUploadProgress, // incoming monitoring progress callback
      cancelToken: source.token
    })
      .then((res) => {<!-- -->
        resolve(res);
      })
      .catch((err) => {<!-- -->
        reject(err);
      });
  });
}
export default {<!-- -->
  data() {<!-- -->
    return {<!-- -->
      fileObj: {<!-- -->
        file: null,
        chunkList:[]
      },
      tempPercent: 0
    };
  },
  methods: {<!-- -->
    handleFileChange(e) {<!-- -->
      const [file] = e. target. files;
      if (!file) return;
      this.fileObj.file = file;
      const fileObj = this. fileObj;
      if (!fileObj.file) return;
      const chunkList = this.createChunk(fileObj.file);
      console.log(chunkList); // see what chunkList looks like
      let that = this
      // Get the hash value of this video as the name
      let fileReader = new FileReader();
      fileReader.readAsDataURL(e.target.files[0]);
      fileReader.onload = function (e2) {<!-- -->
          const hexHash = md5(e2.target.result) + '.' + that.fileObj.file.name.split('.').pop();
          that.fileObj.name = hexHash
          that.fileObj.chunkList = chunkList.map(({<!-- --> file }, index) => ({<!-- -->
          file,
          size: file. size,
          percent: 0,
          chunkName: `${<!-- -->hexHash}-${<!-- -->index}`,
          fileName: hexHash,
          index,
        }))
      };
    },
    createChunk(file, size = 512 * 1024) {<!-- -->
      const chunkList = [];
      let cur = 0;
      while (cur < file. size) {<!-- -->
        // Use the slice method to slice
        chunkList. push({<!-- --> file: file. slice(cur, cur + size) });
        cur += size;
      }
      return chunkList;
    },
    async uploadChunks() {<!-- -->
      const {<!-- -->uploadedList,shouldUpload} = await this.verifyUpload()
      // if the file exists
      if(!shouldUpload){<!-- -->
        console.log('Second transmission succeeded')
        this.fileObj.chunkList.forEach(item => {<!-- -->
          item.percent = 100
        });
        return
      }
      let noUploadChunks = []
      if(uploadedList & amp; & amp; uploadedList.length>0 & amp; & amp;uploadedList.length !== this.fileObj.chunkList.length){<!-- -->
        // If slice exists. Only upload slices without
        noUploadChunks = this.fileObj.chunkList.filter(item=>{<!-- -->
          if(uploadedList.includes(item.chunkName)){<!-- -->
            item.percent = 100
          }
          return !uploadedList.includes(item.chunkName)
        })
      }else{<!-- -->
        noUploadChunks = this.fileObj.chunkList
      }
      const requestList = noUploadChunks.map(({<!-- --> file, fileName, index, chunkName }) => {<!-- -->
          const formData = new FormData();
          formData.append("file", file);
          formData.append("fileName", fileName);
          formData.append("chunkName", chunkName);
          return {<!-- --> formData, index };
        })
        .map(({<!-- --> formData, index }) =>
          axiosRequest({<!-- -->
            url: "http://localhost:3000/upload",
            data: formData,
            onUploadProgress: this.createProgressHandler(
              this.fileObj.chunkList[index]
            ), // incoming monitoring upload progress callback
          })
        );
      await Promise.all(requestList); // use Promise.all to request
      this. mergeChunks()
    },
    createProgressHandler(item) {<!-- -->
      return (e) => {<!-- -->
        // Set the progress percentage of each slice
        item.percent = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    mergeChunks(size = 512 * 1024) {<!-- -->
      axiosRequest({<!-- -->
         url: "http://localhost:3000/merge",
         headers: {<!-- -->
           "content-type": "application/json",
         },
         data: JSON.stringify({<!-- -->
          size,
           fileName: this.fileObj.name
         }),
       });
     },
     pauseUpload() {<!-- -->
      source.cancel("Interrupt upload!");
       source = CancelToken.source(); // Reset the source to ensure that the transfer can continue
     },
     async verifyUpload () {<!-- -->
       const {<!-- --> data } = await axiosRequest({<!-- -->
         url: "http://localhost:3000/verify",
         headers: {<!-- -->
           "content-type": "application/json",
         },
         data: JSON.stringify({<!-- -->
           fileName: this.fileObj.name
        }),
      });
      return data
     }
  },
  computed: {<!-- -->
    totalPercent() {<!-- -->
      const fileObj = this. fileObj;
      if (fileObj. chunkList. length === 0) return 0;
      const loaded = fileObj. chunkList
        .map(({<!-- --> size, percent }) => size * percent)
       .reduce((pre, next) => pre + next);
      return parseInt((loaded / fileObj. file. size). toFixed(2));
    },
  },
  watch: {<!-- -->
      totalPercent (newVal) {<!-- -->
           if (newVal > this.tempPercent) this.tempPercent = newVal
       }
   },
};
</script>

<style lang="scss" scoped></style>

Server receiving code (simulated with node.js)

const http = require("http");
const path = require("path");
const fse = require("fs-extra");
const multiparty = require("multiparty");

const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", `qiepian`); // slice storage directory

server.on("request", async (req, res) => {<!-- -->
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Headers", "*");
  res.setHeader('Content-Type','text/html; charset=utf-8');
  if (req. method === "OPTIONS") {<!-- -->
    res.status = 200;
    res.end();
    return;
  }
  console.log(req.url);

  if (req.url === "/upload") {<!-- -->
    const multipart = new multiparty. Form();

    multipart. parse(req, async (err, fields, files) => {<!-- -->
      if (err) {<!-- -->
        console.log("errrrr", err);
        return;
      }
      const [file] = files. file;
      const [fileName] = fields. fileName;
      const [chunkName] = fields. chunkName;
      // Path to the folder where slices are saved, such as Zhang Yuan-Guest.flac-chunks
      const chunkDir = path.resolve(UPLOAD_DIR, `${<!-- -->fileName}-chunks`);
      // // The slice directory does not exist, create a slice directory
      if (!fse.existsSync(chunkDir)) {<!-- -->
        await fse.mkdirs(chunkDir);
      }
      // move the slice to the slice folder
      await fse.move(file.path, `${<!-- -->chunkDir}/${<!-- -->chunkName}`);
      res.end(
        JSON.stringify({<!-- -->
          code: 0,
          message: "Slice uploaded successfully",
        })
      );
    });
  }

  // Merge slices
  // Receive request parameters
  const resolvePost = (req) =>
    new Promise((res) => {<!-- -->
      let chunk = "";
      req.on("data", (data) => {<!-- -->
        chunk + = data;
      });
      req.on("end", () => {<!-- -->
        res(JSON. parse(chunk));
      });
    });
  const pipeStream = (path, writeStream) => {<!-- -->
    console.log("path", path);
    return new Promise((resolve) => {<!-- -->
      const readStream = fse. createReadStream(path);
      readStream.on("end", () => {<!-- -->
        // fse.unlinkSync(path); // delete slice file
        resolve();
      });
      readStream.pipe(writeStream);
    });
  };

  // Merge slices
  const mergeFileChunk = async (filePath, fileName, size) => {<!-- -->
    // filePath: Where do you merge the slices, the path
    const chunkDir = path.resolve(UPLOAD_DIR, `${<!-- -->fileName}-chunks`);
    let chunkPaths = null;
    // Get all the slices in the slice folder and return an array
    chunkPaths = await fse. readdir(chunkDir);
    // Sort according to the subscript of the slice
    // Otherwise, the order of directly reading the directory may be confused
    chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    const arr = chunkPaths. map((chunkPath, index) => {<!-- -->
      return pipeStream(
        path.resolve(chunkDir, chunkPath),
        // Create a writable stream at the specified location
        fse.createWriteStream(filePath, {<!-- -->
          start: index * size,
          end: (index + 1) * size,
        })
      );
    });
    await Promise. all(arr);
  };
  if (req.url === "/merge") {<!-- -->
    const data = await resolvePost(req);
    const {<!-- --> fileName, size } = data;
    const filePath = path.resolve(UPLOAD_DIR, fileName);
    await mergeFileChunk(filePath, fileName, size);
    res.end(
      JSON.stringify({<!-- -->
        code: 0,
        message: "File merged successfully",
      })
    );
  }
  if (req.url === "/verify") {<!-- -->
      // Return the list of uploaded slice names
      const createUploadedList = async fileName =>
              fse.existsSync(path.resolve(UPLOAD_DIR, fileName))
                  ? await fse.readdir(path.resolve(UPLOAD_DIR, fileName))
                  : [];
      const data = await resolvePost(req);
      const {<!-- --> fileName } = data;
      const filePath = path.resolve(UPLOAD_DIR, fileName);
      console. log(filePath)
      if (fse. existsSync(filePath)) {<!-- -->
          res.end(
              JSON.stringify({<!-- -->
                  shouldUpload: false
              })
          );
      } else {<!-- -->
          res.end(
              JSON.stringify({<!-- -->
                  shouldUpload: true,
                      uploadedList: await createUploadedList(`${<!-- -->fileName}-chunks`)
              })
          );
      }
  }
});

server.listen(3000, () => console.log("Listening on port 3000"));

demo test

  • npm installs the missing dependencies,
  • Run the front-end code.
  • node xxx.js runs the backend code.