My Logo

PUBLISHED MAY 18, 2024

Teghen Donald Ticha
Lastly Updated: 3 months ago
Reading time 27 mins

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.
Implementating Core Features and Conclusion

Prerequisite

It's recommended that you start from parts and of this chapter in order to follow up with this final part smoothly.

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. 1. navigate through directories,
  2. 2.create new directories,
  3. 3.add files,
  4. 4.edit files,
  5. 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. 1.config: Contains configuration files and modules, such as readline setup and configuration.
  2. 2.helpers: Houses helper functions used throughout the application, such as displaying messages and handling user prompts.
  3. 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. 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. 2.prompts.js: Contains functions related to displaying messages and handling user prompts using the readline interface.
  3. 3.directoryHandlers.js: Houses functions responsible for handling directory-related operations, such as creating new directories and navigating existing ones.
  4. 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 to process.stdin, which refers to the standard input stream (usually the keyboard).
  • The output option is set to process.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 the inquirer 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 specified dirName.
      • 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. Use slice to get the parent path up to dirName, join it back into a string, and return the full path to the parent directory.
    • Directory Not Found: If no directory containing dirName is found, log a message and return the path to the storage directory.


  • 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, or size) and order (asc or desc).
    • 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
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.
  • 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 and fs.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
    
NB: The above directories were all created from our app 🤓💻.


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,

nodemon - Makes the arrow keys print gibrish on list prompts. Workaround: Add { 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 with fs.writeFile.
    • Implement role-based access control to restrict users' actions based on their roles.
  • File Management Features:
    • 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!

All Chapter Parts for NodeJs In Theory, An absolute Beginner’s Overview
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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!

  11. 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.

  12. 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.

  13. 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

  14. 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

  15. 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

  16. 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.

  17. 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.

  18. 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.

  19. 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.

  20. 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.

  21. 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.

  22. 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.

  23. 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.