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)))
}
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",
}
}
}
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>
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>
Results
Here's the results, for root directory, and some directory in node_modules
:
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.
Top comments (0)