Ask a Jedi: Dumping a Recursive Directory List

A reader (nicely) asked me something before I left for Boston, and I never got around to answering. He had an interesting problem. He wanted to list directories and files, in a recursive fashion, using HTML's unordered list display to handle the directories and their children.

Now I thought this was a simple thing - just use the recurse=true option in <cfdirectory>. However - the more I thought about it - the more difficult it seemed. You can sort the <cfdirectory> result - but not in an way you can simply output with HTML.

My first thought was to switch back to a recursive <cfdirectory>, and while that would work, I assumed I'd lose a lot in terms of speed due to all the file operations. So what I came up with was a mix of recursive CFML and the built-in recursive <cfdirectory> tag:

<cfset initialDir = "c:\apache2\htdocs\testingzone\blogcfc_flex2"> <cfdirectory directory="#initialDir#" recurse="yes" name="files" sort="directory asc">

<cfset display(files,initialDir)>

<cffunction name="display" returnType="void" output="true"> <cfargument name="files" type="query" required="true"> <cfargument name="parent" type="string" required="true"> <cfset var justMyKids = "">

<cfquery name="justMyKids" dbtype="query"> select * from arguments.files where directory = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.parent#"> </cfquery>

<cfoutput><ul></cfoutput>

<cfoutput query="justMyKids"> <li>#directory##name#</li> <cfif type is "Dir"> #display(arguments.files, directory & "" & name)# </cfif> </cfoutput>

<cfoutput></ul></cfoutput>

</cffunction>

As you can see, I do the initial <cfdirectory> and have it fetch all the files. The UDF simply handles displaying items from the query. I don't normally do output from UDFs, so to be honest, I feel a bit dirty. I'd probably just wrap it up in a cfsavecontent and return that, but this was written in about 5 minutes. Another problem - note I hard code \ as my file delimiter. I could have made this more dynamic by using a Java call:

<cfset separator = createObject("java","java.io.File").separator>

In general, the use of "/" will work just fine in any OS, however, since I was doing a string comparison in my query, I'd probably want to use the same separator CF used.

Archived Comments

Comment 1 by todd posted on 2/7/2006 at 7:38 AM

This is really ironic...I was thinking about this exact topic on my way home from work today (although with a slightly different output). Thanks a lot! This will definitely help.

Comment 2 by emmet posted on 2/7/2006 at 7:57 AM

More on the subject of recursion and coincidence. I just came across this tonite before popping over here. http://rickosborne.org/blog...

I'm interested to see how you handle db sorting and recursion Ray. My method has always been on the sloppy side. Including the same file over and over again until there are no more parents to be found.

Comment 3 by Raymond Camden posted on 2/7/2006 at 7:43 PM

Emmet - well, that is an example of recursion actually. But I know what you mean - recursion can get messy and is a pain to debug at times.

Comment 4 by Doug posted on 2/7/2006 at 7:49 PM

Minor nit-pick: Nested lists should have the sub list between the li tags of the parent, not after.

Simply moving the closing li in your function so it's after the recursive call to the function accomplishes this.

I only point this out because it's a handy function if you use a menuing system like udm4 (www.udm4.com), but it won't work if the nesting isn't proper.

Comment 5 by Raymond Camden posted on 2/7/2006 at 7:53 PM

Thanks Doug - I was a bit unsure on that.

Comment 6 by Daniel Greenfeld posted on 2/7/2006 at 8:01 PM

Nice, simple, and elegant! Great!

Comment 7 by Doug posted on 2/7/2006 at 9:52 PM

emmet - For database hierarchies, I tend to prefer to let the database do the work. The only downside is that different RDBMS' have different methods of approaching the problem.

Oracle has by far the best method of handling hierarchical data with the "start with...connect by prior" syntax.

Given a table with id, name and parentid columns, you can get a sorted tree result like so:

select name, level
from table
start with parentid is null
connect by prior id = parentid
order siblings by name asc

The level column is a pseudo column that gives the depth of each row in the hierarchy, so the root is level 1, it's children level 2, and so on.

The "order siblings by" sorts the results appropriately within each node (in this case, alphabetically by name).

You can get a similar result in other databases using CTE (Common Table Expressions), but not nearly so easily. Here's an article that compares the two methods:

http://www-128.ibm.com/deve...

In my experience, from a strictly performance standpoint, either method is preferrable to doing the sorting in the application, particularly if the dataset is large.

That being said, if your requirements dictate database portability, Rick Osborne's method in the link you cited seems to be a pretty decent approach, although I haven't tested it out myself.

Comment 8 by Emmet posted on 2/7/2006 at 10:04 PM

Im on MSSQL. Oracle is but a pipe dream. I never really had a problem with my method until I noticed how horribly one of our clients apps are now performing. It's an older CF5 app and I dont think it was ever expected to grow to the level it has. It's now taking over 1000ms to generate a tree. Now that were on CFMX7 I need to explore some other options.

Comment 9 by Doug posted on 2/7/2006 at 10:20 PM

For MSSQL, take a look here:
http://msdn2.microsoft.com/...

This covers using CTEs in MSSQL, and combined with the ibm.com link in my previous comment should give you a solid foundation if you want to try this approach.

Comment 10 by Christopher Wigginton posted on 2/8/2006 at 12:52 AM

Though it's not an unordered list, a tree display might be a better solution and easier. I just whipped up an example and added it as a trackback.

Ray... Forgive the title, I couldn't resist :-)

Wiggy

Comment 11 by emmet posted on 2/8/2006 at 2:08 AM

Thanks Doug. Thats alot to wrap my head around.

Comment 12 by todd posted on 2/8/2006 at 2:47 AM

Don't forget Forta's example:
http://www.forta.com/blog/i...

Comment 13 by Doug Wilder posted on 3/10/2006 at 1:19 AM

I have two dropdown menus in a form. The first is populated with directories using cfdirectory (looping thru to find any of type="dir"). The second dropdown needs to be a list of subdirectories that fall under whatever directory the user chooses in the first dropdown menu. Below is the code I've tried but it's not working. Seems like I need something in the onchange event. I've looked at the cf_twoselectsrelated but can't see how to implement it when the query is a set of directories. I don't see exactly how to use the recurse="true" (or "yes"?) either since it looks like all that will do is put the subdirectories into the same dropdown box as their parent directories. Ultimately, I want to allow users to choose at least upto two directory levels for uploading files. For that, I need the parent dir name and subdir name.Any help/ideas is greatly appreciated!
Doug in Fairbanks

<p>Choose top level folder:<br />
<select name="mnuFolder" onchange="">
<cfdirectory directory="D:\myplace\" name="Parent_Folder">
<cfloop query="Parent_Folder">
<cfif #Parent_Folder.Type# eq "Dir">
<cfoutput>
<option value="#Parent_Folder.Name#\">#Parent_Folder.Name#</option>
</cfoutput>
</cfif>
</cfloop>
</select><br />
Choose subfolder (if any):<br />
<select name="mnuSubFolder">
<cfdirectory directory="D:\myplace\#Parent_Folder.Name#\" name="Sub_Folder">
<cfloop query="Sub_Folder">
<cfif #Sub_Folder.Type# eq "Dir">
<cfoutput>
<option value="#Sub_Folder.Name#\">#Sub_Folder.Name#</option>
</cfoutput>
</cfif>
</cfloop>
</select><br />

Comment 14 by Raymond Camden posted on 3/10/2006 at 1:28 AM

What you need is dependant selects. You can find many posts on that if you google. It is a bit too much to discuss here.

Comment 15 by Lamisaunet posted on 5/12/2006 at 11:28 PM

After three days, I've found an easier way :

<cfparam default="#GetDirectoryFromPath(GetTemplatePath())#/showcase" name="repertoire"/>
<cfdirectory action="list" directory="#repertoire#" name="allDirectories" recurse="true"/>
<cfoutput query="allDirectories" group="name">
<cfif #allDirectories.type# IS "DIR">
<h3>#allDirectories.name#</h3>
<cfelse>
<pre>#allDirectories.name#</pre>
</cfif>
</cfoutput>

Comment 16 by Doug Wilder posted on 6/7/2006 at 12:37 AM

Isn't this going to list all directories in one list? What I need is two selects with the first listing top-level directories and the second listing subdirectories of the top-level directory selected in the first select. I searched for "independent selects" but am coming up empty. Sorry if this is a bit much for this site but this is the closest I've come to a solution.

Thanks,
Doug Wilder

Comment 17 by Dylan posted on 5/11/2007 at 9:10 PM

Could someone share an example of how to call this function? I'm new to using UDF, and I'm unsure how to input the correct values for the arguments.

Comment 18 by Raymond Camden posted on 5/11/2007 at 10:24 PM

Please check the CF Docs on how to use UDFs. In general though you call them like any other function. The arguments for the UDF in this entry are first a query of files, then the path to the initial directory.

Comment 19 by Dylan posted on 5/11/2007 at 10:26 PM

ah, a query of files. that's what I needed to know. Thanks.

Comment 20 by glen posted on 7/27/2009 at 6:07 AM

Using Ray's example as a starting point I have tweaked it to create a recursive function that 'copies' a folder structure, 3 deep and excludes files.

http://www.stinkylittlefrie...