Implementing Efficient Large File Uploads Using Web Workers

0

To implement the functionality of uploading large files in a web application, there are several significant challenges that need to be addressed. Particularly, when the file sizes reach several gigabytes to hundreds of gigabytes, finding an efficient and reliable upload method becomes crucial. In this article, we will explore the basic concepts of large file uploads and discuss how to use Web Workers to upload files in the background from the frontend.

1. Challenges of Large File Uploads

The primary challenges faced when uploading large files in a web application include:

  • Network Stability: Uploading large files can take a considerable amount of time, during which the network connection may become unstable or disconnect.
  • Server Resource Management: The server must be prepared to handle multiple users uploading large files simultaneously, requiring sufficient storage and memory management.
  • UI Responsiveness: Large file upload tasks consume significant resources on the client side, potentially causing the UI to slow down or become unresponsive.

To address these issues, a method that combines chunking the file into smaller parts for uploading and using Web Workers for background processing is used.

2. Uploading Files in Chunks

When uploading large files, instead of sending a single large file at once, it can be broken down into smaller chunks and uploaded sequentially. This approach offers the following benefits:

  • Easier Recovery from Network Errors: Uploads can be retried at the chunk level, allowing for partial progress without having to restart the entire upload process.
  • Parallel Uploads: Multiple chunks can be uploaded simultaneously, reducing the overall upload time.

3. Background Upload Processing with Web Workers

Web Workers allow JavaScript code to run in the background, separate from the main thread, ensuring that file upload tasks do not hinder the responsiveness of the UI.

The key steps to implementing large file uploads using Web Workers are as follows:

  • File Selection and Preparation: Once the user selects a file, it is divided into chunks on the client side.
  • Start Background Task: A Web Worker is created to handle the file upload task in the background.
  • Chunk Upload: The Web Worker sequentially or in parallel uploads the file chunks to the server, sending status messages to the main thread upon successful completion of each chunk upload.
  • Chunk Merging on the Server: The server merges the received chunks in order to form the final file.

4. Implementation Example

Server-side Code (Flask)

On the server side, the task is to receive chunks from the client and merge them into the final file.

from flask import Flask, request, jsonify
import os

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads'
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

@app.route('/upload', methods=['POST'])
def upload_chunk():
    file = request.files['file']
    chunk_index = request.form['chunkIndex']
    total_chunks = request.form['totalChunks']
    file_name = request.form['fileName']

    chunk_path = os.path.join(UPLOAD_FOLDER, f'{file_name}_chunk_{chunk_index}')
    file.save(chunk_path)

    if int(chunk_index) + 1 == int(total_chunks):
        merge_chunks(file_name, total_chunks)

    return jsonify({"message": "Chunk uploaded successfully"})

def merge_chunks(file_name, total_chunks):
    with open(os.path.join(UPLOAD_FOLDER, file_name), 'wb') as final_file:
        for i in range(int(total_chunks)):
            chunk_path = os.path.join(UPLOAD_FOLDER, f'{file_name}_chunk_{i}')
            with open(chunk_path, 'rb') as chunk_file:
                final_file.write(chunk_file.read())
            os.remove(chunk_path)

if __name__ == '__main__':
    app.run(debug=True)

Client-side Code (HTML & JavaScript)

The client-side code, written in HTML and JavaScript, selects the file and uses Web Workers to handle the upload task in the background.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Background File Upload</title>
</head>
<body>
    <input type="file" id="fileInput">
    <button onclick="startUpload()">Start Upload</button>
    <p id="status">Waiting to start...</p>

    <script src="main.js"></script>
</body>
</html>
let worker;

function startUpload() {
    const file = document.getElementById('fileInput').files[0];
    if (file) {
        worker = new Worker('worker.js');
        worker.postMessage({ file });

        worker.onmessage = function(event) {
            document.getElementById('status').textContent = event.data.message;
        };

        worker.onerror = function(error) {
            console.error('Worker error:', error);
            document.getElementById('status').textContent = 'Error occurred during upload.';
        };
    } else {
        alert('Please select a file first.');
    }
}
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB

self.onmessage = async function(event) {
    const file = event.data.file;
    const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
    let chunkIndex = 0;

    while (chunkIndex < totalChunks) {
        const start = chunkIndex * CHUNK_SIZE;
        const end = Math.min(start + CHUNK_SIZE, file.size);
        const chunk = file.slice(start, end);

        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('chunkIndex', chunkIndex);
        formData.append('totalChunks', totalChunks);
        formData.append('fileName', file.name);

        try {
            await fetch('/upload', {
                method: 'POST',
                body: formData,
            });

            self.postMessage({ message: `Uploaded chunk ${chunkIndex + 1} of ${totalChunks}` });
            chunkIndex++;
        } catch (error) {
            self.postMessage({ message: `Error uploading chunk ${chunkIndex + 1}` });
            break;
        }
    }

    if (chunkIndex === totalChunks) {
        self.postMessage({ message: 'File upload complete' });
    }
};

5. Execution and Results

  • Run the Flask server to be ready to receive upload requests from the client.
  • In the browser, run the client-side code, select a file, and click the `Start Upload` button. The Web Worker will handle the file upload in the background.
  • The upload status is displayed in real-time on the UI, and users can continue other tasks while the upload is in progress.

Conclusion

Large file uploads are a critical feature in web applications. By using Web Workers to handle file uploads in the background, users can continue to interact with other UI features while the upload process is ongoing. This method effectively maintains UI responsiveness while providing a stable upload experience.

Leave a Reply