Reading and Writing Files/Directories in NodeJS Using the FS Core Module
Yesterday, I answered my own SUPER-NOOB questions about servers and NodeJS. Today, I want to have a look at how we can use a core module within NodeJS which allows us to interact with the file system. This core module is called fs
(file system).
Let's have a look at some of the simplest operations we can achieve using fs
.
A note about asynchronous code
If you aren't already familiar with Promises and the concept of asynchronous code, it is probably best to use the 'Sync' versions of all of the following methods. I won't be covering them here, because they're not what I'm learning, but you can check the NodeJS documentation for more information.
If you kinda get the idea of Promises and/or asynchronous callbacks, then you will probably be fine to get through this.
Asynchronous callbacks in NodeJS
Each of the following methods follows a similar pattern:
- The method/function itself, e.g.
fs.readFile(
- The file or folder it will be working with (aka the path), e.g.
'./file.txt',
- Additional options, e.g.
{flag: "a"},
- A callback function e.g.
(err) => console.log(err ? err : "success"))
Since all of the following methods are asynchronous, the callback function is what will run after the method is complete. It usually takes either one or two parameters, and in all cases listed here, the first parameter is the error message if a problem is encountered.
Now that we've covered the basics, let's have a look at how we can make a new directory.
Setting up a path variable
For the sake of my examples below, I put some nice little code at the top of my file which looks like this:
const fs = require("fs");
const path = require("path");
let currentPath = path.dirname(__filename);
The first two lines import the core module native to NodeJS which we need, and then the third line accesses the file we're in (a simple index.js file) and then pulls out the path into its directory using path.dirname
as the method, and the global variable __filename
.
By creating the currentPath
variable, I can more easily test and play with the following methods. If you don't want to do this or something similar, you can also manually enter the path into the directory in which you want to work. Personally, I feel this is easier.
Now, it's important to note that we don't always need to use the current path/directory in our methods. We could just use ./
or similar, however as I see it, in future we are likely to be needing to work with other paths outside of our source code, so I assume (perhaps wrongly?) that using the full path is a better habit to build. I'd be interested to hear what more experienced developers think about this in the comments!
Using fs.mkdir
to Create a Directory
The following command simply creates a new directory called testFolder
inside our current folder. Using a template literal i.e. `a string with backticks` we can insert our currentPath
into our first argument.
fs.mkdir(`${currentPath}/testFolder`, (err) => {
if (err) throw err;
});
Using fs.readdir
to Check the Contents of a Directory
You may be familiar with the ls
command in the Terminal. This is a similar command, however rather than providing us with a CLI read-out of the files, it returns an array of file and folder names.
fs.readdir(currentPath, (err, files) => {
if (err) throw err;
console.log(files);
});
When I ran this in my test file, this is what I got back:
[ 'index.js', 'test.txt', 'testDir2', 'testDir3', 'testsDir3' ]
Additionally, there is a way to get access to what type of file is in your directory. Here's a neat little function I came up with:
fs.readdir(currentPath, { withFileTypes: true }, (err, files) => {
if (err) throw err;
files.forEach((entry) => {
console.log(`${entry.name}, ${entry.isDirectory() ? "directory" : "file"}`);
});
});
This will allow me to see in my console, whether each item is a directory or a file, using another inbuilt method in Node (I am starting to love all these in-builts!) called isDirectory()
which comes back on file listings when the withFileTypes: true
object is passed in as an optional second argument.
So what do we get back?
index.js, file
test.txt, file
testDir2, directory
testDir3, directory
testsDir3, directory
Using readFile
to Look at File Contents
Let's say we want to look inside the test.txt
file and see what it says. Unfortunately, data from this file is going to come in encoded. Let me show you what I mean:
fs.readFile(`${currentPath}/textInfo.txt`, (err,data) => {
if (err) throw err
}
Here's what we get back
<Buffer 54 68 69 73 20 66 69 6c 65 20 69 73 20 62 79 20 41 6e 6e 61 20 4a 20 4d 63 44 6f 75 67 61 6c 6c 21 21 21>
Uhhh... OK. Well, that's not normal, readable text. WHAT DOES IT MEAN?
Luckily, we can specify what format to use to decode/parse this information. In the case of simple text, utf-8, which we see entered here as a second parameter in string format.
fs.readFile(`${currentPath}/textInfo.txt`, 'utf8', (err,data) => {
if (err) {
console.error("ERROR: File reading did not work. Error code " + err)
} else {
console.log("SUCCESS! Here is your data: " + data)
})
Now what do we get??
This file is by Anna J McDougall!!!
Whew, that makes a lot more sense.
Using writeFile
to Create a New File or Append Text
Now that you're familiar with the pattern of these commands, let's have a look at a simple example where we create or overwrite a text file:
const newText = "Here is some new text!"
fs.writeFile(`${currentPath}/textInfo.txt`, content, (err) => {
if (err) throw (err)
})
Great! We now have a file called textInfo.txt
which has the text "Here is some new text!" within it. Let's try to add some MORE text!
const newText2 = "\nI'm so glad we're adding more text";
fs.writeFile(`${currentPath}/textInfo.txt`, newText2, (err) => {
if (err) throw err;
});
Good work! ...Wait, that's not right...
Where did our first text go? D'oh! That's right! fs.writeFile
overwrites existing file contents! So how can we just add some more text onto the end of our original instead? Using the a
flag.
const newText2 = "\nI'm so glad we're adding more text";
fs.writeFile(`${currentPath}/textInfo.txt`, newText2, {flag: "a"}, (err) => {
if (err) throw err;
});
Aha! Well that looks much better:
Using fs.stat
to Check Your File Details
Last but not least, let's have a little peek in at our file to see what its details/stats are. Here's a fun little method:
fs.stat(`${currentPath}/textInfo.txt`, (err, stats) => {
if (err) throw(err)
console.log(stats)
}
This brings us back the following information:
Stats {
dev: 647735127,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 44754521297123880,
size: 0,
blocks: 0,
atimeMs: 1609859928899.2424,
mtimeMs: 1609859928899.2424,
ctimeMs: 1609859928899.2424,
birthtimeMs: 1609859583171.8276,
atime: 2021-01-05T15:18:48.899Z,
mtime: 2021-01-05T15:18:48.899Z,
ctime: 2021-01-05T15:18:48.899Z,
birthtime: 2021-01-05T15:13:03.172Z
}
Wonderful! Now we have a whole heap of details about our text file. I'm sure one day we'll be able to understand and use this information somehow!