4 - File.io service in Vanilla JS

Real file.io service

File.io is a popular file service for hosting and serving ephemeral files. The home page looks like this:

A fairly simple home page with an option to upload files. While file.io allows multi-file & folder uploads, we'll stick to a single file. Again, we don't want to increase scope too much and make it overwhelming.

The single upload file button opens up a dialog to choose files:

Once upload is complete, we get a unique URL for the file and a QR code that can be shared.

That's all about the upload part. Coming to the download part. We can use the file link anytime to download the file (exactly once). If the link is valid, the file details are shown with an option to download it. As soon as the file gets downloaded, the server removes the file.

If the link is opened next time, we'll get an error:

We're going to go over a very similar service that has been written using vanilla JS & express server in the backend.

File.io service in vanilla JS

The code for the file.io service in vanilla JS is available in a GitHub repo. The repo can be cloned using the following command:

$ git clone https://github.com/mayankchoubey/learn-react-by-example-file-io-app.git

Once the repo is cloned, go to the folder vanilla-app and start the server:

$ cd vanilla-app
$ npm start
vanilla@1.0.0 start node --watch server.mjs
(node:38294) ExperimentalWarning: Watch mode is an experimental feature and might change at any time (Use node --trace-warnings ... to show where the warning was created) File.io vanilla app listening on port 3000

Once the server is up, open the browser and go to http://localhost:3000:

The interface is pretty simple, just like the real file.io service (minus the ads, of course). There is a single upload button that can be used to upload any file.

Pretty good for our simple app. Now, let's open the link provided to download the file. When the link is opened, it'll show the file details and a download button. As soon as the download button is used, the file will be removed from the server. The page in the back will refresh to 'file doesn't exists' as soon as download button is used.

Again, pretty good for our simple vanilla app. Now, let's dig a bit deep in the code for this fake file.io service. The folder structure is very simple:

Server code

The server code is present in a single server.mjs file. This contains the express app, static file server, file upload/download APIs & encryption/decryption logic. Everything in one file. The only reason to put everything in one file is tha,t this book is about learning React, not server-side programming. A single file keeps it simple for us. One thing to note that, the uploaded file is encrypted before writing to disk (just like file.io). The same file is decrypted & removed before it gets served. This make our file.io service very similar to the real one.

server.mjs

import express from "express";
import multer from "multer";
import crypto from "crypto";
import { nanoid } from "nanoid";
import { readFile, stat, unlink, writeFile } from "fs/promises";

const storage = multer.memoryStorage();
const app = express();
const upload = multer({ storage });

const port = 3000;
const algorithm = "aes-256-ctr";
const uploadPath = process.cwd() + "/uploads";

let key = "file-encryption-key";
key = crypto.createHash("sha256").update(key).digest("base64").substr(0, 32);
const iv = crypto.randomBytes(16);

app.use(express.static("public"));

app.get("/", (req, res) => {
  res.redirect("upload.html");
});

app.put("/api/files", upload.single("uploadedFile"), async (req, res) => {
  const fileId = nanoid(6);
  const fileName = `${uploadPath}/${fileId}`;
  await writeFile(fileName, encrypt(req.file.buffer));
  const metaFileName = `${fileName}.meta`;
  await writeFile(
    metaFileName,
    JSON.stringify({
      fileName: req.file.originalname,
      fileType: req.file.mimetype,
      fileSize: req.file.size,
    }),
  );
  res.json({ fileUrl: `http://localhost:${port}/${fileId}` });
});

app.get("/:fileId", (req, res) => {
  res.redirect(`download.html?fileId=${req.params.fileId}`);
});

app.get("/api/files/:fileId/meta", async (req, res) => {
  const fileId = req.params.fileId;
  const fileName = `${uploadPath}/${fileId}.meta`;
  try {
    const fileMeta = JSON.parse(await readFile(fileName, "utf-8"));
    res.json(fileMeta);
  } catch (e) {
    res.status(404).send();
  }
});

app.get("/api/files/:fileId", async (req, res) => {
  const fileId = req.params.fileId;
  const fileName = `${uploadPath}/${fileId}`;
  try {
    if (await stat(fileName)) {
      const encFileBuf = await readFile(fileName);
      const decFileBuf = decrypt(encFileBuf);
      await writeFile(fileName, decFileBuf);
      res.sendFile(fileName);
      res.on("finish", async () => {
        await unlink(fileName);
        await unlink(`${fileName}.meta`);
      });
    }
  } catch (e) {
    console.log(e);
    res.status(404).send();
  }
});

app.listen(port, () => {
  console.log(`File.io vanilla app listening on port ${port}`);
});

const encrypt = (buffer) => {
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  const result = Buffer.concat([cipher.update(buffer), cipher.final()]);
  return result;
};

function decrypt(buffer) {
  let encryptedBuffer = Buffer.from(buffer);
  let decipher = crypto.createDecipheriv(algorithm, key, iv);
  let decrypted = decipher.update(encryptedBuffer);
  decrypted = Buffer.concat([decrypted, decipher.final()]);
  return decrypted;
}

For the UI side code, there are two HTML files, three JS files, and one CSS file. For helping with the UI work, we've used bootstrap, QR generator and sweet alert.

upload.html

This contains everything that you see on the upload page.

<!DOCTYPE html>
<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
    <link rel="stylesheet" href="css/app.css">

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs/qrcode.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    <script src="js/utils.js"></script>
    <script src="js/upload.js"></script> 
</head>
<body onload="onOpen()">

    <nav class="navbar navbar-expand-md navbar-dark bg-secondary sticky-top">
        <span class="display-6 text-white">File.io</span>
        <button class="navbar-toggler" type="button" data-toggle="collapse" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
  
        <div class="collapse navbar-collapse align-middle">
          <ul class="navbar-nav">
            <li class="nav-item active text-white tech-sub-heading">(Vanilla JS)
            </li>
          </ul>
        </div>
    </nav>

    <main role="main" class="container mt-5">
        <div class="text-center">
            <h3 id="fileHeading">Upload file</h3>
            <br>
            <input type="file" id="uploadBtn" class="uploadBtn" name="uploadedFile" onchange="sendFile(this.files)" hidden/>
            <input type="button" id="uploadBtnTemp" class="btn btn-primary uploadBtnTemp" value="Choose a file" onclick="uploadFile()"/>
            <br>
            <div class="fileDetails mt-3" id="fileDetails">
                <div class="container-sm file-details-container bg-light p-3">
                    <div class="row justify-content-between">
                      <div class="col-sm">
                        File Name: <br><b><span id="fileName"></span></b>
                      </div>
                      <div class="col-sm">
                        File Size: <br><b><span id="fileSize"></span></b>
                      </div>
                    </div>
                </div>
                <div class="container-sm upload-result-container bg-light p-3">
                    <div class="row justify-content-between">
                      <div class="col-sm">
                        File URL: <a id="fileUrl"></a>
                      </div>
                      <div class="col-sm justify-content-md-left">
                        <div id="qrcodeCont"></div>
                      </div>
                    </div>
                </div>
            </div>
            <br>
            <a id="uploadAnother" href="upload.html">Upload another file</a>
        </div>
    </main>

    <footer class="footer text-center">
        <div class="container">
            <span class="text-muted">Fake file.io service built with vanilla JS</a></span>
        </div>
    </footer>

</body>
</html> 

upload.js

This file contains all the JS code for the upload functionality to work.

function onOpen() {
  hideElem("fileDetails");
  hideElem("uploadAnother");
}

function uploadFile() {
  document.getElementById("uploadBtn").click();
}

async function sendFile(files) {
  hideElem("uploadBtnTemp");
  const file = files[0];
  const body = new FormData();
  body.set("uploadedFile", file, file.name);
  const resp = await fetch("api/files", {
    method: "PUT",
    body,
  });
  if (resp.status !== 200) {
    Swal.fire({
      title: "Error!",
      text: "Unable to upload the file",
      icon: "error",
      confirmButtonText: "Ok",
    });
    return;
  }
  const json = await resp.json();
  setHTML("fileHeading", "File uploaded!");
  addClass("fileHeading", "text-success");
  showElem("fileDetails");
  setHTML("fileUrl", getLink(json.fileUrl));
  setHTML("fileName", file.name);
  clearHTML("qrcodeCont");
  new QRCode("qrcodeCont", {
    text: json.fileUrl,
    width: 128,
    height: 128,
    colorDark: "#000000",
    colorLight: "#ffffff",
  });
  setHTML("fileSize", getFileSize(file.size));
  showElem("uploadAnother");
}

In the upload.js file, we can see there is a lot of code around showing/hiding elements, adding/removing classes, setting/removing values from elements. This is pretty usual for a vanilla JS app. If the same app is written using jQuery, the only difference would be usage, and nothing else. All the additional helpers come from utils.js.

download.html

This contains everything that you see on the download page.

<!DOCTYPE html>
<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
    <link rel="stylesheet" href="css/app.css">
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
    <script src="js/utils.js"></script>
    <script src="js/download.js"></script> 
</head>
<body onload="onOpen()">

    <nav class="navbar navbar-expand-md navbar-dark bg-secondary sticky-top">
        <span class="display-6 text-white">File.io</span>
        <button class="navbar-toggler" type="button" data-toggle="collapse" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
  
        <div class="collapse navbar-collapse align-middle">
          <ul class="navbar-nav">
            <li class="nav-item active text-white tech-sub-heading">(Vanilla JS)
            </li>
          </ul>
        </div>
    </nav>

    <main role="main" class="container mt-5">
        <div class="text-center">
            <h3 id="fileHeading">Download file</h3>
            <br>
                <div class="container-sm file-error-container p-2 bg-danger text-white" id="fileError"></div>
                <div class="fileDetails mt-3">
                    <div class="container-sm file-details-container bg-light p-3" id="fileDetails">
                        <div class="row justify-content-between">
                        <div class="col-sm">
                            File Name: <br><b><span id="fileName"></span></b>
                        </div>
                        <div class="col-sm">
                            File Size: <br><b><span id="fileSize"></span></b>
                        </div>
                    </div>
                    <div class="container-sm download-container bg-light p-3">
                        <input type="button" id="downloadFile" class="btn btn-primary" value="Download file" onclick="downloadFile()"/>
                    </div>
                </div>
            </div>

            <br>
            <a href="upload.html">Upload another file</a>
        </div>
    </main>

    <footer class="footer text-center">
        <div class="container">
            <span class="text-muted">Fake file.io service built with vanilla JS</a></span>
        </div>
    </footer>
</body>
</html> 

download.js

This file contains all the JS code for download functionality to work.

function onOpen() {
  getFileDetails();
}

async function getFileDetails() {
  const qp = new URLSearchParams(window.location.search);
  const fileId = qp.get("fileId");
  if (!fileId) {
    fileNotFound();
  }
  const resp = await fetch(`api/files/${fileId}/meta`);
  if (resp.status === 200) {
    const json = await resp.json();
    fileFound(json);
  } else {
    fileNotFound();
  }
}

function fileNotFound() {
  hideElem("fileDetails");
  showElem("fileError");
  setHTML("fileError", "File doesn't exist");
  hideElem("downloadFile");
}

function fileFound(json) {
  showElem("fileName");
  showElem("fileSize");
  hideElem("fileError");
  setHTML("fileName", json.fileName);
  setHTML("fileSize", getFileSize(json.fileSize));
}

async function downloadFile() {
  const qp = new URLSearchParams(window.location.search);
  const fileId = qp.get("fileId");
  const resp = await fetch(`api/files/${fileId}`);
  const blobURL = URL.createObjectURL(await resp.blob());
  const a = document.createElement("a");
  a.href = blobURL;
  a.download = getHTML("fileName");
  a.style.display = "none";
  document.body.append(a);
  a.click();
  setTimeout(() => {
    URL.revokeObjectURL(blobURL);
    a.remove();
    getFileDetails();
  }, 1000);
}

The code download.js is very similar to the code in upload.js. There is a good amount of element visibility & content manipulation. There is even a piece of code to create the file download popup when the 'download file' button is pressed. This requires creating a dynamic element 'a' with download option.

utils.js

The utils.js file is common for upload and download. It contains utility function for manipulating the elements.

function getFileSize(size) {
  const sizeFormatter = new Intl.NumberFormat([], {
    style: "unit",
    unit: "byte",
    notation: "compact",
    unitDisplay: "narrow",
  });
  return sizeFormatter.format(size);
}

function hideElem(id) {
  document.getElementById(id).style.display = "none";
}

function showElem(id) {
  document.getElementById(id).style.display = "block";
}

function setHTML(id, val) {
  document.getElementById(id).innerHTML = val;
}

function getHTML(id) {
  return document.getElementById(id).innerHTML;
}

function clearHTML(id) {
  document.getElementById(id).innerHTML = "";
}

function getLink(link) {
  return `<a href="${link}" target="_">${link}</a>`;
}

function addClass(id, cl) {
  document.getElementById(id).classList.add(cl);
}

function removeClass(id, cl) {
  document.getElementById(id).classList.remove(cl);
}
 

app.css

All the additional styling is present here.

.footer {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 60px;
    line-height: 60px;
    background-color: #f5f5f5;
}

.button-with-icon {
    margin-left: 5px;
}

.tech-sub-heading {
    margin-left: 10px;
}

.upload-result-container {
  margin-top: 30px;
  width: 50%;
}

.download-container {
  margin-top: 30px;
}

.file-details-container {
  width: 40%;
}

.file-error-container {
  width: 20%;
}

--

That's all about the file.io vanilla app. The app code should look familiar to you. That's the basic premise of this book. You're coming from a vanilla JS or jQuery background and are interested in learning React.

Before moving ahead to the next section, you should take a pause here and do the following activities:

  • Clone the repo

  • Run the server and test the app some times

  • Go through the code in detail. Understand the HTML and JS code (every line).

Again, nothing in the vanilla app should give you any trouble. This is your day-to-day job after all.

Again, it's best to pause here and follow the exercise.

--

This is the conclusion of part 1 of this book. The next part covers the basics of React starting with setting up a local development environment.

Last updated