Skip to main content
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Justin Alpino
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Justin Alpino ( @jalpino )

ColdFusion File Explorer - Round One

By on
Tags:

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>

Want to use code from this post? Check out the license.

Reader Comments

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

15,663 Comments

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.

15,663 Comments

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!

3 Comments

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.

15,663 Comments

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.

19 Comments

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.

15,663 Comments

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

16 Comments

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.

1 Comments

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?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel