DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on • Updated on

Electron Adventures: Episode 23: Display Information about Files

Let's improve our file manager. There's a lot of information we'd like to display. Let's start with just a few:

  • file size
  • last modified time
  • for symlink, where does it lead to

preload.js

This tiny change already requires restructuring the code a bit, as getting this information in node is - obviously async.

let { readdir } = require("fs/promises")

let directoryContents = async (path) => {
  let results = await readdir(path, {withFileTypes: true})
  return await Promise.all(results.map(entry => fileInfo(path, entry)))
}
Enter fullscreen mode Exit fullscreen mode

I'm not sure how node actually executes it. Pretty much every other language will run system calls one at a time, so we could do return results.map(entry => await fileInfo(path, entry)), but on an off chance that this actually runs in parallel I'm first constructing big list, then awaiting the whole thing.

Now the next part gets a bit awkward. Having a functions of a few lines in preload.js is fine, but this is getting big. We'd much rather put it into some backend code, which we can unit test without complexities of frontend testing. We will absolutely get to it soon.

let { stat, readlink } = require("fs/promises")

let fileInfo = async (basePath, entry) => {
  let {name} = entry
  let fullPath = path.join(basePath, name)
  let linkTarget = null
  let fileStat

  if (entry.isSymbolicLink()) {
    linkTarget = await readlink(fullPath)
  }

  // This most commonly happens with broken symlinks
  // but could also happen if the file is deleted
  // while we're checking it as race condition
  try {
    fileStat = await stat(fullPath)
  } catch {
    return {
      name,
      type: "broken",
      linkTarget,
    }
  }

  let {size, mtime} = fileStat

  if (fileStat.isDirectory()) {
    return {
      name,
      type: "directory",
      mtime,
      linkTarget,
    }
  } else if (fileStat.isFile()) {
    return {
      name,
      linkTarget,
      type: "file",
      size,
      mtime,
      linkTarget,
    }
  } else {
    return {
      name,
      type: "special",
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This should cover a lot of cases, such as:

  • file
  • symlink to a file
  • directory
  • symlink to a directory
  • error (file deleted while we're checking it)
  • symlink to an error (most likely symlink just points to non-existent file, very common)
  • special file (socket, fifo, device, etc)
  • symlink to a special file

Sounds like something we should unit test? We absolutely will do, just not yet!

index.html

One thing I forgot about. When you're serving HTML from just about any webserver, it tells the browser it's UTF8 in HTTP headers. As we're loading raw files, browsers default to some paleolithic encoding nobody's seen since before Y2K, and even Electron does that crazy thing. So we need to tell it that it's UTF8. Here's one of many ways to do so:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <link rel="stylesheet" href="/build/bundle.css">
    <script src="/build/bundle.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

App.svelte

And here's some very simple component for displaying that information in a grid format - name, type, size, last modified time. We can do a lot better, and we absolutely will.

<script>
  let directory = window.api.currentDirectory()
  $: filesPromise = window.api.directoryContents(directory)
  $: isRoot = (directory === "/")

  function navigate(path) {
    if (directory === "/") {
      directory = "/" + path
    } else {
      directory += "/" + path
    }
  }
  function navigateUp() {
    directory = directory.split("/").slice(0, -1).join("/") || "/"
  }
  function formatDate(d) {
    return d ? d.toDateString() : ""
  }
  function formatName(entry) {
    if (entry.linkTarget) {
      return `${entry.name}${entry.linkTarget}`
    } else {
      return entry.name
    }
  }
</script>

<h1>{directory}</h1>

{#await filesPromise}
{:then files}
  <div class="file-list">
    {#if !isRoot}
      <div><button on:click={() => navigateUp()}>..</button></div>
      <div></div>
      <div></div>
      <div></div>
    {/if}
    {#each files as entry}
      <div>
        {#if entry.type === "directory"}
          <button on:click={() => navigate(entry.name)}>
            {formatName(entry)}
          </button>
        {:else}
          {formatName(entry)}
        {/if}
      </div>
      <div>
        {entry.type}
        {entry.linkTarget ? " link" : ""}
      </div>
      <div>{entry.size ? entry.size : ""}</div>
      <div>{formatDate(entry.mtime)}</div>
    {/each}
  </div>
{/await}

<style>
  :global(body) {
    background-color: #444;
    color: #ccc;
  }
  .file-list {
    display: grid;
    grid-template-columns: 3fr 1fr 1fr 1fr;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Results

Here's the results, for root directory, and some directory in node_modules:

Episode 23 Screenshot A

Episode 23 Screenshot B

In the next episode, we'll extract some of that backend code into something we can unit test.

As usual, all the code for the episode is here.

Discussion (0)