One Approach To A Virtual Card Game With Firebase Realtime Database Part IV: Server Cleanup

Welcome back! This is part IV of a series I’m writing on my approach to creating a cribbage platform using Firebase Realtime database and an express server. In the last piece, I showed my approach to dynamically creating shareable urls using Node’s fs module and dynamic routing. In this piece, I will cover some cleanup from that process to ensure that the server’s file structure does not get too out of hand.

First I’ll cover how to delete the unique Firebase node after the game is finished. Next I’ll cover how to delete a file 24 hours after it’s been created.

Deleting the Firebase Nodes

In the picture above, you can see a number of dynamically generated database nodes that were created as I was testing the server’s routing logic. I can easily go in manually and delete them after they’re not needed, but since each gamekey is only valid for 24 hours there will eventually be hundreds or thousands of useless nodes cluttering up the database.

Luckily, the fix will be fairly simple to implement using the ‘beforeunload’ event in the client-side JavaScript (read more about that here). Essentially this event fires when the browser is about to unload the resources for a page. In this situation, we can use the event to delete the unique Firebase node created at the beginning of the game by setting its value to be an empty object.

window.addEventListener('beforeunload', (event) => } 
DeckReference.set({});
})

In the gif below, notice that when the cribbage page on the right is closed, the corresponding database node on the left turns red and is deleted.

Deleting the game files

Deleting the dynamically created ejs game file is slightly more troublesome. We want people to be able to share these unique urls so the corresponding files have to exist for an extended amount of time, but if none of them are ever deleted they’ll needlessly take up disk space. The way forward here is to sweep the directory of game files every 24 hours and delete any files that are more than 24 hours old. For this, Node’s fs module will come in handy once again.

To read the directory, there is a method ‘readdir’ which takes a directory path and a callback function. The callback is executed if the directory is successfully read (read more about ‘readdir’ here). The callback’s first argument is an error if needed, and the second is an array of the files in the directory:

const gameFilesPath = path.join(__dirname, '/views/gameFiles');
fs.readdir(gameFilesPath, (err, files) => {
// code to handle the files here
}

In this situation the code will use a ‘forEach’ to loop over the array of files and check their ages. To check a file’s age, there is another fs method ‘statSync’ which takes a file’s path as argument and returns its metadata. (Read more about ‘statSync’ here.) The metadata we want from each file is its date and time of creation so in the forEach we create a variable called birthTime and make it equal to the file’s birthtime property, which is returned in milliseconds:

files.forEach((file) => {
if(file.includes('gitignore')) {
console.log('ignore the gitignore!');
} else {
let today = new Date();
let birthTime = fs.statSync(gameFilesPath + '/' + file).birthtime;

Then we create a variable timeDiff which is the current time in milliseconds minus the birthtime of the file in milliseconds. If the timeDiff is more than 86400000, or the number of milliseconds in 24 hours, the file is deleted using the fs ‘unlink’ method (read more here). Finally, wrap all of that in a setInterval() with an interval of 86400000 and it’s ready. Here’s what that looks like:

// hold the path to the directory in a variable
const gameFilesPath = path.join(__dirname, '/views/gameFiles');
setInterval(()=> {
// read the files in the specified path
fs.readdir(gameFilesPath, (err, files) => {
// handle any errors
if (err) {
return console.log('Unable to scan directory' + err);
}
// loop over the files in the directory
files.forEach((file) => {
if(file.includes('gitignore')) {
console.log('ignore the gitignore!');
} else {
let today = new Date();
// get the creation time of the file
let birthTime = fs.statSync(gameFilesPath + '/' + file).birthtime;
// get the age of the file in milliseconds
let timeDiff = today.getTime() - birthTime;
// if the file is < 24hrs old, delete it
if(timeDiff > 86400000){
console.log('deleted game file' + file);
fs.unlink(gameFilesPath + '/' + file, (err) => {
if (err) {console.log(err);}
});
} else {
console.log(file + ' is < 24hrs old')
}
}
})
})
}, 86400000);

Handling 404s

Great! It works, but now what if someone goes to a url that has expired? What if someone mistypes their gameKey? For that, we’ll add another ejs file in the views folder called ‘game-not-found.ejs’. It will be very similar to the intro page, but will just have a small message at the top explaining that the game could not be found and would the user like to join another?

We now need a function that checks if a file with the specified gamekey exists in the directory. We put that function into the existing middleware, and structure it so that if the code cannot find the right gamefile, it redirects the request to “/game-not-found”.

app.get('/game/:url', (req, res, next) => {

// get the gamekey from the request
let url = req.url.substr(6);

fs.readdir(gameFilesPath, (err, files) => {
if (err) {
console.log(err);
}
if (files.toString().includes(url)) {
res.render(`./gameFiles/game_${url}`, {
gameNum: url
});
} else {
res.redirect('/game-not-found');
}
})

Now, every 24 hours, the server will check its gamefiles directory and delete any files that are more than 24 hours old. This, coupled with the automatic deletion of the unneeded Firebase nodes, will keep the app clean of unneeded data.

Conclusion

Thanks for reading! In the next installment I will go over debugging and other finishing touches. After that, I’ll be refactoring this project into React, which will be a completely new series, so stay tuned!

I’m a web developer focusing on interactive projects using React and Nodejs.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store