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.

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

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

5 Comments

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.
Thanks a lot for your help. The code works great but only grabs directories, nonetheless simple enough to modify. Appreciate it.
this helped me purely for the Mkdir_p()
You don't have to use Dir.glob('**/*') + predicate File.directory?(f) . 2 stars with slash is enough (Dir.glob('**/))
I can't visulize how to fill files of directory inside the directory hash. Can you explain?
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

2 Comments

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.).
FYI link no longer works.
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  

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.