Watch for file system changes in Deno

Mayank C
Tech Tonic

--

Purpose

Some of the applications could have a need to monitor and get notified of any changes to the files present in one or more locations in the local file system. The use cases might be watching a config file, reloading changed ES modules dynamically, get notified for new files, etc.

Deno’s core runtime comes with an async file watcher that has the ability to recursively watch multiple locations and notify users about any changes. The file watcher is generic to cover all the cases like:

  • create
  • modify
  • remove

The file watcher notifies about the above events for affected files/directories, however, it doesn’t give more details like what changed in the file. That’d be too much to expect!

Basic Usage

The file watcher is part of the core runtime, so there is no need to import anything.

The file watcher needs to be initialized with locations to monitor and the locations can’t be changed for that watcher. There could be multiple watchers running together. To initialize the file watcher, call Deno.watchFs with locations to monitor:

Deno.watchFs(paths, options);

All the given paths (one or more) are checked at this time. An error would be thrown if any of the paths don’t exist or is inaccessible.

Inputs

The first input is the list of paths which could either be:

  • string: to watch a single location
  • an array of strings: to watch multiple locations

The second input is optional, and at the time of writing, the only supported attribute is recursive that can have two values:

  • true: Monitor recursively (default)
  • false: Do not monitor recursively

Output

The output of watchFs is a FileWatcher object that provides AsyncIterator. The iterator could be looped upon infinitely (the most common use-case would be to never end the watching work).

const watcher=Deno.watchFs(paths);
for await (const event of watcher)
//process event if it's interesting

Events

There are three basic file system events that would be raised:

  • create: when a file is created (this includes temp files, swap files, etc.)
  • modify: when a file has been modified
  • remove: when a file has been removed

The structure of the event is quite simple:

  • kind: The type of event: create, modify, or remove
  • paths: The file paths that the event applies to

For each change, there would be an event. The events could get way too verbose, so it’s important to filter them before processing. For example, an application could choose to process only modify events for config file updates, or only create events for uploaded data files, etc.

Examples

Now that we’ve gone through the basics of watching, let’s go over some examples to understand them in detail.

First, let’s go over some error cases. This one is about giving an inexistent path.

// ../../config is a wrong pathconst watcher=Deno.watchFs(["/var/tmp/", "../../config"]);//error: Uncaught NotFound: No path was found. about ["../../config"]

This one is giving a path (/var/tmp) that is inaccessible (access is to /var/tmp1 only):

const watcher=Deno.watchFs("/var/tmp/");--deno run --allow-read=/var/tmp1 deno_file_watcher.ts//error: Uncaught PermissionDenied: read access to "/var/tmp/", run again with the --allow-read flag

Next, let’s go over some events for simple file operations like create and remove:

const watcher=Deno.watchFs("/var/tmp/");for await(const event of watcher)
event;
--touch /private/var/tmp/z.z//{ kind: "create", paths: [ "/private/var/tmp/z.z" ] }rm /private/var/tmp/z.z//{ kind: "remove", paths: [ "/private/var/tmp/z.z" ] }

It’s important to note that the events could get very verbose especially if temporary files are involved. Here is an example of using vim to create a file called z.1, writing abcd in it, and then saving it. There are a lot of events for this supposedly simple event:

vim /private/var/tmp/z.1
>>> write abcd and save it
--{ kind: "create", paths: [ "/private/var/tmp/.z.1.swx" ] }
{ kind: "remove", paths: [ "/private/var/tmp/.z.1.swx" ] }
{ kind: "create", paths: [ "/private/var/tmp/.z.1.swp" ] }
{ kind: "remove", paths: [ "/private/var/tmp/.z.1.swp" ] }
{ kind: "create", paths: [ "/private/var/tmp/.z.1.swp" ] }
{ kind: "create", paths: [ "/private/var/tmp/z.1" ] }
{ kind: "modify", paths: [ "/private/var/tmp/z.1" ] }
{ kind: "modify", paths: [ "/private/var/tmp/z.1" ] }
{ kind: "create", paths: [ "/private/var/tmp/.z.1.swp" ] }
{ kind: "remove", paths: [ "/private/var/tmp/.z.1.swp" ] }
{ kind: "modify", paths: [ "/private/var/tmp/.z.1.swp" ] }

It’d be best to ignore all the events with paths ending in .swp and .swx.

The same events are raised when directories are created:

cd /private/var/tmp
mkdir a b
{ kind: "create", paths: [ "/private/var/tmp/a" ] }
{ kind: "create", paths: [ "/private/var/tmp/b" ] }
--rm -fr a b{ kind: "remove", paths: [ "/private/var/tmp/a" ] }
{ kind: "remove", paths: [ "/private/var/tmp/b" ] }

Lastly, let’s see an example of monitoring multiple directories non-recursively:

const watcher=Deno.watchFs(["/var/tmp/", "../"], { recursive: false});for await(const event of watcher)
event;
--mkdir a//{ kind: "create", paths: [ "/private/var/tmp/a" ] }cd a
touch b
//No event raised as recurse is off--touch ./z//No event is raised as recurse if offtouch ../z//{ kind: "create", paths: [ "/Users/mayankc/Work/source/z" ] }rm ../z//{ kind: "remove", paths: [ "/Users/mayankc/Work/source/z" ] }

--

--