Node fs: Renaming Files

Overview:

Node.js features an handy filesystem module, called fs. The module provides synchronous, asynchronous, and new Promise functionality for many of its functions. This blog entry will focus on messing around with the file renaming functionality of the fs module through the module's various rename functions.

fs Functions for Renaming Files

The Node.js f module api can be found here.
It includes 3 functions for renaming files. They all do more or less he same thing, but they differ in how those actions are run synchronously or asynchronously.
The easiest functions to understand is the synchronous one, however it is also the most inefficient to use:

fs.renameSync(oldPath, newPath)

This functions just takes the location/path of the file to be renamed (which can be expresses as a string literal like "./renameMe.txt"), and the path for the new file [does attempting to move to a nonexistent folder fail?]. The issue with this approach is that renaming files synchronously slows down the program significantly since working with files on a storage device is significantly slower than working with data in program memory or the cpu cache. This means that faster program functions need to wait for the slow file operation to finish execution before the low of the program can continue.
You can see the code for a program that runs the fs.renameSync function below: This function is run within a simple for loop that prints its current iteration number so that you can see when the rename logic is run relative to the surrounding program logic.
Notice how the renaming functionality halts the execution of the for loop when it is being run synchronously (the counter stops at 4 while the file rename operation is being run)
 

fs.rename(oldPath, newPath, callback)

The fs.rename() function is asynchronous. Like the syncheonous version, it takes the source and destination (old and new) file paths, hwever it also takes a callback function. The callback function is called after the rename function has finished executing. The official API documentation presents this snippet showing a callback function with arrow function (a shorthand way of writing a nameless function in JavaScript):

fs.rename('oldFile.txt', 'newFile.txt', (err) => {
  if (err) throw err;
  console.log('Rename complete!');
});

This arrow function can be rewritten as:
 
function(err){
  if (err) throw err;
  console.log('Rename complete!');
} 

The fs.rename() function will call the callback function that is passed to it. fs.rename() is coded so that it will pass an exception object to the callback function. If the exception object was not set (has a value like undefined because fs.rename() never threw an exception), the sample callback function will just print to the console that renaming was successful. If an error was thrown, the callback function just throws the exception again to let it propagate up to the function's enclosing scope.
Exceptions objects in JavaScript will normally have an associated error code and error message. You can see in my implementation below that I've chosen to display these messages as part of the program's output. While the callback function for fs.rename() is passed exceptions that were caught by fs.rename() fs.renameSyc() will throw errors that would need to be handled by a more traditional try-catch block.
The code for using the fs.rename() function is listed below:
Here's the output of a successful run. Notice how the for loop is able to continue to run while the file renaming is happening, and the loop finishes execution before the file operation finishes. No more interruptions from slow file system operations.

Here's what we get if we don't have a file that matches the path in the oldPath argument:

fsPromises.rename(oldPath, newPath)

Promises are a new feature to Node.js and they work differently than the old async functions that take callbacks as arguments. A naive implementation of a promise actually helps to illUstrate this difference:

And here's the output:
Notice how the output of this program makes the promise code appear to behave like the synchronous version; we see the output indicating success of the file operation appear when i==4.
This is because fs.promises.rename() both returns a promise object--which is an object that will be populated later with the results of an async operation--at the same time that it launches the asyc operation that will populate the promise object. The Mozilla Developer's Network has a decently written article on working with JavaScript promises.

Here's an implementation that actually works. Note how callbacks are created independently of the promise object (especially since the promise object is created within the fs.promises.rename() function). These callbacks are then passed to the promise's .then() function. The first argument passed to .then() is the callback to be run on successful execution of the logic that populates the promise with the desired result, and the second argument is a callback that is run on failure (an exception being thrown).

The output for this implementation displays a more desirable behaviour:

Improving on indicators of run times

A enclosing certain operations in a for loop that prints its iterations doesn't give a ton of information on the actual time it takes each version of the rename function to run. Luckily functions like console.time() and console.timeEnd() can be placed before and after functions to give a better idea of this (which was stated in this Stack Overflow post however the current MDN documentation for the alternative performance.now() function seems to have reduced accuracy for security reasons).

It was disappointing that the results of this were rather inconsistent... perhaps doing multiple runs, then averaging the results would be helpful, or using some performance diagnostic tools in Visual Studio would yield better results.

Comments

Popular posts from this blog

Tinkering with Chrome Headless to Handle Mic Input

Using Arrow keys to cycle through Mozilla Screenshots

A look back at Hacktoberfest