1

I need to get the path of the directory from the CL argument and and create an object tree, which contains the file structure of the selected directory.

If it's a file, it's value should be true. And if it's a directory I should do the same for that directory (I think the best approach is recursion).

The output should look like something like this:

{
  file.txt: true,
  directory: {
    one.txt: true,
    two.txt: true,
    ...
  }
  ...
}

So far I tried the recursion version of that but it fails and don't know why. I think it's because I didn't handle the async part of my code properly. Here is my code:

const fs = require("fs");

const basePath = process.argv[2]; //Getting the path (it works)
const result = {};

//Function to check my path is exist and it's a directory
const isDirectory = path => {
  return new Promise((resolve, reject) => {
    fs.lstat(path, (err, stats) => {
      if (err) reject("No such file or Directory");
      resolve(stats.isDirectory());
    });
  });
};

//Recursive function that should create the object tree of the file system
const createTree = (path, target) => {
  return new Promise((resolve, reject) => {
    reject("Promise Oops...");
    fs.readdir(path, (err, data) => {
      data.forEach(item => {
        const currentLocation = `${path}/${item}`;
        isDirectory(currentLocation)
          .then(isDir => {
            if (!isDir) {
              target[item] = true;
              return;
            }

            target[item] = {};
            resolve(createTree(currentLocation, target[item]));
          })
          .catch(err => console.log("Oops in then..."));
      });
    });
  });
};

//Consuming the createTree function
(async () => {
  try {
    const res = await createTree(basePath, result);
    console.log(res);
  } catch (err) {
    console.log("Oops consume...");
  }
})();
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189

2 Answers2

2

You can use fs.promises instead of callback based methods of fs which improves the readability and helps you to find the mistakes easily. You are logging what is returned from createTree function which is undefined. You should log result object to see the result you want.

const fs = require("fs");

const basePath = process.argv[2]; //Getting the path (it works)
const result = {};

//Function to check my path is exist and it's a directory
const isDirectory = async (path) => {
  try {
    const stats = await fs.promises.lstat(path);
    return stats.isDirectory();
  } catch (error) {
    throw new Error("No such file or Directory");
  }
};

//Recursive function that should create the object tree of the file system
const createTree = async (path, target) => {
  const data = await fs.promises.readdir(path);
  for (const item of data) {
    const currentLocation = `${path}/${item}`;
    const isDir = await isDirectory(currentLocation);
    if (!isDir) {
      target[item] = true;
      continue;
    }
    target[item] = {};
    await createTree(currentLocation, target[item]);
  }
};

//Consuming the createTree function
(async () => {
  try {
    await createTree(basePath, result);
    console.log(result);
  } catch (error) {
    console.log(error.message);
  }
})();
Darvesh
  • 645
  • 7
  • 15
2

fs/promises and fs.Dirent

Here's an efficient, non-blocking program using Node's fast fs.Dirent objects and fs/promises module. This approach allows you to skip wasteful fs.exist or fs.stat calls on every path -

// main.js

import { readdir } from "fs/promises"
import { join, basename } from "path"

async function* tokenise (path = ".")
{ yield { dir: path }
  for (const dirent of await readdir(path, { withFileTypes: true }))
    if (dirent.isDirectory())
      yield* tokenise(join(path, dirent.name))
    else
      yield { file: join(path, dirent.name) }
  yield { endDir: path }
}

async function parse (iter = empty())
{ const r = [{}]
  for await (const e of iter)
    if (e.dir)
      r.unshift({})
    else if (e.file)
      r[0][basename(e.file)] = true
    else if (e.endDir)
      r[1][basename(e.endDir)] = r.shift()
  return r[0]
}

async function* empty () {}

Now createTree is simply a combination of tokenise and parse -

const createTree = (path = ".") =>
  parse(tokenise(path))

createTree(".")
  .then(r => console.log(JSON.stringify(r, null, 2)))
  .catch(console.error)

Let's get some sample files so we can see it working -

$ yarn add immutable     # (just some example package)
$ node main.js
{
  ".": {
    "main.js": true,
    "node_modules": {
      ".yarn-integrity": true,
      "immutable": {
        "LICENSE": true,
        "README.md": true,
        "contrib": {
          "cursor": {
            "README.md": true,
            "__tests__": {
              "Cursor.ts.skip": true
            },
            "index.d.ts": true,
            "index.js": true
          }
        },
        "dist": {
          "immutable-nonambient.d.ts": true,
          "immutable.d.ts": true,
          "immutable.es.js": true,
          "immutable.js": true,
          "immutable.js.flow": true,
          "immutable.min.js": true
        },
        "package.json": true
      }
    },
    "package.json": true,
    "yarn.lock": true
  }
}

I hope you enjoyed reading this post. For added explanation and other ways to use async generators, see this Q&A.

Mulan
  • 129,518
  • 31
  • 228
  • 259