ColdFusion File Explorer - Round One

Posted December 27, 2006 at 9:16 AM

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):

 Launch code in new window » Download code as text file »

  • <!--- 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>

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page





Reader Comments

Dec 27, 2006 at 9:36 AM // reply »
45 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.


Dec 27, 2006 at 9:41 AM // reply »
7,572 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.


Dec 27, 2006 at 9:56 AM // reply »
3 Comments

You mentioned that your cfdirectory call is slow. Have you tried any of the techniques discussed in these posts?

http://www.forta.com/blog/index.cfm/2006/11/1/improving-cfdirectory-performance

Specifically using the undocumented LISTINFO="name" attribute of cfdirectory? That might help speed things up.


Dec 27, 2006 at 10:05 AM // reply »
7,572 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!


Dec 27, 2006 at 10:19 AM // reply »
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.


Dec 27, 2006 at 10:21 AM // reply »
7,572 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.


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 19, 2010 at 12:55 PM
Content Is Not Allowed In Prolog - ColdFusion XML And The Byte-Order-Mark (BOM)
Thank you! Thank you! Thank you! One more additional bit to add to this: in addition to the, "Content is not allowed in Prolog," error solved by Ben's REReplace, I was also getting, "An invalid XML ... read »
Mar 19, 2010 at 12:52 PM
Thoughts And Goals For 2010
@Ben Do bodybuilders from our generation take glucosamine supplements to strengthen joints for this type of exercise? I'm assuming in your early 30s, no? ... read »
Mar 19, 2010 at 12:46 PM
Using ColdFusion's CFLocation Tag For Inline Image SRC Attributes
@Ben, Yes, the client reads the headers and if it has a cached version of the file (same ETAG) then shuts down the request and render the cached version. ( I think it is done in the same request/co ... read »
Mar 19, 2010 at 12:14 PM
Why NULL Values Should Not Be Used in a Database Unless Required
@Eric, I think we should just agree that there are no cross-the-board rules on this. NULL values are good when they add value. That is highly contextual - there's nothing about NULL values that is ... read »
Mar 19, 2010 at 12:12 PM
Why NULL Values Should Not Be Used in a Database Unless Required
@Eric, By all means if you need referential integrity and foreign keys for things like "middle name", then you should use NULL values. In the applications I build, 99% of the time, I there is no ... read »
Mar 19, 2010 at 12:10 PM
Why NULL Values Should Not Be Used in a Database Unless Required
@Ben Nadel, Are you saying that correct data doesn't have any business value? Nonsense. What about referential integrity and foreign keys? ... read »
Mar 19, 2010 at 12:00 PM
Using jQuery To Leverage The OnChange Method Of Inputs
Thnx. FYI spelling error in your comment "// Add dirtry flag to the input in" ... read »
Mar 19, 2010 at 10:57 AM
Javascript Number.toFixed() Method
It doesn't have a base() method, but it can be put together simply with: Math.base = function(n, to, from) { return parseInt(n, from || 10).toString(to); }; ... read »