JavaScript

Async file or dir walker class by extending EventEmitter

A file walker can recursively visit all the files in a file tree. It’s a very useful technique to read the statistics of each file and directory (if accessible), for example we can read the stat of each file to determine its type (text, image or video etc.), permissions, extension, modify time, access time or create time.

We’ve already created a basic file walker using async and sycn techniques on following pages:

Following is the async file walker example, notice some lines are bold, we’ll convert this basic file walker into asynchronous event driven file walker by modifying these bold lines into events. For example, we’ll emit an error event instead of throwing an error, emit an file event when a file found and emit a dir event when a directory found.

//Basic asyn file walker

const fs = require('fs');
const path = require('path');

function readTree (entry) {
 fs.lstat(entry, (err,stat) => {
  if (err) throw err; 
  if (stat.isDirectory()){
   fs.readdir(entry, (err,files) => {
    if (err) throw err;
    files.forEach( file => {
     readTree(path.join(entry,file));
    });
   });
  }
  else if(stat.isFile()) {
   //file found
   console.log (entry);
  }
 });
}

readTree (path/of/dir)

We can easily convert these lines into events. In Node applications, event are created using the EventEmitter, see previous page ( The event loop (queue)) to learn more about events.

Creating a dir walker class by extending EventEmitter class

The FileWalker class is supposed to be an instance of EventEmitter that reports the state changes or useful events.

As we’re creating a Duplicate Files Finder app, think what actually we need to collect from the given paths while scanning them:

  •  Information of given path i.e. fs.lstat(entry, callback(err,stat))
  • If error then emit an ERROR event
  • If the given path type is dir then emit a DIR event
  • Then read the dir using readdir
  • If error while reading the dir then emit an ERROR event
  • If the given path type is file then emit a FILE event

Let’s create an even driven asynchronous file walker:

First we’ll include the required Node’s modules:

const path = require('path'),
fs = require('fs'),
{EventEmitter} = require('events');

Now, we’ll create a FileWalker class by extending the EventEmitter class and create the constructor method which accepts a parameter entry (the initial path of a directory to scan).

Then we’ll call the EventEmitter‘s constructor method using super() keyword. Note :  our app will not work if we do not call the parent constructor. Next, pass the entry to readTree method to recursively read the given path.

class FileWalker extends EventEmitter {
 
 //Constructor method
 constructor (entry){
  super();
  this.readTree(entry);
 }
 
 readTree(entry){
  ...
 }
}

The readTree method will read the stat of given path and emit the error event if found error(s).

...
readTree(entry){
 fs.lstat(entry, (err,stat) => {
  if (err){
   this.emit('error', err, entry, stat);
   return;
  }
 }
}
...

If no error, then we emit a file event if the given path is file .

...
readTree(entry){
 fs.lstat(entry, (err,stat) => {
  if (err){
   this.emit('error', err, entry, stat);
   return;
  }
  if(stat.isFile()) {
   this.emit('file',entry,stat);
  }
 }
}
...

If the path is directory we’ll emit a directory event  and then scan the whole directory with fs.readdir method. We’ll emit an error event if the directory not readable. Then we’ll iterate all the entries using forEach loop and pass the each element to readTree method to repeat the same procedure for every entry.

...
if(stat.isFile()) {
 this.emit('file',entry,stat);
}
else if(stat.isDirectory()){
 this.emit('dir',entry,stat);
 fs.readdir(entry, (err,files) => {
  if (err){
   this.emit('error',err,entry,stat);
   return;
  }
  files.forEach( file => {
   this.readTree(path.join(entry,file));
  });
 }
}
...

The complete code of asynchronous and event driven file walker:

//walker.js
const fs = require('fs'),
path = require('path'),
{EventEmitter} = require('events');

class FileWalker extends EventEmitter {

constructor (entry){
 super();
 this.readTree(entry);
}

readTree (entry) {
 entry = entry || this._entry;
 fs.lstat(entry, (err,stat) => {
  if (err){
   this.emit('error',err,entry,stat);
   return;
  }

  if (stat.isFile()){
   this.emit('file',entry,stat);
  }
  else if (stat.isDirectory()){
   this.emit('dir',entry,stat);
   fs.readdir(entry, (err,files) => {
    if (err){
     this.emit('error',err,entry,stat);
     return;
    }
    files.forEach( file => {
     this.readTree(path.join(entry,file));
    });
   });
  }
 });
}
}

//Creating a new instance
let walker = new FileWalker('a/dir/path');

//Listen to error event
walker.on('error', (error,entry,stat) => {
 console.log(error);
});

//Listen to file event
walker.on('file',(file,stat)=>{
 console.log(file);
});

//Listen to dir event
walker.on('dir',(dir,stat)=>{
 console.log(dir);
});

To use FileWalker class, we’ll create a new instance of it by providing a directory path, which we intend to scan. To listen an event we’ll use on method, for example, to listen an error event we’ll use walker.on('error',...).

Save the above file and execute it on shell / command prompt i.e. D:\BrainBell>node walker.js. I’d received the following output on my Windows 10 PC:

D:\BrainBell>node walker.js
C:\empty
C:\empty\abc
C:\empty\abc.txt
{ Error: EPERM: operation not permitted, lstat 'C:\empty\dummy'
 errno: -4048,
 code: 'EPERM',
 syscall: 'lstat',
 path: 'C:\empty\\dummy' }
C:\empty\folder
C:\empty\folder - Shortcut.lnk
C:\empty\lol.txt
C:\empty\abc\abc.txt
C:\empty\folder\abc.bmp
C:\empty\folder\ddd.rtf

In next tutorial, we’ll learn how to create a module? We’ll export our  FileWalker class as a module, so any app can independently use it using ‘require’ keyword.