73

I have a file structure like this:

root
|_ fruits
|___ apple
|______images
|________ apple001.jpg
|________ apple002.jpg
|_ animals
|___ cat
|______images
|________ cat001.jpg
|________ cat002.jpg

I would like to, using Javascript and Node.js, listen to this root directory and all sub directories and create a JSON which mirrors this directory structure, each node contains type, name, path, and children:

data = [
  {
    type: "folder",
    name: "animals",
    path: "/animals",
    children: [
      {
        type: "folder",
        name: "cat",
        path: "/animals/cat",
        children: [
          {
            type: "folder",
            name: "images",
            path: "/animals/cat/images",
            children: [
              {
                type: "file",
                name: "cat001.jpg",
                path: "/animals/cat/images/cat001.jpg"
              }, {
                type: "file",
                name: "cat001.jpg",
                path: "/animals/cat/images/cat002.jpg"
              }
            ]
          }
        ]
      }
    ]
  }
];

Here's a coffeescript JSON:

data = 
[
  type: "folder"
  name: "animals"
  path: "/animals"
  children  :
    [
      type: "folder"
      name: "cat"
      path: "/animals/cat"
      children:
        [
          type: "folder"
          name: "images"
          path: "/animals/cat/images"
          children: 
            [
              type: "file"
              name: "cat001.jpg"
              path: "/animals/cat/images/cat001.jpg"
            , 
              type: "file"
              name: "cat001.jpg"
              path: "/animals/cat/images/cat002.jpg"
            ]
        ]
    ]
]

how to get this json data format in django views?(python)

episodeyang
  • 642
  • 8
  • 15
hagope
  • 5,523
  • 7
  • 38
  • 52
  • 3
    This is a common requirement in acquiring d3.js hierarchical data. I'd like to tag this question with d3.js but Stack Overflow allows a maximum of 5 :( – Sridhar Sarnobat Sep 06 '16 at 20:31
  • 1
    I wish one of these answers would read the paths from stdin, so that you can convert a list of paths into a json object, like this: `find | paths2json`. That would leverage the full power of Unix composability through pipelines. – Sridhar Sarnobat Sep 06 '16 at 20:39

8 Answers8

75

Here's a sketch. Error handling is left as an exercise for the reader.

var fs = require('fs'),
    path = require('path')

function dirTree(filename) {
    var stats = fs.lstatSync(filename),
        info = {
            path: filename,
            name: path.basename(filename)
        };

    if (stats.isDirectory()) {
        info.type = "folder";
        info.children = fs.readdirSync(filename).map(function(child) {
            return dirTree(filename + '/' + child);
        });
    } else {
        // Assuming it's a file. In real life it could be a symlink or
        // something else!
        info.type = "file";
    }

    return info;
}

if (module.parent == undefined) {
    // node dirTree.js ~/foo/bar
    var util = require('util');
    console.log(util.inspect(dirTree(process.argv[2]), false, null));
}
Miikka
  • 4,573
  • 34
  • 47
  • 3
    This works great on the 1st layer, however, children look like: children: [Object] ... do you see any issue here? – hagope Jun 26 '12 at 06:00
  • 3
    Yeah. The created object is fine, but by default console.log prints objects only to a limited depth. I edited the code to print the full tree. – Miikka Jun 26 '12 at 09:56
  • 6
    Thank you for func. I guess better use path.join instead of `a + '/' + b`. `return dirTree( path.join(filename, child));` – tuchk4 Nov 26 '14 at 10:17
  • How would one go about ordering the output such that directories appear first (in alphabetical order) followed by files (also in alphabetical order)? – Peter Butcher Jul 29 '15 at 10:37
  • @peterButcher How would you order them if its printed in a tree structure? But you can use lodash to structure the returned object.. Its just a regular object, so sort it like you would anything else :) – Justin Mar 12 '16 at 01:59
  • @Justin I had my question answered here: http://stackoverflow.com/questions/31699673/order-nested-json-by-property-using-nodejs/31701158#31701158 – Peter Butcher Mar 12 '16 at 02:06
  • I'm not commenting on the correctness of this answer, I just wanted to thank the author for that phrase which I'll be adapting for all future projects: "bug handling is left as an exercise for the client" – Dave Seidman Feb 02 '21 at 09:34
38

there's an NPM Module for it

https://www.npmjs.com/package/directory-tree

Creates an object representing a directory tree.

From:

photos
├── summer
│   └── june
│       └── windsurf.jpg
└── winter
    └── january
        ├── ski.png
        └── snowboard.jpg

To:

{
  "path": "",
  "name": "photos",
  "type": "directory",
  "children": [
    {
      "path": "summer",
      "name": "summer",
      "type": "directory",
      "children": [
        {
          "path": "summer/june",
          "name": "june",
          "type": "directory",
          "children": [
            {
              "path": "summer/june/windsurf.jpg",
              "name": "windsurf.jpg",
              "type": "file"
            }
          ]
        }
      ]
    },
    {
      "path": "winter",
      "name": "winter",
      "type": "directory",
      "children": [
        {
          "path": "winter/january",
          "name": "january",
          "type": "directory",
          "children": [
            {
              "path": "winter/january/ski.png",
              "name": "ski.png",
              "type": "file"
            },
            {
              "path": "winter/january/snowboard.jpg",
              "name": "snowboard.jpg",
              "type": "file"
            }
          ]
        }
      ]
    }
  ]
}

Usage

var tree = directoryTree('/some/path');

And you can also filter by extensions:

var filteredTree = directoryTree('/some/path', ['.jpg', '.png']);
Asaf Katz
  • 4,608
  • 4
  • 37
  • 42
21

The accepted answer works, but it is synchronous and will deeply hurt your performance, especially for large directory trees.
I highly encourage you to use the following asynchronous solution, it is both faster and non-blocking.
Based on the parallel solution here.

var fs = require('fs');
var path = require('path');

var diretoryTreeToObj = function(dir, done) {
    var results = [];

    fs.readdir(dir, function(err, list) {
        if (err)
            return done(err);

        var pending = list.length;

        if (!pending)
            return done(null, {name: path.basename(dir), type: 'folder', children: results});

        list.forEach(function(file) {
            file = path.resolve(dir, file);
            fs.stat(file, function(err, stat) {
                if (stat && stat.isDirectory()) {
                    diretoryTreeToObj(file, function(err, res) {
                        results.push({
                            name: path.basename(file),
                            type: 'folder',
                            children: res
                        });
                        if (!--pending)
                            done(null, results);
                    });
                }
                else {
                    results.push({
                        type: 'file',
                        name: path.basename(file)
                    });
                    if (!--pending)
                        done(null, results);
                }
            });
        });
    });
};

Example usage:

var dirTree = ('/path/to/dir');

diretoryTreeToObj(dirTree, function(err, res){
    if(err)
        console.error(err);

    console.log(JSON.stringify(res));
});
Community
  • 1
  • 1
LifeQuery
  • 3,202
  • 1
  • 26
  • 35
  • 1
    minor nit: you have a typo in diretoryTreeToObj, i think it should be directoryTreeToObj no? – mcnutt Aug 09 '18 at 18:28
3

My CS example (w/ express) based on Miika's solution:

fs = require 'fs' #file system module
path = require 'path' # file path module

# returns json tree of directory structure
tree = (root) ->
    # clean trailing '/'(s)
    root = root.replace /\/+$/ , ""
    # extract tree ring if root exists
    if fs.existsSync root
        ring = fs.lstatSync root
    else
        return 'error: root does not exist'
    # type agnostic info
    info = 
        path: root
        name: path.basename(root)
    # dir   
    if ring.isDirectory()
        info.type = 'folder'
        # execute for each child and call tree recursively
        info.children = fs.readdirSync(root) .map (child) ->
            tree root + '/' + child
    # file
    else if ring.isFile()
        info.type = 'file'
    # link
    else if ring.isSymbolicLink()
        info.type = 'link'
    # other
    else
        info.type = 'unknown'
    # return tree 
    info

# error handling
handle = (e) ->
    return 'uncaught exception...'

exports.index = (req, res) ->
    try
        res.send tree './test/'
    catch e
        res.send handle e
fooling
  • 111
  • 2
2

Here is an async solution:

 function list(dir) {
   const walk = entry => {
     return new Promise((resolve, reject) => {
       fs.exists(entry, exists => {
         if (!exists) {
           return resolve({});
         }
         return resolve(new Promise((resolve, reject) => {
           fs.lstat(entry, (err, stats) => {
             if (err) {
               return reject(err);
             }
             if (!stats.isDirectory()) {
               return resolve({
                 // path: entry,
                 // type: 'file',
                 name: path.basename(entry),
                 time: stats.mtime,
                 size: stats.size
               });
             }
             resolve(new Promise((resolve, reject) => {
               fs.readdir(entry, (err, files) => {
                 if (err) {
                   return reject(err);
                 }
                 Promise.all(files.map(child => walk(path.join(entry, child)))).then(children => {
                   resolve({
                     // path: entry,
                     // type: 'folder',
                     name: path.basename(entry),
                     time: stats.mtime,
                     entries: children
                   });
                 }).catch(err => {
                   reject(err);
                 });
               });
             }));
           });
         }));
       });
     });
   }

   return walk(dir);
 }

Note that when a directory does not exist, an empty result is returned rather than an error being thrown.

Here is a sample result:

{
    "name": "root",
    "time": "2017-05-09T07:46:26.740Z",
    "entries": [
        {
            "name": "book.txt",
            "time": "2017-05-09T07:24:18.673Z",
            "size": 0
        },
        {
            "name": "cheatsheet-a5.pdf",
            "time": "2017-05-09T07:24:18.674Z",
            "size": 262380
        },
        {
            "name": "docs",
            "time": "2017-05-09T07:47:39.507Z",
            "entries": [
                {
                    "name": "README.md",
                    "time": "2017-05-08T10:02:09.651Z",
                    "size": 19229
                }
            ]
        }
    ]
}

which will be:

root
|__ book.txt
|__ cheatsheet-a5.pdf
|__ docs
      |__ README.md
Sean C.
  • 1,494
  • 1
  • 13
  • 22
1

You can use the code from this project but you should adapt the code to your needs:

https://github.com/NHQ/Node-FileUtils/blob/master/src/file-utils.js#L511-L593

From:

a
|- b
|  |- c
|  |  |- c1.txt
|  |
|  |- b1.txt
|  |- b2.txt
|
|- d
|  |
|
|- a1.txt
|- a2.txt

To:

{
    b: {
        "b1.txt": "a/b/b1.txt",
        "b2.txt": "a/b/b2.txt",
        c: {
            "c1.txt": "a/b/c/c1.txt"
        }
    },
    d: {},
    "a2.txt": "a/a2.txt",
    "a1.txt": "a/a1.txt"
}

Doing:

new File ("a").list (function (error, files){
    //files...
});
Gabriel Llamas
  • 18,244
  • 26
  • 87
  • 112
0

I used 'walk' lib in this case, it gets your root path and walks over files and over directories recursively and emits an event of directory / file with all the info you need from a node, check out that implementation -->

const walk = require('walk');

class FsTree {

    constructor(){

    }

    /**
     * @param rootPath
     * @returns {Promise}
     */
    getFileSysTree(rootPath){
        return new Promise((resolve, reject)=>{

            const root = rootPath || __dirname; // if there's no rootPath use exec location
            const tree = [];
            const nodesMap = {};
            const walker  = walk.walk(root, { followLinks: false}); // filter doesn't work well

            function addNode(node, path){
                if ( node.name.indexOf('.') === 0 || path.indexOf('/.') >= 0){ // ignore hidden files
                    return;
                }
                var relativePath = path.replace(root,'');

                node.path = relativePath + '/' + node.name;
                nodesMap[node.path] = node;

                if ( relativePath.length === 0 ){ //is root
                    tree.push(node);
                    return;
                }
                node.parentPath = node.path.substring(0,node.path.lastIndexOf('/'));
                const parent = nodesMap[node.parentPath];
                parent.children.push(node);

            }

            walker.on('directory', (path, stats, next)=>{
                addNode({ name: stats.name, type:'dir',children:[]}, path);
                next();
            });

            walker.on('file', (path,stats,next)=>{
                addNode({name:stats.name, type:'file'},path);
                next();
            });

            walker.on('end',()=>{
                resolve(tree);
            });

            walker.on('errors',  (root, nodeStatsArray, next) => {
                reject(nodeStatsArray);
                next();
            });
        });

    }
}


const fsTreeFetcher = new FsTree();

fsTreeFetcher.getFileSysTree(__dirname).then((result)=>{
    console.log(result);
});
liron_hazan
  • 1,396
  • 2
  • 19
  • 27
0

Adding up on Sean C.'s answer.

I quite like it, but using async await makes it much more readable.

import fs from 'fs';
import {
  lstat,
  readdir,
  access,
} from 'fs/promises';
import path from 'path';

async function existsAsync(file) {
  try {
    await access(file, fs.constants.F_OK);
    return true;
  } catch (e) {
    return false;
  }
}

async function listFileTreeRecursive(dir) {
  const recurse = async (entry) => {
    if (!(await existsAsync(entry))) {
      return {};
    }

    const stats = await lstat(entry);
    if (!stats.isDirectory()) {
      return {
        name: path.basename(entry),
        time: stats.mtime,
        size: stats.size,
      };
    }

    const files = await readdir(entry);
    const childEntries = await Promise.all(
      files.map((child) => recurse(path.join(entry, child))),
    );
    return {
      name: path.basename(entry),
      time: stats.mtime,
      entries: childEntries,
    };
  };

  return recurse(dir);
}
Omar Omeiri
  • 1,506
  • 1
  • 17
  • 33