Deno and the file system
Deno and the file system
Many applications interact with the host file system in some way. For this we have several functions in Deno that are provided to us.
In order to use these in our applications, we need to remember to use the
runtime flags, such as --allow-read
and/or --allow-write
.
In the following chapter we will look at how to interact with the filesystem,
work with directories or manage files. At the end of the chapter we will create
an application, similar to ls
, which can give us information about
directories.
Interact with Directories
Checking the contents of a folder
If we want our application to know what is in a folder, Deno offers us an easy
way to find out. Deno.readDir()
or synchronously Deno.readDirSync()
. This
method, like many of the following, is similar to NodeJS.
Creating new directories
As we are used to from the terminal/console, we can create new directories with
Deno. The command is Deno.mkdir()
.
We have to keep in mind that the system does not check if the directory already
exists when the command is being executed. If it does, Deno.mkdir("/my/path")
will return an error. So we need to check that the directory exists first. We
can do this with Deno.stat("/my/path")
. Let’s look at this in an example.
const path = "my-dir";
try {
const stat = await Deno.stat(path);
if (!stat.isDirectory) {
throw new Error(`Path exists but is not a directory: ${path}`);
}
console.log(`Path exists and is a directory: ${path}`);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log(`Path does not exist. We can create it: ${path}`);
await Deno.mkdir(path);
}
}
We can also create subfolders in one step. To do this, we pass a second
parameter to the Deno.mkdir()
function’s options with { recursive: true }
.
const path = "my-dir/sub-dir";
try {
const stat = await Deno.stat(path);
if (!stat.isDirectory) {
throw new Error(`Path exists but is not a directory: ${path}`);
}
console.log(`Path exists and is a directory: ${path}`);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log(`Path does not exist. We can create it: ${path}`);
await Deno.mkdir(path, { recursive: true });
}
}
Walk directories
For example, if we want to build an application that displays the file system
and its contents, we have the option of traversing the file system from a
starting point. This method is provided by the @std/fs
module from the
standard library. Using an additional function from the @std/path
library, we
can create a kind of tree
application.
import { walk } from "jsr:@std/fs/walk";
import { relative, SEPARATOR } from "jsr:@std/path";
const path = ".";
for await (const entry of walk(path, { includeDirs: true })) {
const relativePath = relative(path, entry.path);
const depth = relativePath.split(SEPARATOR).length - 1;
const indent = "-->".repeat(depth);
const prefix = entry.isDirectory ? "📁 " : "📄 ";
console.log(`${prefix}${indent} ${entry.name}`);
}
An example output would be the following, which shows my test project:
➜ deno run -A walk.ts
📁 .
📁 .zed
📄 --> settings.json
📄 main.ts
📄 deno.json
📄 walk.ts
📄 deno.lock
📄 .env
📄 flags.ts
📁 my-dir
📁 --> sub-dir
📋 Further Documentation
Creating, reading and deleting files
Now we have looked at directories, interacted with them and created them. Of
course, not only directories can be created, but files of all kinds. There are
several methods in the Deno namespace and in the @std/fs
library that we can
use to do this.
The most important ones are described below.
Creating a new file
To create a new file, we can differentiate between a text file and a general file.
Creating text files
Deno offers us a simpler method for the former. We can use
Deno.writeTextFile()
to pass strings directly as input to the text file. We
have an asynchronous promise-based method on the one hand, and a synchronous
variant on the other. We have to wait for the former with an `await’, but the
following code can continue to run and will not block the application.
// async
await Deno.writeTextFile("hello.txt", "Hello, Deno!");
// sync
Deno.writeTextFileSync("hello.txt", "Hello, Deno!");
First we have the filename we want to assign. Then we can pass any string to be written to the text file. If the file already exists, it will be overwritten.
Of course, we don’t always want to create a new file and overwrite existing data. To do this, we can use the method options (3rd parameter) to append our text to a file.
await Deno.writeTextFile("hello.txt", "Hello, Deno!", { append: true });
Creating arbitrary files
It is possible to use the Deno.create()
method to create an empty file and
optionally write bytes to it afterwards. Deno will then return a Deno.FsFile
object that we can work with (read, write, truncate, etc.).
const f = await Deno.create("hello.txt");
const stats = await f.stat();
console.log(stats.isFile); // Result: true
To write bytes to this file, we need a Writer (and a TextEncoder if we want to write text). Let’s have a look at what this looks like in code.
const file = await Deno.create("hello.txt");
// check the file size
const statsBefore = await file.stat();
console.log(statsBefore.size); // Result: 0
const writer = file.writable.getWriter();
await writer.write(new TextEncoder().encode("Hello Deno!"));
// check the size now
const statsAfter = await file.stat();
console.log(statsAfter.size); // Result: 11
// close the file so we don't leak
writer.close(); // or f.close() - writer.close() also closes the file
A simple workflow is provided by Deno.writeFile()
. This allows us to
significantly shorten the previous code.
await Deno.writeFile(
"hello.txt",
new TextEncoder().encode("Hello Deno!"),
);
const stats = await Deno.stat("hello.txt");
console.log(stats.size); // Result: 11
Reading the contents of a file
When we want to read a file, we can differentiate between a text file and a general file, just like when we create a file. For text files, there is a method that returns a string directly. If we want to read the file we created earlier, we can do this as follows.
const content = await Deno.readTextFile("hello.txt");
console.log(content); // Hello Deno!
However, if we want to load an arbitrary file into memory, we can use the
Deno.readFile()
method. This will return a byte array that we can work with.
const bytes = await Deno.readFile("hello.txt");
console.log(new TextDecoder().decode(bytes));
We don’t have to worry about closing them with either method. Deno does that for us.
As with Deno.create()
, we can open files to work with. For this we have the
Deno.open()
method. This will open the file and we can then work with it. Be
it reading or writing. As with Deno.create()
we get a Deno.FsFile
object.
Removing files or directories
Deleting files and directories is simpler. Deno does not differentiate between
the two types. With Deno.remove()
or Deno.removeSync()
we can remove both
types.
// remove a file
await Deno.remove("hello.txt");
// remove an empty directory
await Deno.remove("my-dir");
// remove a non-empty directory
await Deno.remove("my-dir", { recursive: true });
When we try to delete a non-empty directory, we get an error message from Deno
error: Uncaught (in promise) error: Directory not empty (os error 66): remove "my-dir"
.
So we need to set the { recursive: true }
option deliberately to remove
everything in that directory.
Renaming and moving files
Sometimes we just want to move or rename files. For this we have the
Deno.rename()
function, which behaves similarly to the mv
we may know from
the terminal. The function is quickly explained. The first parameter is the
current location and the second is the future location.
await Deno.rename("hello.txt", "./public/hello.txt");
It is important to note that the source location must be readable and the destination location must be writable for our application.
Temporary files
Temporary files are great for large amounts of data that you don’t want to keep
in memory. If they are to be used later in the program, we can use them to store
data temporarily. Or, if you want to send them somewhere else, you can simply
remove them at the end of the program. By default, these files are created in
the operating system’s temporary folder (such as /tmp
or /var/folders
). To
get the current temporary directory, we can use one of the following two methods
(preferably the first)
// method 1 via NodeJS compatibility layer
import { tmpdir } from "node:os";
const tempPath = tmpdir();
console.log(tempPath);
// method 2 using Deno environment variables
const tempDir = Deno.env.get("TMPDIR") ||
Deno.env.get("TMP") ||
Deno.env.get("TEMP");
console.log(tempDir);
For the creation of temporary files or directories we then have the functions
Deno.makeTempDir()
and Deno.makeTempFile()
.
const tempPath = await Deno.makeTempDir();
console.log(tempPath);
const tempFile = await Deno.makeTempFile();
console.log(tempFile);
Both functions return the path to the directory or file created, so we can interact with it.
The options that are optional as parameters help us to better identify the
created files, as they have gibberish looking names. For example
/var/folders/j_/g0c8_fyd0zq3vg76xr8xcyhc0000gn/T/fc8e227ff388a6c2
. To make the
files or directories more identifiable, we can use either {prefix: ""}
or
{suffix: ""}
as an option (or both).
With the following code we get results like
/var/folders/j_/g0c8_fyd0zq3vg76xr8xcyhc0000gn/T/temp_f0bba33b51e5bb67.txt
.
const tempFile = await Deno.makeTempFile({ prefix: "temp_", suffix: ".txt" });
console.log(tempFile);
That way we know that it’s probably a text file we’re working with. This makes it easier to find and work with.
📋 Further Documentation
- Writing files - Deno by Example
- Reading files - Deno by Example
- Deleting files - Deno by Example
- Moving/Renaming files - Deno by Example
- Temporary files & directories - Deno by Example
Manage files (permissions, directories, statistics)
See statistics about files and directories
We can use Deno functions to get all sorts of information about our environment.
For example, we can get statistics about files and directories. The
Deno.stat()
function is available for this purpose.
In the following we will look at a slimmed down implementation of the terminal
application ls
. We want to give the user information about the current
directory. The output should return the available files, with optional CLI
arguments for file size and last modified date.
In the following case, we create a file called ls.ts
and insert the following
code.
import { parseArgs } from "jsr:@std/cli/parse-args";
import { ensureDir } from "jsr:@std/fs";
interface FileInfo {
name: string;
size: number;
mtime: Date | null;
isDirectory: boolean;
isFile: boolean;
isSymlink: boolean;
}
interface Options {
all: boolean;
long: boolean;
human: boolean;
}
/**
* Convert a file size in bytes to a human-readable string.
* @param sizeBytes The size in bytes.
* @param human Whether to use human-readable units.
* @returns The size as a string.
*/
function convertSize(sizeBytes: number, human = false): string {
if (!human) return sizeBytes.toString();
const units = ["B", "K", "M", "G", "T"];
let size = sizeBytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)}${units[unitIndex]}`;
}
/**
* Get file information for a directory entry.
* @param path The path to the directory.
* @param entry The directory entry.
* @returns The file information.
*/
async function getFileInfo(
path: string,
entry: Deno.DirEntry,
): Promise<FileInfo> {
const fullPath = `${path}/${entry.name}`;
const stat = await Deno.stat(fullPath);
return {
name: entry.name,
size: stat.size,
mtime: stat.mtime,
isDirectory: entry.isDirectory,
isFile: entry.isFile,
isSymlink: entry.isSymlink,
};
}
/**
* List the contents of a directory.
* @param path The path to the directory.
* @param options The options to use.
*/
async function listDirectory(path: string, options: Options) {
const entries: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(path)) {
if (!options.all && entry.name.startsWith(".")) continue;
entries.push(entry);
}
// Sort entries
entries.sort((a, b) => a.name.localeCompare(b.name));
if (options.long) {
// Add . and .. if showing all
if (options.all) {
const currentDir = await Deno.stat(".");
const parentDir = await Deno.stat("..");
console.log(formatLongEntry({
name: ".",
size: currentDir.size,
mtime: currentDir.mtime,
isDirectory: true,
isFile: false,
isSymlink: false,
}, options));
console.log(formatLongEntry({
name: "..",
size: parentDir.size,
mtime: parentDir.mtime,
isDirectory: true,
isFile: false,
isSymlink: false,
}, options));
}
// Process and display each entry
for (const entry of entries) {
const info = await getFileInfo(path, entry);
console.log(formatLongEntry(info, options));
}
return;
}
// Simple listing
for (const entry of entries) {
console.log(entry.name);
}
return;
}
/**
* Format a file information object for a long listing.
* @param info The file information.
* @param options The options to use.
* @returns The formatted string.
*/
function formatLongEntry(info: FileInfo, options: Options): string {
const mtime = info.mtime
? info.mtime.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "???";
const size = convertSize(info.size, options.human);
return `${size.padStart(6)} ${mtime} ${info.name}`;
}
async function main() {
const args = parseArgs(Deno.args, {
boolean: ["a", "l", "h"],
alias: {
a: "all",
l: "long",
h: "human",
},
});
const options: Options = {
all: args.a || false,
long: args.l || false,
human: args.h || false,
};
const path = args._[0]?.toString() || ".";
try {
await ensureDir(path);
await listDirectory(path, options);
} catch (error) {
console.error(`Error: ${error.message}`);
Deno.exit(1);
}
}
if (import.meta.main) {
main();
}
We can run this application with
deno run --allow-read ls.ts <dir> <parameter>
. If we run it without specifying
any directories or parameters, we will get the information from the current
directory.
➜ deno run --allow-read ls.ts
deno.json
deno.lock
ls.ts
It gets interesting when we use one (or more) of our parameters. We have the
parameters -a
, -l
and -h
. The last one works in combination with -l
.
When used, we get the following output from our application.
➜ deno run --allow-read ls.ts -alh
160.0B Nov 17, 03:35 PM .
96.0B Oct 16, 05:37 PM ..
118.0B Nov 17, 03:36 PM deno.json
675.0B Nov 17, 03:36 PM deno.lock
3.7K Nov 17, 03:37 PM ls.ts
As a result, we have built our own ls
application in a simple way.
📋 Further Documentation
Concluding “Deno and the file system”
In this chapter we learned about Deno’s file system operations. We covered how
to interact with directories, including checking their contents, creating new
ones, and traversing through them using the walk
function. We then explored
file operations such as creating, reading, and deleting files, as well as
working with temporary files. The chapter demonstrated both synchronous and
asynchronous methods, showing how to handle file permissions and statistics.
Throughout the examples, we saw how Deno provides a secure approach to file
system operations by requiring explicit permissions through runtime flags like
--allow-read
and --allow-write
. We concluded with a practical implementation
of an ls
-like command-line tool, which brought together many of the concepts
we learned.