oldboy
oldboy

Reputation: 5954

Tree structure not building as expected

I'm trying to write a function to create a tree from a given starting directory. In other words, I choose a directory, and then the function maps absolutely everything in that particular directory. I've never done anything like this before, but I'm close to the expected output. However, I'm not sure where I'm going wrong.

Starting Directory

> Test Root
    > 1a
    > 1b
        > 2a
        > 2b
        > 2c
    > 1c
        > 2a
    > 1d
        > 2a
        > 2b
    > testfile.txt

Output

The first level is good to go. The second level is almost good to go, but for some reason it's duplicating the parent property and creating an additional nested object. It's also always inserting the type property. I've tried removing properties in the constructor, but that output is just as bewildering.

{
  "parent": null,
  "path": "C:\\Users\\Anthony\\Desktop\\Test Root",
  "name": "Test Root",
  "kids": [
    {
      "parent": {
        "parent": "C:\\Users\\Anthony\\Desktop\\Test Root",
        "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1a",
        "name": "1a",
        "kids": []
      },
      "type": "fileType"
    },
    {
      "parent": {
        "parent": "C:\\Users\\Anthony\\Desktop\\Test Root",
        "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b",
        "name": "1b",
        "kids": [
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b\\2a",
              "name": "2a",
              "kids": []
            },
            "type": "fileType"
          },
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b\\2b",
              "name": "2b",
              "kids": []
            },
            "type": "fileType"
          },
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1b\\2c",
              "name": "2c",
              "kids": []
            },
            "type": "fileType"
          }
        ]
      },
      "type": "fileType"
    },
    {
      "parent": {
        "parent": "C:\\Users\\Anthony\\Desktop\\Test Root",
        "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1c",
        "name": "1c",
        "kids": [
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1c",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1c\\2a",
              "name": "2a",
              "kids": []
            },
            "type": "fileType"
          }
        ]
      },
      "type": "fileType"
    },
    {
      "parent": {
        "parent": "C:\\Users\\Anthony\\Desktop\\Test Root",
        "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1d",
        "name": "1d",
        "kids": [
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1d",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1d\\2a",
              "name": "2a",
              "kids": []
            },
            "type": "fileType"
          },
          {
            "parent": {
              "parent": "C:\\Users\\Anthony\\Desktop\\Test Root\\1d",
              "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\1d\\2b",
              "name": "2b",
              "kids": []
            },
            "type": "fileType"
          }
        ]
      },
      "type": "fileType"
    },
    {
      "parent": {
        "parent": "C:\\Users\\Anthony\\Desktop\\Test Root",
        "path": "C:\\Users\\Anthony\\Desktop\\Test Root\\testfile.txt",
        "name": "testfile.txt",
        "type": "fileType"
      },
      "type": "fileType"
    }
  ]
}

Function

async function createTree(root){
  const
    Node = class {
      constructor(parent, path, name, fifo){
        this.parent = parent
        this.path = path
        this.name = name
        fifo ? this.kids = [] : this.type = 'fileType'
      }
      addChild(parent, path, name, fifo){
        this.kids.push(new Node(parent, path, name, fifo))
      }
    },
    traverse = async function(node, path){
      const childPaths = await fsp.readdir(path)
      for (const childPath of childPaths){
        const
          name = childPath,
          stats = await fsp.stat(join(path, childPath))
        let
          fifo

        if (stats.isDirectory()) fifo = 1
        else if (stats.isFile()) fifo = 0

        const
          childNode = new Node(path, join(path, childPath), name, fifo)

        node.addChild(childNode)

        traverse(childNode, join(path, childPath))
      }
    },
    rootName = root.slice(-1) === '\\' ? root.slice(0,1) : root.slice(root.lastIndexOf('\\')+1),
    tree = new Node(null, root, rootName, 1)

    traverse(tree, root)

    setTimeout(function(){
      console.log(JSON.stringify(tree, null, 2))
    }, 2500)
}

I just realized my asynchronous functions aren't returning anything. I'm going to have to look over all of this tomorrow.

Upvotes: 0

Views: 37

Answers (1)

James Hay
James Hay

Reputation: 7315

The problem is that you are passing in only the first argument to your addChild function here.

The addChild function expects 4 arguments to be provided. However:

node.addChild(childNode)

// is approximately equivalent to:

addChild ( Node {...}, undefined, undefined, undefined)

You should be calling the function like this:

node.addChild(childNode.path, childNode.path, childNode.name, childNode.fifo)

You're on the right path logically, approaching a nicely recursive solution, but I think the class is a bit overkill and far too object-oriented for a functional problem such as this. Here's a quick example of a more functional version which should give you the result you expect:

const nodePath = require('path');
const fs = require('fs');

/**
 * Supply an `fs` function and it will turn it into a promisified version
 * @param {Function} fn The function to promisify. This means you do not 
 * need the `fsp` library.
 */
function promisify (fn) {
  return (...args) => new Promise((resolve, reject) => fn(...args, (err, data) => err ? reject(err) : resolve(data)));
};

// Create promisified versions of `fs.stat` and `fs.readdir`
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);

function createDirectory (parent, path, name, kids) {
  return { parent, path, name, kids: kids || [] };
};

function createFile (parent, path, name) {
  return { parent, path, name, type: 'fileType' };
}

// The main recursive function.
async function createTree (parent, path) {
  let stats = await stat(path);

  if (stats.isDirectory()) {
    const children = await readdir(path);

    // Because each recursive call retruns a promise, we want to continue only when
    // all of them have resolved. So we use Promise.all for this.
    let child_nodes = await Promise.all(
      children.map(child => createTree(path, nodePath.join(path, child)))
    );

    // Create a directory node
    return createDirectory(parent, path, nodePath.basename(path), child_nodes);
  }

  // Create a file node
  return createFile(parent, path, nodePath.basename(path));
};


// Startup code, can use your own stuff here.
async function start () {
  let tree = await createTree(null, '/home/james/stackoverflow/58706769/Test Root');

  console.log(JSON.stringify(tree, null, 2));
}

start();

Running this file in Node, I get the following output:

{
  "parent": null,
  "path": "/home/james/stackoverflow/58706769/Test Root",
  "name": "Test Root",
  "kids": [
    {
      "parent": "/home/james/stackoverflow/58706769/Test Root",
      "path": "/home/james/stackoverflow/58706769/Test Root/1a",
      "name": "1a",
      "kids": []
    },
    {
      "parent": "/home/james/stackoverflow/58706769/Test Root",
      "path": "/home/james/stackoverflow/58706769/Test Root/1b",
      "name": "1b",
      "kids": [
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1b",
          "path": "/home/james/stackoverflow/58706769/Test Root/1b/2a",
          "name": "2a",
          "kids": []
        },
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1b",
          "path": "/home/james/stackoverflow/58706769/Test Root/1b/2b",
          "name": "2b",
          "kids": []
        },
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1b",
          "path": "/home/james/stackoverflow/58706769/Test Root/1b/2c",
          "name": "2c",
          "kids": []
        }
      ]
    },
    {
      "parent": "/home/james/stackoverflow/58706769/Test Root",
      "path": "/home/james/stackoverflow/58706769/Test Root/1c",
      "name": "1c",
      "kids": [
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1c",
          "path": "/home/james/stackoverflow/58706769/Test Root/1c/2a",
          "name": "2a",
          "kids": []
        }
      ]
    },
    {
      "parent": "/home/james/stackoverflow/58706769/Test Root",
      "path": "/home/james/stackoverflow/58706769/Test Root/1d",
      "name": "1d",
      "kids": [
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1d",
          "path": "/home/james/stackoverflow/58706769/Test Root/1d/2a",
          "name": "2a",
          "kids": []
        },
        {
          "parent": "/home/james/stackoverflow/58706769/Test Root/1d",
          "path": "/home/james/stackoverflow/58706769/Test Root/1d/2b",
          "name": "2b",
          "kids": []
        }
      ]
    },
    {
      "parent": "/home/james/stackoverflow/58706769/Test Root",
      "path": "/home/james/stackoverflow/58706769/Test Root/testfile.txt",
      "name": "testfile.txt",
      "type": "fileType"
    }
  ]
}

Upvotes: 1

Related Questions