12

I need to recursively traverse a directory and create a tree to be used with the jsTree control. The control accepts a JSON format like so. I need some ruby magic to make this happen cleanly and quickly.

Any help is appreciated.

Mem
  • 123
  • 1
  • 1
  • 5

4 Answers4

25

You probably want something like this (untested):

def directory_hash(path, name=nil)
  data = {:data => (name || path)}
  data[:children] = children = []
  Dir.foreach(path) do |entry|
    next if (entry == '..' || entry == '.')
    full_path = File.join(path, entry)
    if File.directory?(full_path)
      children << directory_hash(full_path, entry)
    else
      children << entry
    end
  end
  return data
end

Recursively walk down the tree, building up a hash. Turn it into json with your favourite serialisation library.

Glenjamin
  • 7,150
  • 6
  • 25
  • 26
  • I made a small change to prevent it traversing too far: `next if (entry == '..' || entry == '.')` Thanks a lot for your help. I really appreciate it. – Mem Apr 03 '11 at 18:12
7

First take your tree, convert it to a list of paths to leaves, similar to:

def leaves_paths tree
  if tree[:children]
    tree[:children].inject([]){|acc, c|
      leaves_paths(c).each{|p|
        acc += [[tree[:name]] + p]
      }
      acc
    }
  else
    [[tree[:name]]]
  end
end

(Not sure if above exactly follows your jsTree structure, but the principle is the same.)

Here's a sample of input and output:

tree = {name: 'foo', children: [
      {name: 'bar'},
      {name: 'baz', children: [
        {name: 'boo'}, 
        {name: 'zoo', children: [
          {name: 'goo'}
        ]}
      ]}
    ]}

p leaves_paths tree
#=> [["foo", "bar"], ["foo", "baz", "boo"], ["foo", "baz", "zoo", "goo"]]

Then, for each path, call FileUtils#mkdir_p:

paths = leaves_paths tree
paths.each do |path|
  FileUtils.mkdir_p(File.join(*path))
end

And you should be ok.

Edit: Simpler version:

You don't need to create list of leaves, just traverse whole tree and create a directory for every node:

# executes block on each tree node, recursively, passing the path to the block as argument
def traverse_with_path tree, path = [], &block
  path += [tree[:name]]
  yield path
  tree[:children].each{|c| traverse_with_path c, path, &block} if tree[:children]
end

traverse_with_path tree do |path|
  FileUtils.mkdir(File.join(*path))
end

Edit2:

Oh, sorry, I misunderstood. So, here's a way to make a Hash based on directory tree on disk:

Dir.glob('**/*'). # get all files below current dir
  select{|f|
    File.directory?(f) # only directories we need
  }.map{|path|
    path.split '/' # split to parts
  }.inject({}){|acc, path| # start with empty hash
    path.inject(acc) do |acc2,dir| # for each path part, create a child of current node
      acc2[dir] ||= {} # and pass it as new current node
    end
    acc
  }

So, for the following structure:

#$ mkdir -p foo/bar
#$ mkdir -p baz/boo/bee
#$ mkdir -p baz/goo

code above returns this hash:

{
  "baz"=>{
    "boo"=>{
      "bee"=>{}},
    "goo"=>{}},
  "foo"=>{
    "bar"=>{}}}

Hope you'll manage to suit it to your needs.

Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • Hey, thanks a lot for the response. I might have been unclear in my original post but I dont need to actually create any directories but recursively list all existing files/directories given a path. – Mem Apr 02 '11 at 09:12
  • Thanks a lot for your help. The code works great but only grabs directories, nonetheless simple enough to modify. Appreciate it. – Mem Apr 03 '11 at 18:12
  • this helped me purely for the Mkdir_p() – New Alexandria Oct 03 '12 at 19:46
  • 2
    You don't have to use `Dir.glob('**/*')` + predicate `File.directory?(f)` . 2 stars with slash is enough (`Dir.glob('**/)`) – Darek Nędza Feb 07 '15 at 20:57
  • I can't visulize how to fill files of directory inside the directory hash. Can you explain? – drmwndr Mar 03 '21 at 21:12
2

Ruby's Find module (require 'find') is minimalist but handles directory recursion well: http://www.ruby-doc.org/stdlib/libdoc/find/rdoc/classes/Find.html

Asherah
  • 18,948
  • 5
  • 53
  • 72
  • Thanks for your quick reply. I'm trying to use Find but am unsure of how to create the structure thats necessary for the tree (e.g. one directory has children which are sub-directories and those have children, etc.). – Mem Apr 02 '11 at 01:40
1

The accepted answer did not work as of June 2015. I changed the key :data to 'text'. I also generalized the code to exclude directories and files.

def directory_hash(path, name=nil, exclude = [])                                
  exclude.concat(['..', '.', '.git', '__MACOSX', '.DS_Store'])                  
  data = {'text' => (name || path)}                                             
  data[:children] = children = []                                               
  Dir.foreach(path) do |entry|                                                  
    next if exclude.include?(entry)                                             
    full_path = File.join(path, entry)                                          
    if File.directory?(full_path)                                               
      children << directory_hash(full_path, entry)                              
    else                                                                        
      children << {'icon' => 'jstree-file', 'text' => entry}                    
    end                                                                         
  end                                                                           
  return data                                                                   
end  
Martin Velez
  • 1,379
  • 11
  • 24