This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

File upload is a common requirement in project development. This article will show you how to build a multi-file breakpoint continuation component that can handle multiple files at the same time, and can resume uploads in the case of exceptions or network outages. You can manually suspend and resume file uploads. This article covers both the front end and the back end, and is a small, full-stack project that will use NodeJs, Express, Busboy, and XMLHttpRequest to build the project using the scaffolding genergenerator – Norm that I developed myself.

The source code for this article is on GitHub, view the source code.

Project installation

This is a Node project and you can set it up in the normal way. If you already have a project, you can continue with that project without any problems. For a fresh start, perform the following operations:

  1. Download and install Node, which installs NPM globally;
  2. Install the Yeoman,npm install -g yoAnd global installation of scaffoldingnpm install -g generator-norm
  3. Create the folder where the project files are located.
  4. Open the terminal and use itcdCommand to navigate to the project directory, for examplecd multifile-uploader;
  5. Run the commandnpm init -yInitialize the NPM project to generate a simplepackage.json;
  6. Run the commandyo normInitialize project base dependencies;
  7. usenpm install express --saveCommand to installexpressThe module.
  8. runyarn startTo open the preview

The working principle of

Now to see how the system works, the application has two processes that require strict coordination between the server and the client.

  • Upload process: Get a new file, send information about the file to the server, and the server returns a key (ID) that is needed to send the file block, allowing it to track the file and resume its upload in the event of a later interruption.

  • Resume upload process: Query the server for the status of the file provided with the name and key (ID) so that the server can respond to the block size at which the upload stops so that the upload can continue from that point.

There is also a fourth endpoint, which is used to get all the file keys that need to be processed to resume the upload in case the upload stops and you want to resume it a few days later. For this tutorial, once the ids are uploaded and retrieved, they are kept on the client side to recover them, but if the browser TAB is closed, the ids are lost and cannot be recovered.

The client

The client here is mainly the WEB application side. The HTML of this project is very simple, modify the file app/index.html, below is the core code.

<div class="row marketing"> <div class=" col-lG-12 "> <label class="upload-btn"> Upload file <input type="file" multiple accept="video/*" id="file-upload-input" style="display: none" /> </label> </div> </div>Copy the code

The important detail here is that the input attribute must have a multiple attribute to allow the user to select multiple files, and optionally use the Accept attribute to identify the types of files that are allowed to be uploaded.

For uploaded files, the element object is retrieved via the ID attribute of the input and bound to the event change to listen for the user’s file selection.

const elemFileInput = document.getElementById("file-upload-input");

elemFileInput.addEventListener("change", (e) => {
    // handle file here
});
Copy the code

The uploadFiles method is defined in the following code:

const uploadFiles = (() => { const URL = `http://localhost:3008/`; const ENDPOINTS = { UPLOAD: `${URL}upload`, UPLOAD_STATUS: `${URL}/upload-status`, UPLOAD_REQUEST: `${URL}/upload-request`, }; const defaultOptions = { url: ENDPOINTS.UPLOAD, startingByte: 0, fileId: "", onAbort() {}, onProgress() {}, onError() {}, onComplete() {}, }; return (files, options = defaultOptions) => { // handle file objects here }; }) ();Copy the code

The above code returns a function that takes a list of files and an optional option object to handle the upload. The three API endpoints involved are as follows:

  • upload: Transfers file blocks
  • upload-status: Queries the status of file uploads. If the upload starts and stops or something breaks, forcing the selection of a file to upload again, this returns information about where the file stopped and how many blocks were uploaded before the break.
  • upload-request: notifies the server of what to upload so that the server can set the key and track the file when the upload begins.

Options call back to the various stages or states of an upload (abort, progress, error, and completion), starting with a byte from where in the file stream the upload is to be initiated, and a file ID is a way to identify the file.

File uploading is processed in UI/UX

Before setting up the upload endpoint on the server, it is best to process it on the client, as this will help make more sense on the server side.

For this part, you need something to do with the UI. This can easily be done with any UI library or framework, such as Vue and Angular.

UploadAndTrackFiles, similar to the uploadFiles function, needs to interact with uploadFiles and update its UI. UploadAndTrackFiles is a function that takes a list of files, calls uploadFiles, and then sets up the view by attaching a progress indicator for each file element on the page body to the progressBox container.

Internally, it also has all the callback functions to track and respond to each file state.

const uploadAndTrackFiles = (() => { const progressBox = document.createElement("div"); let uploader = null; const setFileElement = (file) => { // create file element here }; const onProgress = (e, file) => {}; const onError = (e, file) => {}; const onAbort = (e, file) => {}; const onComplete = (e, file) => {}; return (uploadedFiles) => { [...uploadedFiles].forEach(setFileElement); document.body.appendChild(progressBox); uploader = uploadFiles(uploadedFiles, { onProgress, onError, onAbort, onComplete, }); }; }) ();Copy the code

Call uploadAndTrackFiles from the Change event listener to pass the list of files.

elemFileInput.addEventListener("change", (e) => {
    uploadAndTrackFiles(e.currentTarget.files);
    e.currentTarget.value = "";
});
Copy the code

One thing to do now is to clear the value of the subsequent input so that the browser does not prevent the user from adding more files. So far, you can go back to the uploadFiles function to handle uploads.

File Upload Logic

Return to the uploadFiles function, loop through the list of files, and call the uploadFile that was initialized to handle the request. When the request is made, an object is returned, which is the set of public functions that are called from a file.

const uploadFiles = (() => { const URL = `http://localhost:3008/`; const ENDPOINTS = { UPLOAD: `${URL}upload`, UPLOAD_STATUS: `${URL}/upload-status`, UPLOAD_REQUEST: `${URL}/upload-request`, }; const defaultOptions = { url: ENDPOINTS.UPLOAD, startingByte: 0, fileId: "", onAbort() {}, onProgress() {}, onError() {}, onComplete() {}, }; const fileRequests = new WeakMap(); const uploadFile = (file, options) => {}; const abortFileUpload = (file) => {}; const retryFileUpload = (file) => {}; const clearFileUpload = (file) => {}; const resumeFileUpload = (file) => {}; return (files, options = defaultOptions) => { [...files].forEach((file) => { uploadFile(file, { ... defaultOptions, ... options }); }); return { abortFileUpload, retryFileUpload, clearFileUpload, resumeFileUpload, }; }; }) ();Copy the code

Because multiple files can be uploaded at the same time, fileRequests of type WeakMap (compared to Map, key names are weak references, key values can be arbitrary, and objects that key names point to can be garbage collected) are defined to track all files.

The uploadFile function simply lets the server know that it should expect a file to be uploaded soon and provides the filename.

const uploadFiles = (() => { const URL = `http://localhost:3008/`; const ENDPOINTS = { UPLOAD: `${URL}upload`, UPLOAD_STATUS: `${URL}/upload-status`, UPLOAD_REQUEST: `${URL}/upload-request`, }; const defaultOptions = { url: ENDPOINTS.UPLOAD, startingByte: 0, fileId: "", onAbort() {}, onProgress() {}, onError() {}, onComplete() {}, }; const fileRequests = new WeakMap(); const uploadFile = (file, options) => {}; const abortFileUpload = (file) => {}; const retryFileUpload = (file) => {}; const clearFileUpload = (file) => {}; const resumeFileUpload = (file) => {}; const uploadFileChunks = (file, options) => {}; const uploadFile = (file, options) => { return fetch(ENDPOINTS.UPLOAD_REQUEST, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ fileName: file.name, }), }) .then((res) => res.json()) .then((res) => { options = { ... options, ... res }; fileRequests.set(file, { request: null, options }); uploadFileChunks(file, options); }) .catch((e) => { options.onError({ ... e, file }); }); }; return (files, options = defaultOptions) => { [...files].forEach((file) => { uploadFile(file, { ... defaultOptions, ... options }); }); return { abortFileUpload, retryFileUpload, clearFileUpload, resumeFileUpload, }; }; }) ();Copy the code

Once the server responds with the file ID, it updates the function and uses the request option to create an empty record of file requests for later retries. Once this is all set up, it calls uploadFileChunks with file and update options, which will actually handle the upload.

Here, you first initialize the form data and request, and then slice the file from the start byte point.

A file is a Blob type that lets you slice the file bytes using TypedArray, which is how you track where the upload starts and the file server takes care of putting the files together piece by piece.

const uploadFileChunks = (file, options) => { const formData = new FormData(); const req = new XMLHttpRequest(); const chunk = file.slice(options.startingByte); formData.append("chunk", chunk, file.name); formData.append("fileId", options.fileId); req.open("POST", options.url, true); req.setRequestHeader( "Content-Range", `bytes=${options.startingByte}-${options.startingByte + chunk.size}/${ file.size }` ); req.setRequestHeader("X-File-Id", options.fileId); If (req.status === 200) {options.oncomplete (e, file); } else { options.onError(e, file); }}; req.upload.onprogress = (e) => { const loaded = options.startingByte + e.loaded; options.onProgress( { ... e, loaded, total: file.size, percentage: (loaded * 100) / file.size, }, file ); }; req.ontimeout = (e) => options.onError(e, file); req.onabort = (e) => options.onAbort(e, file); req.onerror = (e) => options.onError(e, file); fileRequests.get(file).request = req; req.send(formData); };Copy the code

You then communicate with the server by providing the name of the block (that is, the filename) and the file ID, and by setting these properties in the form data to be sent.

When using XMLHttpRequest, you need to open a request, make a POST request here, and set some headers.

  • Content-Range: This is the range of file bytes sent by way of communication with the server. This information is important for the server to know how to put the files back together and validate the request.
  • X-File-Id: Pass filesidIs another way of saying.

Once the request is set up, upload progress events are mapped with option callbacks for passing events and file bloBs.

  • Once the upload is complete, it will triggerloadEvents.
  • When the request receives more data, it firesprogressEvent from which you can extract the total number of bytes uploaded so far and calculate the upload progress percentage.
  • timeouterrorWill be seen as a failure;
  • When an abort is triggered to suspend an upload,suspendWill be triggered.

What you do next is update the file request and send the form data using the actual request in WeakMap.

File upload logic is restored

The logic of restoring the upload is much simpler. Here, all you need to do is to use the provided file to get the file request from WeakMap and pass the file ID and name as query parameters to the server to query the upload status of the file.

Once you have the status (total bytes uploaded previously), call the uploadFileChunks function to provide the bytes to start uploading.

const resumeFileUpload = (file) => { const fileReq = fileRequests.get(file); if (fileReq) { return fetch( `${ENDPOINTS.UPLOAD_STATUS}? fileName=${file.name}&fileId=${fileReq.options.fileId}` ) .then((res) => res.json()) .then((res) => { uploadFileChunks(file, { ... fileReq.options, startingByte: Number(res.totalChunkUploaded), }); }) .catch((e) => { fileReq.options.onError({ ... e, file }); }); }};Copy the code

File retry upload logic

File uploads can go wrong, the network may lose its connection, the server may crash, etc.

The logic is almost identical to that for resuming uploads, the only difference being that on the catch block, the upload is restarted in case the file never started uploading and the server doesn’t know about it.

const retryFileUpload = (file) => { const fileReq = fileRequests.get(file); if (fileReq) { // try to get the status just in case it failed mid upload return fetch( `${ENDPOINTS.UPLOAD_STATUS}? fileName=${file.name}&fileId=${fileReq.options.fileId}` ) .then((res) => res.json()) .then((res) => { // if uploaded we continue uploadFileChunks(file, { ... fileReq.options, startingByte: Number(res.totalChunkUploaded), }); }) .catch(() => { // if never uploaded we start uploadFileChunks(file, fileReq.options); }); }};Copy the code

File upload logic is suspended

To suspend the currently uploaded file, abort the request. In addition to grabbing file upload progress, the ability to abort requests is the second reason to use XMLHttpRequest.

When this function is called with a file, the request is retrieved from WeakMap and the request calls abort function to stop the upload.

const abortFileUpload = async (file) => {
    const fileReq = fileRequests.get(file);

    if (fileReq && fileReq.request) {
        fileReq.request.abort();
        return true;
    }

    return false;
};
Copy the code

This will simply stop sending blocks to the server, which can then continue uploading by calling the resumeFileUpload function.

File cleanup logic

Clearing or canceling an upload at or after completion of the upload simply aborts the request and clears it, and the upload cannot be resumed after the clean operation is performed.

const clearFileUpload = async (file) => {
    const fileReq = fileRequests.get(file);

    if (fileReq) {
        await abortFileUpload(file);
        fileRequests.delete(file);

        return true;
    }

    return false;
};
Copy the code

Upload progress

In the uploadAndTrackFiles function, you can define the internal HTML of the progress box, which will show how many files are currently being uploaded, and provide an upload progress recovery table telling how many files failed, succeeded, paused, and so on.

There is also a maximize button, which when clicked will expand or collapse the uploader. Following this, there is a total file upload progress bar and a container that will place each file indicator element and individual states and controls.

const uploadAndTrackFiles = (() => { const progressBox = document.createElement("div"); let uploader = null; progressBox.className = "upload-progress-tracker"; progressBox.innerHTML = ` <h3>Uploading 0 Files</h3> <p class="upload-progress"> <span class="uploads-percentage">0%</span> <span class="success-count">0</span> <span class="failed-count">0</span> <span class="paused-count">0</span> </p> <button type="button" class="maximize-btn">Maximize</button> <div class="uploads-progress-bar" style="width: 0;" ></div> <div class="file-progress-wrapper"></div> `; progressBox .querySelector(".maximize-btn") .addEventListener("click", (e) => { e.currentTarget.classList.toggle("expanded"); progressBox.classList.toggle("expanded"); }); const setFileElement = (file) => { // create file element here }; const onProgress = (e, file) => {}; const onError = (e, file) => {}; const onAbort = (e, file) => {}; const onComplete = (e, file) => {}; return (uploadedFiles) => { [...uploadedFiles].forEach(setFileElement); document.body.appendChild(progressBox); uploader = uploadFiles(uploadedFiles, { onProgress, onError, onAbort, onComplete, }); }; }) ();Copy the code

Once you receive the file Blobs, you can use it to create the file element by calling setFileElement, which creates a div containing the file name, progress bar, percentage, and control buttons to handle file uploads.

You also need to trace the individual file elements so that they can be referenced later using the file Blob. You need to track its size, status, percentage, and uploaded block size. All of these details are used to render the element progress details and update the view accordingly.

const files = new Map(); const FILE_STATUS = { PENDING: "pending", UPLOADING: "uploading", PAUSED: "paused", COMPLETED: "completed", FAILED: "failed", }; const setFileElement = (file) => { const extIndex = file.name.lastIndexOf("."); const fileElement = document.createElement("div"); fileElement.className = "file-progress"; fileElement.innerHTML = ` <div class="file-details" style="position: relative"> <p> <span class="status">pending</span> <span class="file-name"> ${file.name.substring(0, extIndex)} </span> <span class="file-ext"> ${file.name.substring(extIndex)} </span> </p> <div class="progress-bar" style="width: 0;" ></div> </div> <div class="file-actions"> <button type="button" class="retry-btn" style="display: none">Retry</button> <button type="button" class="cancel-btn" style="display: none">Pause</button> <button type="button" class="resume-btn" style="display: none">Resume</button> <button type="button" class="clear-btn" style="display: none">Clear</button> </div> `; files.set(file, { element: fileElement, size: file.size, status: FILE_STATUS.PENDING, percentage: 0, uploadedChunkSize: 0}); const [ _, { children: [retryBtn, pauseBtn, resumeBtn, clearBtn], }, ] = fileElement.children; clearBtn.addEventListener("click", () => { uploader.clearFileUpload(file); files.delete(file); fileElement.remove(); updateProgressBox(); }); retryBtn.addEventListener("click", () => { uploader.retryFileUpload(file); }); pauseBtn.addEventListener("click", () => { uploader.abortFileUpload(file); }); resumeBtn.addEventListener("click", () => { uploader.resumeFileUpload(file); }); progressBox .querySelector(".file-progress-wrapper") .appendChild(fileElement); };Copy the code

Once these elements are used, a reference to its control button is obtained and a click event is appended to call the public method from the uploader to control the content of the passed file.

When the element is cleared, the progress box element needs to be updated by calling the updateProgressBox function. This is a function that updates the overall details of all uploaded files. Like all failures, successes, pauses, and uploads.

const updateProgressBox = () => {
    const [title, uploadProgress, expandBtn, progressBar] =
        progressBox.children;

    if (files.size > 0) {
        let totalUploadedFiles = 0;
        let totalUploadingFiles = 0;
        let totalFailedFiles = 0;
        let totalPausedFiles = 0;
        let totalChunkSize = 0;
        let totalUploadedChunkSize = 0;
        const [uploadedPerc, successCount, failedCount, pausedCount] =
            uploadProgress.children;

        files.forEach((fileObj) => {
            if (fileObj.status === FILE_STATUS.FAILED) {
                totalFailedFiles += 1;
            } else {
                if (fileObj.status === FILE_STATUS.COMPLETED) {
                    totalUploadedFiles += 1;
                } else if (fileObj.status === FILE_STATUS.PAUSED) {
                    totalPausedFiles += 1;
                } else {
                    totalUploadingFiles += 1;
                }

                totalChunkSize += fileObj.size;
                totalUploadedChunkSize += fileObj.uploadedChunkSize;
            }
        });

        const percentage =
            totalChunkSize > 0
                ? Math.min(
                      100,
                      Math.round(
                          (totalUploadedChunkSize * 100) / totalChunkSize
                      )
                  )
                : 0;

        title.textContent =
            percentage === 100
                ? `Uploaded ${totalUploadedFiles} File${
                      totalUploadedFiles !== 1 ? "s" : ""
                  }`
                : `Uploading ${totalUploadingFiles}/${files.size} File${
                      files.size !== 1 ? "s" : ""
                  }`;

        uploadedPerc.textContent = `${percentage}%`;
        successCount.textContent = totalUploadedFiles;
        failedCount.textContent = totalFailedFiles;
        pausedCount.textContent = totalPausedFiles;
        progressBar.style.width = `${percentage}%`;
        progressBox.style.backgroundSize = `${percentage}%`;
        expandBtn.style.display = "inline-block";
        uploadProgress.style.display = "block";
        progressBar.style.display = "block";
    } else {
        title.textContent = "No Upload in Progress";
        expandBtn.style.display = "none";
        uploadProgress.style.display = "none";
        progressBar.style.display = "none";
    }
};
Copy the code

You also need to update each file element based on the file upload progress event, which simply updates the progress bar percentage and status text messages, and toggles the visible or invisible buttons based on the file upload status.


const updateFileElement = (fileObject) => {
    const [
        {
            children: [
                {
                    children: [status],
                },
                progressBar,
            ],
        },
        // .file-details
        {
            children: [retryBtn, pauseBtn, resumeBtn, clearBtn],
        },
    ] = fileObject.element.children;

    requestAnimationFrame(() => {
        status.textContent =
            fileObject.status === FILE_STATUS.COMPLETED
                ? fileObject.status
                : `${Math.round(fileObject.percentage)}%`;
        status.className = `status ${fileObject.status}`;

        progressBar.style.width = fileObject.percentage + "%";
        progressBar.style.background =
            fileObject.status === FILE_STATUS.COMPLETED
                ? "green"
                : fileObject.status === FILE_STATUS.FAILED
                ? "red"
                : "#222";

        pauseBtn.style.display =
            fileObject.status === FILE_STATUS.UPLOADING
                ? "inline-block"
                : "none";

        retryBtn.style.display =
            fileObject.status === FILE_STATUS.FAILED ? "inline-block" : "none";

        resumeBtn.style.display =
            fileObject.status === FILE_STATUS.PAUSED ? "inline-block" : "none";

        clearBtn.style.display =
            fileObject.status === FILE_STATUS.COMPLETED ||
            fileObject.status === FILE_STATUS.PAUSED
                ? "inline-block"
                : "none";

        updateProgressBox();
    });
};
Copy the code

Now that you have a function to update elements based on upload events, you need to handle all file upload events accordingly.

Therefore, whenever a process event calls the onProgress callback function, that file is used to get the rendered file object from the file set in the setFileElement function, update its state, percentage, and block size with the event details, and then call the updateFileElement function.

const onProgress = (e, file) => {
    const fileObj = files.get(file);

    fileObj.status = FILE_STATUS.UPLOADING;
    fileObj.percentage = e.percentage;
    fileObj.uploadedChunkSize = e.loaded;

    updateFileElement(fileObj);
};
Copy the code

When the upload is complete, do almost the same thing with the file object and call the updateFileElement function again.

const onComplete = (e, file) => {
    const fileObj = files.get(file);

    fileObj.status = FILE_STATUS.COMPLETED;
    fileObj.percentage = 100;

    updateFileElement(fileObj);
};
Copy the code

For abort events, the status is updated to “pause,” updating the file elements.

const onAbort = (e, file) => {
    const fileObj = files.get(file);

    fileObj.status = FILE_STATUS.PAUSED;

    updateFileElement(fileObj);
};
Copy the code

Finally, if there is a problem, set the percentage to 100 so that the progress bar is rendered in its entirety, and set the status to failed so that the progress bar turns red.

const onError = (e, file) => {
    const fileObj = files.get(file);

    fileObj.status = FILE_STATUS.FAILED;
    fileObj.percentage = 100;

    updateFileElement(fileObj);
};
Copy the code

This completes all the logic for the client-side file upload module.

The service side

This is a simple full-stack project that will require the server to handle the upload request. You need to create a server.js file in the project directory as follows:

const express = require("express"); const app = express(); App.listen (3008, () => {console.log(" service running on port: 3008"); });Copy the code

This is a very simple server that only handles uploads and does not serve application website files.

Endpoint presetting

There is little to do to set up the endpoint, and the CORS module will be installed first so that requests from different domains can be allowed to pass through. The next step is to allow requests to process the JSON request body by adding express.json middleware.

const express = require("express"); const cors = require("cors"); const PORT = 3008; const app = express(); app.use(express.json()); app.use(cors()); App.listen (PORT, () => {console.log(' service running on PORT: ${PORT} '); });Copy the code

Since the server is involved in file processing, you need to import the FS module, and you need to add a function to create a unique ID generator. You will also place the file in the upload directory, so you need a utility function to combine the file path with the ID and filename.

const uniqueAlphaNumericId = (() => { const heyStack = "0123456789abcdefghijklmnopqrstuvwxyz"; const randomInt = () => Math.floor(Math.random() * Math.floor(heyStack.length)); return (length = 24) => Array.from({ length }, () => heyStack[randomInt()]).join(""); }) (); const getFilePath = (fileName, fileId) => `./uploads/file-${fileId}-${fileName}`;Copy the code

Upload request endpoint

The upload request endpoint is very simple, it requires a filename and should throw an error if it is not provided. When there is a filename, generate an ID and create an empty file with the ID and name, and then respond with the file ID.

app.post("/upload-request", (req, res) => { if (! req.body || ! Req.body.filename) {res.status(400). Json ({message: "fileName content cannot be empty "}); } else { const fileId = uniqueAlphaNumericId(); fs.createWriteStream(getFilePath(req.body.fileName, fileId), { flags: "w", }); res.status(200).json({ fileId }); }});Copy the code

Upload status endpoint

Upload status is also simple, with the first setting committing to the fs.stat method to make it easier to use. Query the file and get its status to read the total size of the blocks uploaded so far and return it, otherwise, respond with 400 status and error.

const getFileDetails = promisify(fs.stat); app.get("/upload-status", (req, res) => { if (req.query && req.query.fileName && req.query.fileId) { getFileDetails(getFilePath(req.query.fileName, req.query.fileId)) .then((stats) => { res.status(200).json({ totalChunkUploaded: stats.size }); }). The catch ((err) = > {the console. The error (" file read error ", err); Res.status (400). Json ({message: "no corresponding file ", credentials: req.query,}); }); }});Copy the code

Upload the endpoint

To process file blocks, you need to install the Busboy module

npm install busboy --save
Copy the code

For the first half of the handler, you just need to make sure you have everything you need to continue by validating the request headers for the file ID and content scope.

app.post("/upload", (req, res) => { const contentRange = req.headers["content-range"]; const fileId = req.headers["x-file-id"]; if (! contentRange) { console.log("Missing Content-Range"); return res .status(400) .json({ message: 'Missing "Content-Range" header' }); } if (! fileId) { console.log("Missing File Id"); return res.status(400).json({ message: 'Missing "X-File-Id" header' }); } const match = contentRange.match(/bytes=(\d+)-(\d+)\/(\d+)/); if (! match) { console.log("Invalid Content-Range Format"); return res .status(400) .json({ message: 'Invalid "Content-Range" Format' }); } const rangeStart = Number(match[1]); const rangeEnd = Number(match[2]); const fileSize = Number(match[3]); if ( rangeStart >= fileSize || rangeStart >= rangeEnd || rangeEnd > fileSize ) { return res .status(400) .json({ message: 'Invalid "Content-Range" provided' }); } const busboy = new Busboy({ headers: req.headers }); busboy.on("file", (_, file, fileName) => { const filePath = getFilePath(fileName, fileId); if (! fileId) { req.pause(); } getFileDetails(filePath) .then((stats) => { if (stats.size ! == rangeStart) { return res .status(400) .json({ message: 'Bad "chunk" provided' }); } file.pipe(fs.createWriteStream(filePath, { flags: "a" })).on( "error", (e) => { console.error("failed upload", e); res.sendStatus(500); }); }) .catch((err) => { console.log("No File Match", err); res.status(400).json({ message: "No file with such credentials", credentials: req.query, }); }); }); busboy.on("error", (e) => { console.error("failed upload", e); res.sendStatus(500); }); busboy.on("finish", () => { res.sendStatus(200); }); req.pipe(busboy); });Copy the code

The busboy module gets the request header and can pipe requests to it. In the busboy file event, make sure the byte starts with a block in memory, then start writing the block and putting the files together.

To this point, the server side of the logic is also basically complete, the source code involved in the article on GitHub, view the source code.

Update record

  • Version 1.0.0-07 October 2021

    • The UI to increasepengdingstate
    • Added preloading of SVG ICONS, not mandatory