paint-brush
Handling Files on the Web: A Deep Diveby@psuranas
743 reads
743 reads

Handling Files on the Web: A Deep Dive

by Prateek SuranaApril 24th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, we will take a deep dive into file handling in JavaScript. We will explore how files work, how to access them, and how to upload them to a server. We'll also explore some third-party services you can use while managing files for your applications.
featured image - Handling Files on the Web: A Deep Dive
Prateek Surana HackerNoon profile picture

In this article, we will take a deep dive into file handling in JavaScript; starting by exploring how files work, how to access them, how to upload them to a server, and some third-party services you can use while managing files for your applications.


Let's dive in and discover the world of file handling in JavaScript on the web!

How Do Files Work in the Browser?

Before understanding how files are represented on the web, we must understand the Blob in JavaScript.

Blob

According to MDN Web Docs, a Blob object represents a file-like object of immutable raw data that can be read as text or binary data and converted into a ReadableStream, which further allows its methods to process the data as needed.


A simple example of a blob would be creating it via the Blob constructor:


const array = ['<span>hello</span>','<span>world</span>'];
const blob = new Blob(array, { type: "text/html" });


Here, the Blob constructor accepts an iterable object such as an Array, ArrayBuffer strings, etc., or a mix of any of such elements as the first argument and returns a blob whose content is the concatenation of the values in the array.


The second argument is an options object with an optional type property for specifying the MIME type of the blob.


If you try running the above snippet in your browser console and then try logging the output of the blob variable, you'll see an output like this:


> blob
  Blob {size: 36, type: 'text/html'}


You can also do blob.size to get the blob size in bytes, which will be 36 in the above case.


Blob also has some instance methods, which allow us to read the data from the Blob or create a new Blob with the subset of the data of the original Blob. For instance, the text method returns a Promise that resolves with a string containing the contents of the blob interpreted as UTF-8.


blob.text().then(data => {
	console.log(data); // <span>hello</span><span>world</span>
});


File

The File interface enables JavaScript on a webpage to provide information about files and access their content. The File object is a specific type of Blob, meaning it inherits all the properties and methods of Blob.


Like the Blob constructor, the File constructor also accepts iterable objects Array, ArrayBuffer, etc., and other Blobs as its first argument. The second argument is the file's name or the path to the file.


const array = ['<span>hello</span>','<span>world</span>'];
const file = new File(array, "index.html", {
  type: "text/html"
});


In the options object, the File constructor also accepts a property called lastModifiedAt, which, as the name suggests, is a read-only property that stores the last modified date of the file and defaults to Date.now().


Now similar to Blob, you can perform all the actions for reading the file's content. You can also download the file by creating an object URL for the file via the URL.createObjectURL method, which creates a string containing a URL representing a File, Blob, or MediaSource object.


So we can download the above file with the following snippet:


const array = ['<span>hello</span>','<span>world</span>'];
const file = new File(array, "index.html", {
  type: "text/html"
});

// Create a download URL for the File object
const downloadUrl = URL.createObjectURL(file);
// Create an anchor element for the download link
var downloadLink = document.createElement('a');
// Set the download link's href attribute to the download URL
downloadLink.href = downloadUrl;
// Set the download link's download attribute to the filename of the File object
downloadLink.download = file.name;
// Programmatically click the download link to initiate the file download
downloadLink.click();
// Revoke (cleanup) the download URL to free up memory
URL.revokeObjectURL(downloadUrl);


You can try running the above script in the browser console, and you'll see a file called index.html with the content <span>hello</span><span>world</span> getting downloaded.

Accessing Files On the Web

Now that we understand how the File interface works, let's look into how you can access files in your web applications.


Using the File API, you can ask the user to select local files and then read the content of those files, usually using the <input type= "file"> element. The file input in JavaScript returns a FileList via the files property. This object lets you access the files selected by the user.


To understand how this API works, let's look at a quick example inspired by the MDN docs, where we ask for image input from the user and display them with their sizes in KB.


<!DOCTYPE html>
<html>
  <head>
    <title>File handling demo</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <input type="file" id="fileElem" multiple accept="image/*" />
    <div id="fileList">
      <p>No files selected!</p>
    </div>

    <script>
      const fileElem = document.getElementById("fileElem");
      const fileList = document.getElementById("fileList");

      fileElem.addEventListener(
        "change",
        () => {
          if (!fileElem.files.length) {
            fileList.innerHTML = "<p>No files selected!</p>";
          } else {
            fileList.innerHTML = "";
            const list = document.createElement("ul");
            fileList.appendChild(list);
            for (let i = 0; i < fileElem.files.length; i++) {
              const li = document.createElement("li");
              list.appendChild(li);

              const img = document.createElement("img");
              img.src = URL.createObjectURL(fileElem.files[i]);
              img.height = 60;
              img.onload = () => {
                URL.revokeObjectURL(img.src);
              };
              li.appendChild(img);
              const info = document.createElement("span");
              info.innerHTML = `${fileElem.files[i].name}: ${Math.round(
                fileElem.files[i].size / 1024
              )} KB`;
              li.appendChild(info);
            }
          }
        },
        false
      );
    </script>
  </body>
</html>


Check out the live demo on CodeSandbox.


What's happening in the above snippet is:

  1. We have a file input element that only accepts images via the accept attribute with the id fileElem and a div with the id fileList, which initially contains a paragraph saying, “No files selected”


  2. Then in JavaScript, we get the DOM elements for both the file input and the div and attach a change listener to the file input element.


  3. Now whenever the user selects a single file or multiple files, the change event is triggered, which does the following stuff:


    1. It checks whether the user has selected any files by checking the length property of the FileList object we get via fileElem.files. If it doesn't have any files, we just set the HTML of the fileList div to the same initial value again.


    2. If it has files, we create an unordered list (ul) element, add it to the fileList div, and then iterate over the files creating and appending a li element for each file.


    3. Within each li tag, we create an img element that will be added to the li tag. Then create an object URL for the file via the URL.createObjectURL method we saw in the last section and set the img src to it. We also revoke the object URL via URL.revokeObjectURL once the image is loaded to free up the memory.


    4. Lastly, we also create another span in which we calculate the image size in KiloBytes by getting the size of the file via the size property and dividing it by 1024.

Uploading Files to a Remote Server

In the previous section, we learned how to use the Files API to accept files from the user and perform some operations in the browser, such as displaying the file size and preview for images.


However, in most real-world scenarios, the primary goal of accepting files is to upload them to a remote server for storage or further processing.


Let's take a simple example of how we can upload a file to a server with the progress being shown to the user as the file gets uploaded. The server then stores the file in its local file system and returns the file's name and size as the response.


This is what the code for the frontend would look like:


<!DOCTYPE html>
<html>
<head>
  <title>File Upload Example</title>
</head>
<body>
  <h1>File Upload Example</h1>
  <form id="uploadForm">
    <input type="file" name="file" id="fileInput">
    <button type="submit">Upload</button>
  </form>
  <progress id="progressBar" value="0" max="100"></progress>
  <div id="status"></div>

  <script>
    document.getElementById('uploadForm').addEventListener('submit', (event) => {
      event.preventDefault();
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];
      if (!file) {
        alert('Please select a file to upload');
        return;
      }

      const progressBar = document.getElementById('progressBar');
      progressBar.value = 0; // Reset progress bar
      const statusContainer = document.getElementById('status');
      statusContainer.textContent = ''; // Clear status container

      const xhr = new XMLHttpRequest();
      xhr.open('POST', '/upload');
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable) {
          const progress = Math.round((event.loaded / event.total) * 100); // Calculate upload progress
          progressBar.value = progress; // Update progress bar
        }
      });
      xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          if (xhr.status === 200) {
            const message = xhr.responseText;
            statusContainer.textContent = message; // Update status container
          } else {
            const error = xhr.responseText || 'Failed to upload file';
            alert(`Error: ${error}`);
          }
        }
      };
      const formData = new FormData();
      formData.append('file', file);
      xhr.send(formData);
    });
  </script>
</body>
</html>


Check out the demo with the complete code for the server on CodeSandbox.


In this example, when the user uploads a file, they will see progress as the file gets uploaded, and they will see the file's name and the file's size, which would be returned from the server.


A brief overview of what's happening in the above snippet is as follows:


  1. We create a simple form with file input and an upload button to submit the form. We also have a progress bar in the HTML which will be updated when the file is uploaded.


  2. Then in the JavaScript code, we attach an event listener to the form for the submit action in which we first get the selected file and initialize the progress bar to its initial state.


  3. After that, we create an XHR object which will be used to make an HTTP request to upload the file to our server. For the xhr object we:


    1. We initialize a new post request to the /upload endpoint via the open method.


    2. We then add the progress event listener to the xhr object and update the progress bar in the listener based on event.loaded


    3. We also add another event listener for onreadystatechange in which, once the request is finished, we check for 200 status and update the status div with the value returned from the server.


    4. Lastly, we create a new FormData object which provides a way to construct a set of key/value pairs representing form fields and their values.


      It uses the same format a form would use if the encoding type were set to "multipart/form-data"; this type also allows up to upload file data.


      We then add the file added by the user to the file key in the formData object, and then send the request with the formData via send method on the xhr object.


On the server, we use the multer package to save the file uploaded by the user to the file system and return the file name and size. The server code is out of the scope of this article, but if you're curious, you can check it out in the same sandbox linked above in the app.js file.

Using Third-Party Services for Managing File Uploads

As we saw in the last section, uploading files to a remote server is more complicated than making regular API requests to the server, and that was just for the frontend.


A lot more goes into building a robust file upload system from scratch, which can be time-consuming and costly, involving server infrastructure, storage, bandwidth, security measures, handling all the edge cases, and ensuring scalability and reliability.


This is why it is recommended to use cloud-based file storage services like Amazon S3, which offers an API for developers to integrate file upload and storage capabilities into their applications.


You can also use a full-fledged solution with Filestack or Cloudinary, which, apart from providing a managed file storage infrastructure, also provides you with many other features like interactive file upload widgets that allows your users to upload files from a variety of sources, provide CDN to ensure fast and reliable global access to your assets, perform transformations on assets like images giving the browser the ability to decide what kind of images to request based on viewport information and user-defined breakpoints, and many more features.


Let's take a quick look at an example of how you can integrate the Filestack File Picker widget into your application:


<!DOCTYPE html>
<html>
  <head>
    <title>File Upload with FileStack</title>
    <script src="//static.filestackapi.com/filestack-js/3.x.x/filestack.min.js"></script>
    <meta charset="UTF-8" />
  </head>

  <body>
    Upload an image
    <button id="upload">upload</button>

    <script>
      const uploadButton = document.getElementById("upload");
      const client = filestack.init("ADD_YOUR_API_KEY");
      uploadButton.addEventListener("click", (event) => {
        const options = {
          accept: ["image/*"],
          onFileUploadFinished: () => {
            alert("Image uploaded Successfully!");
          }
        };

        client.picker(options).open();
      });
    </script>
  </body>
</html>


Check out the demo on CodeSandbox. You will need to create your own Filestack API key for the demo to upload the file.


This is all the code required to create a simple uploader that accepts images and uploads the image to Filestack's CDN.


When the user clicks on the upload button, Filestack presents them with an interactive widget that allows the user the ability to upload from multiple sources and also allows them to crop or rotate before uploading.


Screenshot of Filestack's File Picker


Filestack also provides you with a wide variety of options that let you customize almost every aspect of the File Picker, including the ability to customize file sources, set the minimum and maximum number of files that can be uploaded, set the required dimensions of the image to be uploaded, etc.

Conclusion

In conclusion, this article has provided an in-depth exploration of file handling in JavaScript on the web. We delved into the intricacies of working with files, starting with the Blob object and understanding how to accept files from users through file input.


We also looked at a basic example of uploading files to a server and displaying the progress as the file gets uploaded to the user.


Additionally, we discussed the limitations of creating a file management system from scratch and saw why you should probably consider using a cloud-based storage solution like Filestack to streamline file handling in your applications.