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.