PUBLISHED MAY 18, 2024
Implementating Core Features and Conclusion
In this guide, we'll complete the rest of the more advanced functionalities of our app including, create, search, sort, delete, rename and navigate file directories.
Prerequisite
Recap of the Interactive File Explorer Application
The Interactive File Explorer is a command-line application developed using Node.js. It allows users to:
- 1.
navigate through directories
, - 2.
create new directories
, - 3.
add files
, - 4.
edit files
, - 5.and perform various other file management operations.
For advance functionalities, we'll aims to enhance the directory exploration functionality of the Interactive File Explorer application by introducing more capabilities such as:
- ➢Ability to list all directories within a specified directory
- ➢Ability to list all files within a specified directory
- ➢Sorting files based on different criteria such as creation date, last modified date, and file size
By providing users with more comprehensive directory listing and sorting options, they can navigate and manage their files more efficiently. This feature also aligns with the application's goal of offering a robust file management solution.
Enough blabla, let's jump right into it then.
In this section, we'll delve into the structure of the Interactive File Explorer codebase. We'll explore key files, dependencies, and the overall architecture to gain a better understanding of how the application is structured.
Directory Structure
The Interactive File Explorer codebase follows a structured directory layout, which helps organize the various components of the application. Here's an overview of the main directories and their purposes:
- 1.config: Contains configuration files and modules, such as
readline
setup and configuration. - 2.helpers: Houses helper functions used throughout the application, such as displaying messages and handling user prompts.
- 3.src: Contains the main source code files of the application, including the entry point (
index.js
) and feature-specific modules.
Key Files
Let's highlight some of the key files in the Interactive File Explorer codebase:
- 1.index.js: This file serves as the entry point of the application. It imports necessary modules and initiates the execution of the application.
- 2.prompts.js: Contains functions related to displaying messages and handling user prompts using the
readline
interface. - 3.directoryHandlers.js: Houses functions responsible for handling directory-related operations, such as creating new directories and navigating existing ones.
- 4.utils/index.js: Contains utility functions used across the application, such as retrieving directories and files from the filesystem.
Dependencies
The Interactive File Explorer application utilizes several dependencies to facilitate its functionality. Some of the key dependencies include:
- ➢fs-extra: An enhanced version of the Node.js
fs
module, providing additional features and utilities for working with the filesystem. - ➢chalk: A library for styling terminal text with colors and formatting.
Understanding the structure and key components of the codebase lays the foundation for the rest of the implementing.
In the next section, we'll discuss the design and implementation of this feature.
In this section, we will cover the app configuration and utility implementations.
The code presented here is broken down into manageable chunks, explaining each part thoroughly, to improve code content digestion.
Lets start with some configuration.
NB: Setting up an app global configuration is not a requirement. In our case, we will using a global config for readline's createInterface
and inquirer.js createPromptModule
, both for convinience and illustrative purposes.
So in the config folder, add these three file and their content.
// src/config/readline-global.js
const readline = require('readline');
// Create readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
module.exports = rl;
Breakdown:
- ➢
readline.createInterface
is a method that creates a new readline interface instance. - ➢The
input
option is set toprocess.stdin
, which refers to the standard input stream (usually the keyboard). - ➢The
output
option is set toprocess.stdout
, which refers to the standard output stream (usually the terminal).
// src/config/inquirer-global.js
const inquirer = require('inquirer');
const prompt = inquirer.createPromptModule({ input: process.stdin, output: process.stdout });
module.exports = prompt;
Breakdown:
- ➢
inquirer.createPromptModule
: This method creates a prompt module using theinquirer
library. - ➢
input: process.stdin
: Sets the input stream to the standard input, which is usually the keyboard. - ➢
output: process.stdout
: Sets the output stream to the standard output, which is usually the terminal.
Let's export our configuration via an index.js file.
NB: Using an index file to export all modules or functions in a directory simplifies imports, centralizes exports, and improves maintainability by providing a single point of access and abstraction for all modules within the directory.// src/config/index.js
const prompt = require('./inquirer-global');
const rl = require('./readline-global');
module.exports = {
prompt,
rl
}
Now, let's implement some utility functions which we'll be re-using across multiple other functions.
// utils/index.js
const fse = require('fs-extra');
const path = require('path');
const fs = require('fs');
function pathToRootDirectory(dirName) {
const pathToStorage = path.join(__dirname, '../../storage');
if (dirName === 'storage') {
return path.join(__dirname, '../../');
}
else {
const dirList = fse.readdirSync(pathToStorage, {recursive: true});
if (dirList.some(dir => dir.includes(dirName))) {
for (let dir of dirList) {
if (dir.includes(dirName)) {
if (dir.split('/').length === 0) {
return pathToStorage;
}
else {
const indexOfDirName = dir.split('/').indexOf(dirName);
const dirParentPath = dir.split('/').slice(0, indexOfDirName).join('/');
return `${pathToStorage}/${dirParentPath}/`
}
}
}
}
else {
console.log('Couldn\'t find the parent directory! You are being sent back to storage directory');
return pathToStorage;
}
}
}
async function getAllDirectories (rootDirectory) {
let list = [];
try {
const files = await fse.readdir(rootDirectory);
for (let file of files) {
const filePath = rootDirectory + '/' + file;
const stat = await fse.stat(filePath);
if (stat.isDirectory()){
list.push(file);
}
}
return list;
} catch (error) {
throw error;
}
}
function getAllFilesSync (rootDirectory) {
const filelist = [];
function dirReader (dir) {
console.log('dir: ', dir)
try {
const files = fse.readdirSync(dir);
for (let file of files) {
const filePath = dir + '/' + file;
const stat = fse.statSync(filePath);
if (stat.isDirectory()){
dirReader(filePath)
}
else {
filelist.push(filePath);
}
}
return filelist;
} catch (error) {
throw error;
}
}
const list = dirReader(rootDirectory, []);
return list;
}
function getAndSortAllFilesSync (rootDirectory, sortOption = {criteria: 'created', order: 'asc'}) {
const filelist = [];
function dirReader (dir) {
try {
const files = fse.readdirSync(dir);
for (let file of files) {
const filePath = dir + '/' + file;
const stat = fse.statSync(filePath);
if (stat.isDirectory()){
dirReader(filePath)
}
else {
const fileObj = {
created: stat.birthtimeMs,
lastModified: stat.mtimeMs,
size: stat.size ,
fileName: file
}
filelist.push(fileObj);
}
}
return filelist;
} catch (error) {
throw error;
}
}
const list = dirReader(rootDirectory, []);
if (sortOption.criteria === 'lastModified') {
return sortOption.order === 'asc' ? list.sort((fileObj1 , fileObj2) => fileObj1.lastModified - fileObj2.lastModified) : list.sort((fileObj1 , fileObj2) => fileObj2.lastModified - fileObj1.lastModified);
}
else if (sortOption.criteria === 'size') {
return sortOption.order === 'asc' ? list.sort((fileObj1 , fileObj2) => fileObj1.size - fileObj2.size) : list.sort((fileObj1 , fileObj2) => fileObj2.size - fileObj1.size);
}
else {
return sortOption.order === 'asc' ? list.sort((fileObj1 , fileObj2) => fileObj1.created - fileObj2.created) : list.sort((fileObj1 , fileObj2) => fileObj2.created - fileObj1.created);
}
}
function getdirOrFileFromPath (path, revPosition) {
if (!path) {
throw new Error('Please provide a path!');
}
const pathArray = path.split('/');
const index = (pathArray.length - 1 ) + revPosition;
return pathArray[index];
}
module.exports = {
getAllDirectories,
getAllFilesSync,
getdirOrFileFromPath,
getAndSortAllFilesSync
}
Breakdown:
- ➢
pathToRootDirectory
: This function returns the path to the root directory of a specified subdirectory within the storage folder. If the specified subdirectory is not found, it returns the storage folder by default.- ➢Path to Storage: Define the path to the storage directory using
path.join
and__dirname
. - ➢Check for 'storage': If the provided
dirName
is 'storage', return the path to the project's root directory. - ➢Read Storage Directory: Use
fse.readdirSync
to synchronously read the contents of the storage directory. - ➢Check for Directory Name: Use
some
to check if any of the directories in the storage directory contain the specifieddirName
.- ➢Find Specific Directory: When a directory that includes
dirName
is found:- ➢Empty Split Result: If splitting the directory path by '/' results in an array with zero length, return the path to storage.
- ➢Construct Parent Path: Otherwise, find the index of the specified
dirName
in the split path array. Useslice
to get the parent path up todirName
, join it back into a string, and return the full path to the parent directory.
- ➢Find Specific Directory: When a directory that includes
- ➢Directory Not Found: If no directory containing
dirName
is found, log a message and return the path to the storage directory.
- ➢Path to Storage: Define the path to the storage directory using
- ➢
getAllDirectories
: This function asynchronously retrieves all directories within a specified root directory- ➢Initialize an empty list to store directories.
- ➢Use
fse.readdir
to read the contents of the root directory. - ➢Iterate over each item in the directory.
- ➢Construct the full file path and use
fse.stat
to get its status. - ➢If the item is a directory, add it to the list.
- ➢Return the list of directories.
- ➢Catch and throw any errors encountered during the process.
- ➢
getAllFilesSync
: This function synchronously retrieves all files within a specified directory and its subdirectories.- ➢Initialize an empty list to store file paths.
- ➢Define a recursive helper function
dirReader
that reads directory contents. - ➢Use
fse.readdirSync
to synchronously read the contents of the directory. - ➢Iterate over each item and construct its full file path.
- ➢Use
fse.statSync
to get the item's status. - ➢If the item is a directory, recursively read its contents.
- ➢If the item is a file, add its path to the list.
- ➢Start the process from the root directory.
- ➢Return the list of all files.
- ➢
getAndSortAllFilesSync
: This function synchronously retrieves and sorts all files within a specified directory and its subdirectories based on specified sorting criteria.- ➢Initialize an empty list to store file objects.
- ➢Define a recursive helper function
dirReader
that reads directory contents. - ➢Use
fse.readdirSync
to synchronously read the directory contents. - ➢Iterate over each item and construct its full file path.
- ➢Use
fse.statSync
to get the item's status. - ➢If the item is a directory, recursively read its contents.
- ➢If the item is a file, create a file object containing its creation time, last modified time, size, and name, and add it to the list.
- ➢Start the process from the root directory.
- ➢Sort the list of file objects based on the specified criteria (
created
,lastModified
, orsize
) and order (asc
ordesc
). - ➢Return the sorted list.
- ➢
getdirOrFileFromPath
: This function retrieves a directory or file name from a given path based on a specified position.- ➢Check if the path is provided. If not, throw an error.
- ➢Split the path into an array using the '/' delimiter.
- ➢Calculate the index by adding the reverse position to the last index.
- ➢Return the directory or file name at the calculated index.
In this section, we'll complete the remaining core functionalities.
All the core functions are placed within the core folder. Let's implement and breakdown each of these core functions.
src/core/directoryHandlers.js
const fse = require('fs-extra');
const chalk = require('chalk');
const { continueWithDirectoryInteraction } = require('./directoryNavigation');
const { pathToRootDirectory } = require('../utils');
const {prompt} = require('../config');
// Function to create a new directory
function createNewDirectory() {
prompt([
{
type: 'input',
name: 'directoryName',
message: 'Enter the name of the new directory: '
}
])
.then((answers) => {
const fullDirectoryPath =
pathToRootDirectory() + answers.directoryName;
console.log('fullDirectoryPath: ', fullDirectoryPath);
// Create new directory
fse.ensureDir(fullDirectoryPath)
.then(() => {
console.log(
`New directory "${answers.directoryName}" created successfully.`
);
// Continue with directory interaction
continueWithDirectoryInteraction(answers.directoryName);
})
.catch((err) => {
console.error('Error creating directory:', err);
});
});
}
// Function to provide an existing directory
function useExistingDirectory() {
prompt([
{
type: 'input',
name: 'directoryPath',
message: 'Enter the path of the existing directory: '
}
])
.then((answers) => {
const fullDirectoryPath =
pathToRootDirectory(answers.directoryPath) + answers.directoryPath;
// Check if directory exists
fse.pathExists(fullDirectoryPath)
.then((exists) => {
if (exists) {
console.log(
chalk.green(`Existing directory "${chalk.bold(answers.directoryPath)}" loaded successfully.`)
);
// Continue with directory interaction
continueWithDirectoryInteraction(answers.directoryPath);
} else {
console.error(
chalk.yellow('Directory does not exist:'),
chalk.underline.bgRed(answers.directoryPath)
);
useExistingDirectory(); // Prompt user again
}
})
.catch((err) => {
console.error(
chalk.red('Error checking directory existence: '),
err
);
});
});
}
module.exports = {
createNewDirectory,
useExistingDirectory
};
Breakdown:
createNewDirectory
- ➢Prompts user for a new directory name.
- ➢Constructs the full path using
pathToRootDirectory
. - ➢Creates the directory with
fse.ensureDir
. - ➢On success, logs a message and continues with directory interaction.
useExistingDirectory
- ➢Prompts user for an existing directory path.
- ➢Constructs the full path using
pathToRootDirectory
. - ➢Checks if the directory exists with
fse.pathExists
. - ➢If it exists, logs a message and continues with directory interaction.
- ➢If not, logs an error and prompts the user again.
src/core/initial-prompts.js
src/core/initial-prompts.js
const {rl} = require('../config');
const {
useExistingDirectory,
createNewDirectory
} = require('./directoryHandlers');
// Function to display welcome message and initial prompt
function displayWelcomeMessage() {
console.log('Welcome to the Interactive File Explorer!');
console.log('-----------------------------------------');
console.log('Do you want to:');
console.log('1. Create a new directory');
console.log('2. Provide an existing directory');
}
// Function to handle user input for initial prompt
function handleInitialPrompt() {
rl.question('Enter your choice (1 or 2): ', (choice) => {
if (choice === '1') {
// Create new directory
createNewDirectory();
} else if (choice === '2') {
// Provide existing directory
useExistingDirectory();
} else {
console.log('Invalid choice. Please enter 1 or 2.');
handleInitialPrompt(); // Prompt user again
}
});
}
module.exports = {
displayWelcomeMessage,
handleInitialPrompt
};
Breakdown:
displayWelcomeMessage
- ➢Logs a welcome message and options for the user.
- ➢This function sets the context for the user, informing them what actions they can take next.
handleInitialPrompt
- ➢Prompts the user to enter a choice (1 or 2).
- ➢Based on the user input:
- ➢If `1` it calls
createNewDirectory
to create a new directory. - ➢If `2`, it calls
useExistingDirectory
to use an existing directory. - ➢If invalid input is provided, it prompts the user again.
- ➢If `1` it calls
- ➢This function handles the flow of user input and directs the application accordingly.
src/core/directoryNavigation.js
const chalk = require('chalk');
const fs = require('fs');
const fse = require('fs-extra');
const path = require('path');
const {prompt} = require('../config');
const { pathToRootDirectory } = require('../utils');
const {getAllDirectories, getAllFilesSync, getdirOrFileFromPath, getAndSortAllFilesSync} = require('../utils');
const CHOICES = [
'Add subdirectory',
'View contents of current directory',
'Navigate into subdirectory',
'Navigate out to parent directory',
'Add new file',
'Edit existing file',
'Rename file',
'Delete file',
'Move file',
'Search for file',
'Sort files',
'Exit the app'
]
// Function to continue with directory interaction
function continueWithDirectoryInteraction(directoryPath) {
setTimeout(() => { // we push this execution into a timer, make sure that it's push back into the callstack after directory loading or creation is completed
console.log(`\n\nContinue interacting with : ${chalk.blue(directoryPath)}`);
function runner () {
prompt([
{
type: 'list',
message: 'What do you want to do with this directory ? ',
name: 'choice',
choices: CHOICES,
loop: false
}
])
.then(answers => {
const fullDirectoryPath = pathToRootDirectory(directoryPath) + directoryPath;
console.log('fullDirectoryPath', fullDirectoryPath)
switch (answers.choice) {
case CHOICES[0]:
addSubDirectory(fullDirectoryPath);
break;
case CHOICES[1]:
listDirectoryContents(fullDirectoryPath);
break;
case CHOICES[2]:
navigateSubdirectory(fullDirectoryPath);
break;
case CHOICES[3]:
navigateOutOfDirectory(fullDirectoryPath);
break;
case CHOICES[4]:
addFile(fullDirectoryPath);
break;
case CHOICES[5]:
editFile(fullDirectoryPath);
break;
case CHOICES[6]:
renameFile(fullDirectoryPath);
break;
case CHOICES[7]:
deleteFile(fullDirectoryPath);
break;
case CHOICES[8]:
moveFile(fullDirectoryPath);
break;
case CHOICES[9]:
searchFile(fullDirectoryPath);
break;
case CHOICES[10]:
sortFiles(fullDirectoryPath);
break;
case CHOICES[11]:
confirmExit(fullDirectoryPath);
break;
default:
console.log('Invalid option! Please select a valid option.');
runner() ;
}
});
}
runner();
}, 200);
}
function confirmExit (currentDir) {
prompt([
{
type:'confirm',
name: 'choice',
message: 'You\'re about to exit the app. Sure that\'s what you want?',
default: false
}
])
.then(answers => {
if (answers.choice) {
console.log(`\n${chalk.bold.green('Goodbye! 👋')} \n${chalk.dim('See you next time! 🚀')}\n`);
process.exitCode = 1;
process.nextTick(process.exit);
}
else {
continueWithDirectoryInteraction(currentDir.split('/').pop());
}
});
}
// Function to list directory's content
function listDirectoryContents(directory) {
console.log(`\n *** ${directory.split('/').pop()} ***`);
if (!directory) {
const error = new Error('directory not found!')
throw error;
}
listDirectory(directory).then(() => {
continueWithDirectoryInteraction(directory.split('/').pop());
});
}
async function listDirectory(directoryPath, indent = '') {
fse.readdir(directoryPath, (err, files) => {
if (err) {
throw err;
}
files.forEach((file) => {
const filePath = `${directoryPath}/${file}`;
fse.stat(filePath, (err, stats) => {
if (err) {
throw err;
}
if (stats.isDirectory()) {
console.log(`${indent}📁 ${file}`);
listDirectory(filePath, `${indent} `); // Recursively list subdirectory
} else {
console.log(`${indent}📄 ${file}`);
}
});
});
});
}
// Function adds a child directory
async function addSubDirectory (currentDir) {
prompt([
{
name: 'dirName',
type:'input',
message: 'Enter a name for the sub folder (directory) : ',
validate: (input) => (input.trim().length > 1 && !input.trim().includes('.')) ? true : 'You first enter a valid name for the new sub folder : '
}
])
.then(answers => {
const dirName = answers.dirName;
fse.mkdir(`${currentDir}/${dirName}`)
.then(() => {
console.log('\n', chalk.green(`Successfully added ${chalk.bgGreen.bold(dirName)} to ${chalk.bgGreen.bold(currentDir.split('/').pop())}`));
continueWithDirectoryInteraction(currentDir.split('/').pop());
})
.catch(err => {
throw err;
});
});
}
// Function to navigate into subdirectories
function navigateSubdirectory(currentDirection) {
getAllDirectories(currentDirection)
.then(dirList => {
if (dirList.length === 0) {
setTimeout(() => {
console.log(`\nDirectory ${currentDirection.split('/').pop()} has no subforlder! You are being redirected to the parent directory.\n`);
navigateOutOfDirectory(currentDirection);
}, 500);
}
else {
prompt([
{
type: 'list',
name: 'choice',
message: dirList.length > 1 ? 'Which sub directory do you wish to navigate into ?' : 'Select this directory to navigate into it :',
choices: dirList,
loop: false
}
])
.then(answers => {
const fullPath = currentDirection + '/' + answers.choice;
const choiceOptions = ['Continue With Navigation', 'Add a sub directory', 'Explore Directory Content', 'Exit the App'];
prompt([
{
name: 'choice',
type: 'list',
message: 'What next would you like to do ? ',
choices: choiceOptions,
loop: false
}
])
.then(answers => {
if (answers.choice === choiceOptions[0]) {
navigateSubdirectory(fullPath);
}
else if (answers.choice === choiceOptions[1]) {
addSubDirectory(fullPath);
}
else if (answers.choice === choiceOptions[2]) {
continueWithDirectoryInteraction(fullPath.split('/').pop());
}
else if (answers.choice === choiceOptions[3]) {
confirmExit(fullPath.split('/').pop());
}
})
});
}
}) ;
}
function navigateOutOfDirectory(directory) {
let directories = directory.split('/');
directories = directories.slice(0, directories.length - 1);
const parentDirectory = directories.join('/');
continueWithDirectoryInteraction(parentDirectory.split('/').pop());
}
function addFile(directory) {
prompt([
{
message: 'Enter file name: ',
type: 'input',
name: 'fileName'
}
])
.then(answers => {
const fileName = answers.fileName;
prompt([
{
message: 'Enter file extension: ',
type: 'input',
name: 'fileExt',
default:'txt'
}
])
.then(answers => {
const fileExt = answers.fileExt;
fs.writeFile(`${directory}/${fileName}.${fileExt}`, '', err => {
if (err) {
console.error('Error creating file:', err);
return;
}
console.log(`\nSuccessfully created file: ${fileName}.${fileExt}`);
continueWithDirectoryInteraction(directory.split('/').pop());
});
})
})
}
// Function to rename existing file within current directory
async function renameFile(directory) {
const filePathList = getAllFilesSync(directory);
const fileList = filePathList.map(path => path.slice(path.lastIndexOf('/') + 1));
if (fileList.length === 0) {
console.log(`\nThe directory ${directory.split('/').pop()} has no files!`);
continueWithDirectoryInteraction(directory.split('/').pop());
}
else {
prompt([
{
name: 'oldFile',
type:'list',
message: 'Select the file to be renamed: ',
choices: fileList
}
])
.then(answers => {
const oldFile = answers.oldFile;
prompt([
{
name: 'newName',
type: 'input',
message: 'Enter the new file name you want : '
}
])
.then(answers => {
const newName = answers.newName;
const oldPath = filePathList[fileList.indexOf(oldFile)];
const newPath = oldPath.replace(oldFile, newName);
fs.rename(oldPath, newPath, err => {
if (err) {
throw err;
}
console.log(`\nSuccessfully change file from ${oldFile} to ${newName}`);
continueWithDirectoryInteraction(directory.split('/').pop());
});
});
})
}
}
// Function to edit file within current directory
async function editFile(directory) {
const filePathList = getAllFilesSync(directory);
const fileList = filePathList.map(path => path.slice(path.lastIndexOf('/') + 1));
if (fileList.length === 0) {
console.log(`\nThe directory ${directory.split('/').pop()} has no file to edit !`);
continueWithDirectoryInteraction(directory.split('/').pop());
}
else {
prompt([
{
name: 'choice',
type: 'list',
message: 'Select the file to be edited:',
choices: fileList
}
])
.then(answers => {
const fileName = answers.choice;
const filePathToEdit = filePathList[fileList.indexOf(fileName)];
prompt([
{
name:'content',
type:'editor',
message: 'Enter the file content: ',
validate(text) {
if (text.split('\n').length < 1) {
return 'Must be at least a line.';
}
return true;
},
waitUserInput: true,
}
])
.then(answers => {
const textContent = answers.content;
fse.writeFile(filePathToEdit, textContent, {
encoding:'utf8'
}, (err) => {
if (err) {
throw err;
}
console.log(`\n${fileName} has been successfully updated!`);
continueWithDirectoryInteraction(directory.split('/').pop());
})
})
});
}
}
// Function to delete a file within current directory
async function deleteFile(directory) {
const filePathList = getAllFilesSync(directory);
const fileList = filePathList.map(path => path.slice(path.lastIndexOf('/') + 1));
if (fileList.length === 0) {
console.log(`\nThe directory ${directory.split('/').pop()} has no file to delete!`);
continueWithDirectoryInteraction(directory);
}
else {
prompt([
{
name: 'choice',
type: 'list',
message: 'Select the file to be deleted: (NB: You can\'t undo this action ⛔️)',
choices: fileList
}
])
.then(answers => {
const filePathToDelete = filePathList[fileList.indexOf(answers.choice)];
fse.unlink(filePathToDelete, err => {
if (err) {
throw err;
}
console.log(`\nSuccessfully deleted the file : ${answers.choice}`);
continueWithDirectoryInteraction(directory.split('/').pop());
}) ;
});
}
}
// Function to move file between directories
async function moveFile(directory) {
const filePathList = getAllFilesSync(directory);
const fileList = filePathList.map(path => path.slice(path.lastIndexOf('/') + 1));
if (fileList.length === 0) {
console.log(`\nThe directory ${directory.split('/').pop()} has no files!`);
continueWithDirectoryInteraction(directory.split('/').pop());
}
else {
prompt([
{
name: 'oldFile',
type:'list',
message: 'Select the file to be renamed: ',
choices: fileList
}
])
.then(answers => {
const fileName = answers.oldFile
const oldPath = filePathList[fileList.indexOf(fileName)];
getAllDirectories(directory)
.then(dirList => {
if (dirList.length === 0) {
console.log(`\nDirectory ${directory.split('/').pop()} has no subdirectories! You need to create a subdirectory first.\n`);
continueWithDirectoryInteraction(directory.split('/').pop());
}
else {
prompt([
{
type: 'list',
name: 'choice',
message: 'Select the new directory for your file: ',
choices: dirList,
loop: false
}
])
.then(answers => {
const newPath = `${directory}/${answers.choice}/${fileName}`;
fse.rename(oldPath, newPath, (err) => {
if (err) {
throw err;
}
console.log(`\nFile has been moved from 📂${getdirOrFileFromPath(oldPath, -1)} to 🗂${getdirOrFileFromPath(newPath, -1)}`);
continueWithDirectoryInteraction(directory.split('/').pop())
})
});
}
}) ;
})
}
}
// Function to search for file by name or content
async function searchFile(directory) {
prompt([
{
name: 'fileName',
type: 'input',
message: 'Enter the name of the file to search: ',
validate: (input) => input.trim().length < 3 ? 'Please enter a valid file name' : true
}
])
.then(answers => {
const fileName = answers.fileName;
runSearch(directory, fileName)
.then(() => {
continueWithDirectoryInteraction(directory.split('/').pop());
})
});
}
// Function that search for a file in side a nested directory recursively
async function runSearch (directory, fileName) {
fs.readdir(directory, (error, files) => {
if (error) {
console.error(chalk.red('Error reading directory:'), error);
continueWithDirectoryInteraction(directory.split('/').pop());
}
for (const file of files) {
const filePath = path.join(directory, file);
fs.stat(filePath, (error, stats) => {
if (error) {
console.error(`\n${chalk.yellow('Error getting file stats for filePath: ') } ${chalk.underline.bgYellow.bold(filePath)} : `, error);
}
if (stats.isDirectory()) {
runSearch(filePath, fileName);
}
else if (file === fileName) {
console.log(`\n${chalk.green('File found! The path to file is : ')}`, chalk.underline.bgGreen.bold(filePath));
}
});
}
});
}
// Function to get and sort the files in a provided directory based on provided sorting criteria
async function sortFiles (directory) {
const files = getAllFilesSync(directory);
if (files.length === 0) {
console.log(`${chalk.yellow('The directory')} ${chalk.bgYellow.bold(directory.split('/').pop())} ${chalk.yellow('contains no file!')}`);
continueWithDirectoryInteraction(directory.split('/').pop());
}
const sortCriteriaOptions = ['created', 'lastModified', 'size'];
const sortOrderOptions = ['asc', 'desc'];
prompt([
{
name: 'sortCriteria',
type: 'list',
choices: ['1 ==> creation date', '2 ==> Last modification date', '3 ==> File size'],
loop: false,
message: 'Select a sorting criteria : '
}
])
.then(answers => {
const sortCriteria = sortCriteriaOptions[Number(answers.sortCriteria[0]) - 1];
prompt([
{
name: 'sortOrd',
type: 'list',
choices: ['1 => Ascending', '2 => Descending'],
loop: false,
message: 'Select a sorting order : '
}
])
.then(answers => {
const sortOrd = sortOrderOptions[Number(answers.sortOrd[0]) - 1];
const result = getAndSortAllFilesSync(directory, {criteria: sortCriteria, order: sortOrd});
console.log('\n******* Here is a summary of the files in the provided direction ******* ')
// Print table header
console.log(`\n${chalk.bold('FileName')}\t\t${chalk.bold('Created')}\t\t${chalk.bold('LastModified')}\t\t${chalk.bold('Size')}`);
console.log('--------\t\t-------\t\t------------\t\t----');
// Iterate over each object in the array and print row
result.forEach(item => {
console.log(`${chalk.green(item.fileName)}\t\t${chalk.cyan(new Date(item.created).toLocaleString())}\t\t${chalk.magenta(new Date(item.lastModified).toLocaleString())}\t\t${chalk.blue(item.size)}`);
});
continueWithDirectoryInteraction(directory.split('/').pop());
});
});
}
module.exports = {
continueWithDirectoryInteraction,
confirmExit,
listDirectoryContents
};
Breakdown:
continueWithDirectoryInteraction
- ➢Displays a menu of options for interacting with the current directory.
- ➢Executes the corresponding function based on the user's choice.
confirmExit
- ➢Asks the user for confirmation before exiting the application.
- ➢If confirmed, exits the application; otherwise, continues with directory interaction.
listDirectoryContents
- ➢Lists the contents of the current directory.
- ➢Calls the
listDirectory
function to recursively list subdirectories and files.
listDirectory
- ➢Recursively lists the contents of a directory.
- ➢Uses
fs.readdir
to read the directory contents andfs.stat
to determine if each item is a directory or file.
addSubDirectory
- ➢Prompts the user to enter a name for a new subdirectory.
- ➢Creates the new subdirectory using
fse.mkdir
. - ➢Displays a success message and continues with directory interaction.
navigateSubdirectory
- ➢Lists the subdirectories of the current directory.
- ➢Prompts the user to choose a subdirectory to navigate into.
- ➢Provides options to continue navigation, add a subdirectory, explore directory content, or exit the app.
navigateOutOfDirectory
- ➢Navigates to the parent directory of the current directory.
- ➢Continues with directory interaction in the parent directory.
addFile
- ➢Prompts the user to enter a file name and extension.
- ➢Creates a new file using
fs.writeFile
. - ➢Displays a success message and continues with directory interaction.
renameFile
- ➢Prompts the user to select a file to rename and enter a new name.
- ➢Renames the selected file using
fs.rename
. - ➢Displays a success message and continues with directory interaction.
editFile
- ➢Prompts the user to select a file to edit.
- ➢Allows the user to edit the file content.
- ➢Writes the edited content to the file using
fse.writeFile
. - ➢Displays a success message and continues with directory interaction.
deleteFile
- ➢Prompts the user to select a file to delete.
- ➢Deletes the selected file using
fse.unlink
. - ➢Displays a success message and continues with directory interaction.
moveFile
- ➢Prompts the user to select a file to move and choose a destination directory.
- ➢Moves the file to the chosen directory using
fse.rename
. - ➢Displays a success message and continues with directory interaction.
searchFile
- ➢Prompts the user to enter the name of the file to search.
- ➢Recursively searches for the file in the current directory and its subdirectories.
- ➢Displays the path to the file if found.
sortFiles
- ➢Prompts the user to select sorting criteria and order.
- ➢Sorts the files in the current directory based on the selected criteria and order.
- ➢Displays a summary of the sorted files and continues with directory interaction.
In this final section, we'll implement a basic app entry logic and run our app to make sure everything works as expected.
Let's implement the app entry point in the `src/index.js` directory.
This index.js file essentially serves as the entry point for the application, importing necessary functions from core.js
and orchestrating the initial steps for user interaction.
src/index.js
const { displayWelcomeMessage, handleInitialPrompt } = require('./core');
// Call functions to display welcome message and initial prompt
function main () {
displayWelcomeMessage();
handleInitialPrompt();
}
main();
Test Run:
NB: Before we start testing our app, make sure that you have a storage folder in the root directory of this project.Also, for convinience, please manually add some subdirectories to the storage folder and to these subdirectory add other subdirectories and files (.txt, .js, .ts, or .css files with some basic content).
In my case, here is what I have:
📁nameer
📁 another
📄 name.txt
📄 nameer-code.js
📁 player
📁 yetAnother
📄 code.css
📄 play.js
📁 final
📄 testing.js
📁 new-dir
📁 sun-new-dir
📁 sub-sub-new-dir
📄 sub-text.txt
Once, you have a few directories to test with, then let's make sure our starting script is set up correctly.
Revisiting the pactkage.json file
// package.json
......
"scripts": {
"format": "prettier --write src test",
"start": "node src/index.js",
"test": "mocha",
"dev": "nodemon --no-stdin --exec \"npm run start\""
},
.....
Make sure you add `--no-stdin` to the nodemon command. That resolves some inconsistency issues with the arrow key. According to their documentation,
{ stdin : false }
in the configuration file or pass --no-stdin
in the CLI.
If you still encounter some weirdities during development using the npm run dev
command, then you can simply use the npm start
command instead, but ofcourse this means you'd have to manually restart your app after each change.
Before concluding, let's discuss some additional features that you can bring to this app.
NB: Although testing was initially included, I decided reluctantly to remove the section on testing to keep things simple.You may do some research on testing and come up with your own testing strategy and implementation. You may even start a discussion on that in the comments.
To enhance the Interactive File Explorer app, you could consider adding the following functionalities:
- ➢File and Directory Operations:
- ➢Implement additional file and directory operation such as copying files.
- ➢Include features for searching for files by content.
- ➢User Authentication and Permissions:
- ➢Introduce user authentication to allow multiple users to access the file explorer with different permissions. This is acheived by setting a
{ mode: permissions }
option when creating the file withfs.writeFile.
- ➢Implement role-based access control to restrict users' actions based on their roles.
- ➢Introduce user authentication to allow multiple users to access the file explorer with different permissions. This is acheived by setting a
- ➢File Management Features:
- ➢Provide options for organizing files and directories through tagging or categorization.
- ➢Provide options for organizing files and directories through tagging or categorization.
- ➢Integration with Cloud Storage:
- ➢Allow users to access and manage files stored in the cloud directly from the file explorer.
Extra things to do or learn while working on the project:
- ➢Security Best Practices:
- ➢Learn about common security threats in file management applications and implement security measures to protect user data.
- ➢Explore techniques such as input validation, sanitization, and encryption to enhance security.
- ➢Error Handling and Logging:
- ➢Practice error handling techniques to gracefully handle exceptions and provide meaningful error messages to users.
- ➢Implement logging functionality to record application events and monitor system behavior for debugging purposes.
- ➢Performance Optimization:
- ➢Learn about performance optimization techniques to improve the speed and responsiveness of the application.
- ➢Experiment with strategies such as caching, lazy loading, and asynchronous processing to optimize file operations.
Conclusion
In conclusion, the Interactive File Explorer app provides a versatile CLI based UI for managing files and directories with ease.
By continuously adding new functionalities and usecases, you can further enhance the app's capabilities and your own skills.
Whether it's improving security, optimizing performance, or refining the user experience, there are endless possibilities for enhancement.
NB: Th complete source code for this project can be found here. Also, please don't hesitate to reach out should you need any further clarification.
Happy exploring!
Chapter 1 , Part 1 : Introduction to NodeJS
In this series part, I introduce nodeJS and some technical concepts associated with it. I also show how easy it is to setup and start a simple nodeJS web server.
Chapter 1 , Part 2 : How to Install and Setup NodeJS
In this series part, I run you through the various ways to install nodeJS. I also discuss how to install nvm and use it to switch between different node versions.
Chapter 1 , Part 3 : How much JavaScript do you need to learn NodeJS
In this series part, we explore the nuanced relationship between JavaScript and NodeJS, highlighting some subtle distinctions between the two environments.
Chapter 1 , Part 4 : The v8 Engine and the difference Between NodeJS and the browser
In this series part, we explore the V8 engine and how it interacts with nodeJS. We also discuss node’s event loop and uncover the mystery behinds node’s ability to handle concurrent operations.
Chapter 1 , Part 5 : NPM, the NodeJS package manager
Discover the essentials of npm, the powerful package manager for Node.js. Learn installation, management, publishing, and best practices
Chapter 1 , Part 6 : NodeJS in Development Vs Production
Explore how Node.js behaves differently in development and production environments. Learn key considerations for deploying Node.js applications effectively.
Chapter 2 , Part 1 : Asynchronous Flow Control
In this series part, we'll explore various aspects of asynchronous flow control in Node.js, from basic concepts to advanced techniques.
Chapter 2 , Part 2 : Blocking vs Non-blocking I/O
Explore the differences between blocking and non-blocking I/O in Node.js, and learn how to optimize performance and scalability.
Chapter 2 , Part 3 : Understanding NodeJS Event loop
Exploring the Node.js event loop by understanding its phases, kernel integration, and processes enabling seamless handling of asynchronous operations in your applications.
Chapter 2 , Part 4 : The NodeJS EventEmitter
Explore the power of Node.js EventEmitter: an essential tool for building scalable and event-driven applications. Learn how to utilize it effectively!
Chapter 3 , Part 1 : Working with files in NodeJS
Gain comprehensive insights into file management in Node.js, covering file stats, paths, and descriptors, to streamline and enhance file operations in your applications.
Chapter 3 , Part 2 : Reading and Writing Files in NodeJS
Uncover the fundamentals of reading and writing files in nodeJS with comprehensive examples and use cases for some widely used methods.
Chapter 3 , Part 3 : Working with Folders in NodeJS
Unlock the secrets of folder manipulation in Node.js! Explore essential techniques and methods for working with directories efficiently
Chapter 4 , Part 1 : Running NodeJS Scripts
Master the command line interface for executing nodeJS scripts efficiently. Learn common options and best practices for seamless script execution
Chapter 4 , Part 2 : Reading Environment Variables in NodeJS
Learn how to efficiently manage environment variables in nodeJS applications. Explore various methods and best practices for security and portability
Chapter 4 , Part 3 : Writing Outputs to the Command Line in NodeJS
Learn essential techniques for writing outputs in nodeJS CLI. From basic logging to formatting and understanding stdout/stderr.
Chapter 4 , Part 4 : Reading Inputs from the Command Line in NodeJS
Learn the various ways and strategies to efficiently read command line inputs in nodeJS, making your program more interactive and flexible.
Chapter 4 , Part 5 : The NodeJS Read, Evaluate, Print, and Loop (REPL)
Explore the power of nodeJS's Read, Evaluate, Print, and Loop (REPL). Learn how to use this interactive environment for rapid prototyping, debugging, and experimentation.
Chapter 5 , Part 1 : Introduction to Testing in NodeJS
Discover the fundamentals of testing in nodeJS! Learn about testing types, frameworks, and best practices for building reliable applications.
Chapter 5 , Part 2 : Debugging Tools and Techniques in NodeJS
Explore essential debugging tools and techniques in Node.js development. From built-in options to advanced strategies, and best practices for effective debugging.
Chapter 6 , Part 1 : Project Planning and Setup
Discuss the planning and design process for building our interactive file explorer in Node.js, focusing on core features, UI/UX design, and implementation approach and initial setup.
Chapter 6 , Part 2 : Implementing Basic functionalities
In this guide, we'll implement the basic functionalities of our app which will cover initial welcome and action prompts.
Chapter 6 , Part 3 : Implementating Core Features and Conclusion
In this guide, we'll complete the rest of the more advanced functionalities of our app including, create, search, sort, delete, rename and navigate file directories.