Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2010) with:

ColdFusion File Explorer - Round One

By Ben Nadel on
Tags: ColdFusion

A while back, I promised that I would post the code I used for the ColdFusion file explorer used in Skin Spider. Things have been crazy here so I have been getting to my task list very slowly. Last night I sat down and started cleaning this thing up. I started from scratch to try out a new technique. So far, I am liking where this is going. It funny, the hard part is not the code display, it's the directory / file navigation.

Right now, it reads in all the directories and files through a recursive CFDirectory. This can be VERY slow on large directories. The file data itself is gotten via AJAX calls so that the page does not have to reload. What I would like is the file list and the sub-directory list to also be read in via AJAX so that the CFDirectory tag does NOT have to recurse. This will make it much much faster and easier to use. That will be part of round two.

Currently there is no color coding, but like I said, I feel that part (especially since I am using PRE tags) is the easy part. Navigating the large directories is the hard part.

You can DEMO the round-one ColdFusion file explorer here. It's all contained in one file (which is how I want to keep it) so that it can be picked up and dropped anywhere. I am taking security measures to make sure that only files within the root directory can be selected. The code knows to eliminate reference to things like "..\" or "\\".

Here is the code (more to come later):

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!---
  • Set the directory that we are going to be viewing. This is
  • the ROOT directory. We will be able to view files that are
  • in sub-directories of this one.
  • --->
  • <cfset REQUEST.RootDirectory = ExpandPath( "../" ) />
  •  
  •  
  •  
  • <!--- Param the URL variables. --->
  • <cfparam
  • name="URL.file"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!--- Get the proper slash. --->
  • <cfset REQUEST.Slash = Left(
  • REQUEST.RootDirectory.ReplaceAll(
  • "[^\\/]+",
  • ""
  • ),
  • 1
  • ) />
  •  
  •  
  • <!--- Check to see if any file is being requested. --->
  • <cfif Len( URL.file )>
  •  
  • <!--- Get the target file. --->
  • <cfset REQUEST.TargetFile = UrlDecode(REQUEST.RootDirectory & URL.file) />
  •  
  • <!--- Remove any sneaky navigation hacks. --->
  • <cfset REQUEST.TargetFile = REQUEST.TargetFile.ReplaceAll( "(\.\.[\\/]{1})|([\\/]{2,})", "" ) />
  •  
  • <!--- Check to see if the file exists. --->
  • <cfif (
  • FileExists( REQUEST.TargetFile ) AND
  • REFindNoCase( "\.(aspx?|cfc|cfml?|css|csv|dtd|html?|java|js|php|sql|txt|xml)$", REQUEST.TargetFile )
  • )>
  •  
  • <!--- Read in the file. --->
  • <cffile
  • action="READ"
  • file="#REQUEST.TargetFile#"
  • variable="REQUEST.FileData"
  • />
  •  
  •  
  • <!--- Escape the code. --->
  • <cfset REQUEST.FileData = REQUEST.FileData.ReplaceAll( "<", "&lt;" ) />
  • <cfset REQUEST.FileData = REQUEST.FileData.ReplaceAll( ">", "&gt;" ) />
  •  
  •  
  • <!--- Stream the file content to the browser. --->
  • <cfcontent
  • type="text/plain"
  • variable="#ToBinary( ToBase64( REQUEST.FileData ) )#"
  • />
  •  
  • <cfelseif FileExists( REQUEST.TargetFile )>
  •  
  • <!--- The file exists but is not a text document. --->
  • <cfcontent
  • type="text/plain"
  • variable="#ToBinary( ToBase64( 'The requested file [ #URL.file# ] is not a readable text document.' ) )#"
  • />
  •  
  • <cfelse>
  •  
  • <!--- The file does not exists. Return a file not found text. --->
  • <cfcontent
  • type="text/plain"
  • variable="#ToBinary( ToBase64( 'The requested file [ #URL.file# ] could not be found.' ) )#"
  • />
  •  
  • </cfif>
  •  
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, if a file has been requested, it has been found,
  • processed, and returned. We will only reache THIS point if NO file has
  • been requested.
  • --->
  •  
  •  
  • <!--- Get the files from the root directory. --->
  • <cfdirectory
  • action="LIST"
  • directory="#REQUEST.RootDirectory#"
  • name="REQUEST.FileQuery"
  • recurse="true"
  • />
  •  
  •  
  • <cffunction name="OutputDirectory" access="public" returntype="any" output="false"
  • hint="Output the list of directories.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Buffer" type="any" required="true" />
  • <cfargument name="FileQuery" type="query" required="true" />
  • <cfargument name="ParentDirectory" type="string" required="true" />
  • <cfargument name="Depth" type="numeric" default="0" required="false" />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Make sure the parent directory doesn't have a trailing "/". --->
  • <cfset ARGUMENTS.ParentDirectory = ARGUMENTS.ParentDirectory.ReplaceAll(
  • "[\\/]+$",
  • ""
  • ) />
  •  
  •  
  • <!--- Add the root directory if we are at the zero depth. --->
  • <cfif NOT ARGUMENTS.Depth>
  •  
  • <!--- Output the root level directory first. --->
  • <cfset ARGUMENTS.Buffer.Append(
  • "<a href=""javascript:ShowFiles( '#Hash( ARGUMENTS.ParentDirectory )#' );"">#GetFileFromPath( ARGUMENTS.ParentDirectory )#</a>"
  • ) />
  •  
  • <!--- Increment the depth by one. --->
  • <cfset ARGUMENTS.Depth = (ARGUMENTS.Depth + 1) />
  •  
  • </cfif>
  •  
  • <!--- Query for files. --->
  • <cfquery name="LOCAL.Directory" dbtype="query">
  • SELECT
  • name
  • FROM
  • ARGUMENTS.FileQuery
  • WHERE
  • type = 'Dir'
  • AND
  • directory = <cfqueryparam value="#ARGUMENTS.ParentDirectory#" cfsqltype="CF_SQL_VARCHAR" />
  • ORDER BY
  • name ASC
  • </cfquery>
  •  
  • <!--- Output the directories. --->
  • <cfloop query="LOCAL.Directory">
  •  
  • <cfset ARGUMENTS.Buffer.Append(
  • "<a href=""javascript:ShowFiles( '#Hash( ARGUMENTS.ParentDirectory & REQUEST.Slash & LOCAL.Directory.name )#' );"">#RepeatString( "../", ARGUMENTS.Depth )##LOCAL.Directory.name#</a>"
  • ) />
  •  
  • <!--- Output the sub files / directories. --->
  • <cfset OutputDirectory(
  • Buffer = ARGUMENTS.Buffer,
  • FileQuery = ARGUMENTS.FileQuery,
  • ParentDirectory = (ARGUMENTS.ParentDirectory & REQUEST.Slash & LOCAL.Directory.name),
  • Depth = (ARGUMENTS.Depth + 1)
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Return the buffer. --->
  • <cfreturn ARGUMENTS.Buffer />
  • </cffunction>
  •  
  •  
  • <cffunction name="OutputDirectoryFiles" access="public" returntype="any" output="false"
  • hint="Outputs the list of files for each directory.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Buffer" type="any" required="true" />
  • <cfargument name="FileQuery" type="query" required="true" />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Query for directories. --->
  • <cfquery name="LOCAL.Directory" dbtype="query">
  • (
  • SELECT DISTINCT
  • (directory + '#REQUEST.Slash#' + name ) AS name
  • FROM
  • ARGUMENTS.FileQuery
  • WHERE
  • type = 'Dir'
  •  
  • )
  •  
  • UNION
  •  
  • <!--- Make sure that the ROOT directory is part of the directory list. --->
  • (
  • SELECT DISTINCT
  • ( '#REQUEST.RootDirectory.ReplaceAll( "[\\/]$", "" )#' ) AS name
  • FROM
  • ARGUMENTS.FileQuery
  • )
  • </cfquery>
  •  
  •  
  • <!--- Output the directories. --->
  • <cfloop query="LOCAL.Directory">
  •  
  • <!--- Query for files. --->
  • <cfquery name="LOCAL.File" dbtype="query">
  • SELECT
  • name,
  • directory
  • FROM
  • ARGUMENTS.FileQuery
  • WHERE
  • type = 'File'
  • AND
  • directory = <cfqueryparam value="#LOCAL.Directory.name#" cfsqltype="CF_SQL_VARCHAR" />
  • ORDER BY
  • name ASC
  • </cfquery>
  •  
  • <cfset ARGUMENTS.Buffer.Append(
  • "<div id=""#Hash( ToString( LOCAL.Directory.name ).ReplaceFirst( "[\\/]+$", "" ) )#"" class=""filelist"">"
  • ) />
  •  
  • <!--- Output the files first. --->
  • <cfloop query="LOCAL.File">
  •  
  • <cfset ARGUMENTS.Buffer.Append(
  • "<a href=""javascript:LoadFile( '#Replace( (LOCAL.File.directory & REQUEST.Slash & LOCAL.File.name), REQUEST.RootDirectory, "", "ONE" ).ReplaceAll( "\\", "\\\\" )#' );"">#LOCAL.File.name#</a>"
  • ) />
  •  
  • </cfloop>
  •  
  • <cfset ARGUMENTS.Buffer.Append(
  • "</div>"
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Return the buffer. --->
  • <cfreturn ARGUMENTS.Buffer />
  • </cffunction>
  •  
  •  
  • <!--- Set page content and clear buffer. --->
  • <cfcontent
  • type="text/html"
  • reset="true"
  • />
  •  
  • </cfsilent>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>ColdFusion File Explorer</title>
  •  
  • <style type="text/css">
  •  
  • html,
  • body {
  • height: 100% ;
  • }
  •  
  • body {
  • font-family: verdana, arial, georgia ;
  • font-size: 10px ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • }
  •  
  • #directoryframe,
  • #fileframe {
  • background-color: #F0F0F0 ;
  • height: 50% ;
  • left: 0px ;
  • overflow: scroll ;
  • position: absolute ;
  • width: 20% ;
  • }
  •  
  • #directoryframe div.buffer,
  • #fileframe div.buffer {
  • padding: 7px 7px 7px 7px ;
  • }
  •  
  • #directoryframe a,
  • #fileframe a {
  • color: #333333 ;
  • display: block ;
  • line-height: 19px ;
  • font-size: 11px ;
  • padding: 0px 7px 0px 5px ;
  • text-decoration: none ;
  • white-space: nowrap ;
  • }
  •  
  • #directoryframe a:hover,
  • #fileframe a:hover {
  • border-color: #999999 ;
  • }
  •  
  • #fileframe {
  • top: 50% ;
  • }
  •  
  • div.filelist {
  • display: none ;
  • }
  •  
  • #codeframe {
  • float: left ;
  • height: 100% ;
  • left: 20% ;
  • overflow: scroll ;
  • position: absolute ;
  • width: 80% ;
  • }
  •  
  • #codeframe div.buffer {
  • padding: 15px 15px 15px 15px ;
  • }
  •  
  • pre {
  • font-size: 12px ;
  • }
  •  
  • </style>
  •  
  • <script type="text/javascript">
  •  
  • var objPrevFileList = null;
  •  
  • function ShowFiles( strID ){
  • var objFileList = document.getElementById( strID );
  •  
  • // Check to see if we have a file list.
  • if (objFileList){
  •  
  • // Check to see if we need to hide the previous files.
  • if (objPrevFileList){
  • objPrevFileList.style.display = "none";
  • }
  •  
  • // Show current file list.
  • objFileList.style.display = "block";
  •  
  • // Store the current into the prev files.
  • objPrevFileList = objFileList;
  •  
  • }
  •  
  • }
  •  
  •  
  • function LoadFile( strPath ){
  • var objRequest = null;
  •  
  • // Try to create the AJAX request object.
  • try {
  • objRequest = new XMLHttpRequest();
  • } catch ( errTryMicrosoft ){
  • // Try the MS xml object.
  • try {
  • objRequest = new ActiveXObject( "Msxml2.XMLHTTP" );
  • } catch ( errTryOtherMicrosoft ){
  • // Try secondary Microsoft method.
  • try {
  • objRequest = new ActiveXObject( "Microsoft.XMLHTTP" );
  • } catch ( errFailed ){
  • // None of the connection objects were created. Be sure to se the
  • // connection object back to null for later reference.
  • objRequest = null;
  • }
  • }
  • }
  •  
  • if (objRequest){
  •  
  • // Open the connection.
  • objRequest.open(
  • "GET", // Method of data delivery.
  • ("<cfoutput>#CGI.script_name#</cfoutput>?file=" + escape( strPath )), // The Url we are posting to.
  • true // Perform this async.
  • );
  •  
  • // Set the state change handler.
  • objRequest.onreadystatechange = function(){ ShowFile( objRequest ); };
  •  
  • objRequest.send();
  •  
  • }
  • }
  •  
  •  
  • function ShowFile( objRequest ){
  • if (objRequest.readyState == 4){
  • document.getElementById( "code" ).innerHTML = objRequest.responseText;
  • }
  • }
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <!-- BEGIN: Directory Frame. -->
  • <div id="directoryframe">
  • <div class="buffer">
  •  
  • <cfoutput>
  • #OutputDirectory(
  • Buffer = CreateObject( "java", "java.lang.StringBuffer" ).Init( "" ),
  • FileQuery = REQUEST.FileQuery,
  • ParentDirectory = REQUEST.RootDirectory,
  • Type = 'Dir'
  • ).ToString()#
  • </cfoutput>
  •  
  • </div>
  • </div>
  • <!-- END: Directory Frame. -->
  •  
  •  
  • <!-- BEGIN: File Frame. -->
  • <div id="fileframe">
  • <div class="buffer">
  •  
  • <cfoutput>
  • #OutputDirectoryFiles(
  • Buffer = CreateObject( "java", "java.lang.StringBuffer" ).Init( "" ),
  • FileQuery = REQUEST.FileQuery
  • ).ToString()#
  • </cfoutput>
  •  
  • </div>
  • </div>
  • <!-- END: File Frame. -->
  •  
  •  
  • <!-- BEGIN: Code Column. -->
  • <div id="codeframe">
  • <div class="buffer">
  •  
  • <!--- This is where the file data will go. --->
  • <pre id="code"></pre>
  •  
  • </div>
  • </div>
  • <!-- END: Code Column. -->
  •  
  • </body>
  • </html>
Tweet This Interesting post by @BenNadel - ColdFusion File Explorer - Round One Thanks my man — you rock the party that rocks the body!



Reader Comments

Ben,
Have you had a chance to checkout Spry yet? I think this could be accomplished much easier using Spry's new updateContent method. Its worth a look.

Reply to this Comment

Dan,

To be honest, I have not looked into SPRY at all. If you think it will help accomplish this, I will definitely check it out. Thanks for the tip.

Reply to this Comment

Tim,

That's a really cool post. Not sure how I missed it. The only problem with it is that I need the "directory" column from the CFDirectory result set and it seems like its only "name" and "all". But, certainly, that is very cool. In fact, if I end up going non-recursive, the "name" value will speed things up nicely.

Thanks!

Reply to this Comment

You could also go the pure java way too. That way, you get to choose what data you need. cfdirectory will return everything regardless of if you need it or not.

Reply to this Comment

Yeah, I might try that as an optimization once I get the rest of it up and running. I also want to check out the SPRY stuff that Dan mentioned above, although I am not sure which aspect of it SPRY would handle.

Reply to this Comment

Did anything ever come of this? I have been trying to find a good way to browse a huge directory structure in a pleasing way.

Reply to this Comment

@Kevin,

Not really. I used it from time to time when demoing code that didn't fit entirely with a single blog post in an easy way (typically with a demo project). I never got it to where I really wanted it. That was before I really got better with jQuery. Now that I know jQuery much better, I think I could get this viewer in a nicer way.

Reply to this Comment

I've had some code on my site for awhile that was meant to be a tutorial for a file browser. I finally decided to release "my internal version" that I had been using, to the public:

http://www.cjboco.com/projects.cfm/project/cj-file-browser/3.1.0

It's interface is completely written in jQuery and HTML (No Flash or SWF!) and relies on a "plug-in" architecture for the handler system. Right now the only handler plug-in is ColdFusion 8. But I'm writing a PHP5 handler plug-in as well. (Some of my clients are CF8 and some are PHP, so I wanted to create a universal file browser)

It's about 1000x more complicated that my previous versions, but you can dig into the code to figure things out. I commented the heck out it. Anyway, take a look you might like it.

Reply to this Comment

Ben - is there anything like this that would allow access outside of the file structure where the code lives?

Say within a network share on a company intranet?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.