background

When we do the file import function, if the imported file is too large, it may take a long time and need to upload again after failure. We need to combine the front and back ends to solve this problem

Train of thought

We need to do a few things:

  • Slicing a file, that is, splitting a request into multiple requests, reduces the time for each request, and if a request fails, you only need to re-send the request instead of starting from scratch
  • Notify the server to merge slices. After uploading slices, the front end notifies the server to merge slices
  • Controls the concurrency of multiple requests to prevent browser memory overflow and page congestion caused by multiple requests being sent at the same time
  • When multiple requests fail to be sent, such as network failure, page shutdown, etc., we have to process the failed requests and let them be sent repeatedly

implementation

The front end

Sample code repository

The warehouse address

Step 1- Slice and merge slices

In JavaScript, the FIle object is a subclass of the Blob object, which contains an important method, slice, through which we can split binary files as follows:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = s, initial - scale = 1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">upload</button>
</body>
<script>
// Request the base address
axios.defaults.baseURL = 'http://localhost:3000'
// The selected file
var file = null
// Select the file
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0]}// Start uploading
document.getElementById('uploadBtn').onclick = async function(){
    if(! file)return
    // Create a slice
    // let size = 1024 * 1024 * 10
    let size = 1024 * 50  //50KB slice size
    let fileChunks = []
    let index = 0 // Section number
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // Upload slices
    const uploadList = fileChunks.map((item, index) = > {
        let formData = new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        return axios({
            method: 'post'.url: '/upload'.data: formData
        })
    })
    await Promise.all(uploadList)
    // merge slices
    await axios({
        method: 'get'.url: '/merge'.params: {
            filename: file.name
        }
    });
    console.log('Upload completed')}</script>
</html>
Copy the code

Step 2- Concurrency control

Combined with promise.race and asynchronous function implementation, the number of concurrent requests to prevent overflow of browser memory, the code is as follows:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = s, initial - scale = 1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">upload</button>
</body>
<script>
// Request the base address
axios.defaults.baseURL = 'http://localhost:3000'
// The selected file
var file = null
// Select the file
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0]}// Start uploading
document.getElementById('uploadBtn').onclick = async function(){
    if(! file)return
    // Create a slice
    // let size = 1024 * 1024 * 10; //10MB slice size
    let size = 1024 * 50 //50KB slice size
    let fileChunks = []
    let index = 0 // Section number
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        });
    }
    // Control concurrency
    let pool = []/ / concurrent pool
    let max = 3 // Maximum concurrency
    for(let i=0; i<fileChunks.length; i++){let item = fileChunks[i]
        let formData = new FormData()
        formData.append('filename', file.name)
        formData.append('hash', item.hash)
        formData.append('chunk', item.chunk)
        // Upload slices
        let task = axios({
            method: 'post'.url: '/upload'.data: formData
        })
        task.then((data) = >{
            // Remove the Promise task from the concurrency pool after the request ends
            let index = pool.findIndex(t= > t===task)
            pool.splice(index)
        })
        pool.push(task)
        if(pool.length === max){
            // Each time the concurrent pool finishes running a task, another task is inserted
            await Promise.race(pool)
        }
    }
    // All tasks completed, merge slices
    await axios({
        method: 'get'.url: '/merge'.params: {
            filename: file.name
        }
    });
    console.log('Upload completed')}</script>
</html>
Copy the code

Step 3- Resumable

After a single request fails, when the catch method is triggered, the current request is put into the failure list. After the completion of this round of requests, the failed request is processed repeatedly, with the specific code as follows:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = s, initial - scale = 1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput">
    <button id="uploadBtn">upload</button>
</body>
<script>
// Request the base address
axios.defaults.baseURL = 'http://localhost:3000'
// The selected file
var file = null
// Select the file
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0]}// Start uploading
document.getElementById('uploadBtn').onclick = function(){
    if(! file)return;
    // Create a slice
    // let size = 1024 * 1024 * 10; //10MB slice size
    let size = 1024 * 50; //50KB slice size
    let fileChunks = [];
    let index = 0 // Section number
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // Control concurrency and breakpoint continuation
    const uploadFileChunks = async function(list){
        if(list.length === 0) {// All tasks completed, merge slices
            await axios({
                method: 'get'.url: '/merge'.params: {
                    filename: file.name
                }
            });
            console.log('Upload completed')
            return
        }
        let pool = []/ / concurrent pool
        let max = 3 // Maximum concurrency
        let finish = 0// The number of completions
        let failList = []// List of failures
        for(let i=0; i<list.length; i++){let item = list[i]
            let formData = new FormData()
            formData.append('filename', file.name)
            formData.append('hash', item.hash)
            formData.append('chunk', item.chunk)
            // Upload slices
            let task = axios({
                method: 'post'.url: '/upload'.data: formData
            })
            task.then((data) = >{
                // Remove the Promise task from the concurrency pool after the request ends
                let index = pool.findIndex(t= > t===task)
                pool.splice(index)
            }).catch(() = >{
                failList.push(item)
            }).finally(() = >{
                finish++
                // All requests are completed
                if(finish===list.length){
                    uploadFileChunks(failList)
                }
            })
            pool.push(task)
            if(pool.length === max){
                // Each time the concurrent pool finishes running a task, another task is inserted
                await Promise.race(pool)
            }
        }
    }
    uploadFileChunks(fileChunks)

}
</script>
</html>
Copy the code

The back-end

Step 1. Install dependencies

NPM I [email protected] NPM I [email protected]Copy the code

Step 2. Interface implementation

const express = require('express')
const multiparty = require('multiparty')
const fs = require('fs')
const path = require('path')
const { Buffer } = require('buffer')
// Upload the final path of the file
const STATIC_FILES = path.join(__dirname, './static/files')
// Upload a temporary file path
const STATIC_TEMPORARY = path.join(__dirname, './static/temporary')
const server = express()
// Static file hosting
server.use(express.static(path.join(__dirname, './dist')))
// Interface for uploading slices
server.post('/upload'.(req, res) = > {
    const form = new multiparty.Form();
    form.parse(req, function(err, fields, files) {
        let filename = fields.filename[0]
        let hash = fields.hash[0]
        let chunk = files.chunk[0]
        let dir = `${STATIC_TEMPORARY}/${filename}`
        // console.log(filename, hash, chunk)
        try {
            if(! fs.existsSync(dir)) fs.mkdirSync(dir)const buffer = fs.readFileSync(chunk.path)
            const ws = fs.createWriteStream(`${dir}/${hash}`)
            ws.write(buffer)
            ws.close()
            res.send(`${filename}-${hash}Section uploaded successfully)}catch (error) {
            console.error(error)
            res.status(500).send(`${filename}-${hash}Section upload failed ')}})})// Merge slicing interfaces
server.get('/merge'.async (req, res) => {
    const { filename } = req.query
    try {
        let len = 0
        const bufferList = fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`).map(hash= > {
            const buffer = fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${hash}`)
            len += buffer.length
            return buffer
        });
        // Merge files
        const buffer = Buffer.concat(bufferList, len);
        const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`)
        ws.write(buffer);
        ws.close();
        res.send('Slice merge completed');
    } catch (error) {
        console.error(error);
    }
})

server.listen(3000._= > {
    console.log('http://localhost:3000/')})Copy the code

Other implementations

If you use Tencent Cloud or Ali Cloud file upload services, they provide NPM libraries, such as Tencent Cloud coS-Js-SDK-V5, which provides its own slicing related configurations