ActionScript for recursive file searches in AIR

The following is a sample usage of the FileFinder class I developed for an ongoing project. You’ll find the source code below the usage samples. It’s pretty generic and has only one purpose — to recursively search (through subdirectories) for a specific file name from a starting directory. Usage is straightforward…

package {

	import FileFinder;

	public class Main extends Sprite {

		private var _fileFinder:FileFinder = null;

		public function Main() {
			//Find "chrome.exe" somewhere on the c: drive...
			this._fileFinder = new FileFinder("c:\", "chrome.exe");
	        }
	}
}

Additionally, you can create an optional list of excluded directories, as a File vector array, that you don’t wan’t searched:

package {

	import FileFinder;
	import flash.filesystem.File;

	public class Main extends Sprite {

		private var _fileFinder:FileFinder = null;

		public function Main() {
			//Find "chrome.exe" somewhere on the c: drive, but don't look in "c:\Windows\"...
			var exclusions:Vector.<File>=new Vector.<File>();
			exclusions.push(FileFinder.resolveToFile("c:\Windows\");
			this._fileFinder = new FileFinder("c:\", "chrome.exe", exclusions);
		}
	}
}

I’ve fully commented the source code below, as well as sections of the class that I highly recommend you update. At the very least, update the section that says “File was found! Do something with currentFile here…” (because it’s not a very useful class otherwise 🙂 )

//Update package path as desired...
package  {

	import flash.filesystem.File;
	import flash.events.FileListEvent;

	public class FileFinder {

		private var _basePath:* = null;
		private var _fileName:String = null;
		private var _currentDir:File = null;
		private var _directoryStack:Vector.<File> = null;
		private var _completedStack:Vector.<File> = null;

		/**
		 * Searches for a specific file from a specified base path, optionally excluding certain paths.
		 * 
		 * @param	basePath The base path (for example, "c:\" or "/") to begin the search at.
		 * @param	fileName The file name ("file.ext") to search for.
		 * @param	exclusions An optional File vector array of paths to exclude. Any File items that are not
		 * directories will be removed.
		 */
		public function FileFinder(basePath:*, fileName:String, exclusions:Vector.<File>=null) {
			this._basePath = basePath;
			this._fileName = fileName;
			if (exclusions != null) {
				//The exclusions are the completed stack, we just need to ensure that they're all directories.
				this._completedStack = exclusions;
				this._completedStack = this.pruneNonDirectories(this._completedStack);
			} else {
				this._completedStack = new Vector.<File>();
			}//else			
			this._directoryStack = new Vector.<File>();			
			this.findFile();
		}//constructor

		/**
		 * Begins the file search by resolving the base path and starting the initial asynch file list retrieval.
		 */
		private function findFile():void {
			this._currentDir = resolveToFile(this._basePath);						
			if (this._currentDir == null) {
				trace ("Couldn't resolve root directory: \""+this._basePath+"\"");
				return;
			}//if
			this._currentDir.addEventListener(FileListEvent.DIRECTORY_LISTING, this.onDirectoryListing);
			this._currentDir.getDirectoryListingAsync();
		}//findFile

		/**
		 * Event handler for asynch directory listing.
		 * 
		 * @param	eventObj A FileListEvent object.
		 */
		private function onDirectoryListing(eventObj:FileListEvent):void {
			var dirString:String = new String();
			this._currentDir.removeEventListener(FileListEvent.DIRECTORY_LISTING, this.onDirectoryListing);
			//Current directory being searched: this._currentDir.nativePath
			//We set _currentDir to null as a precaution here. Not 100% necessary.
			this._currentDir = null;			
			var fileList:Array = eventObj.files;
			if (fileList.length == 0)  {
				//This is an empty directory so abort here and keep searching...
				this._currentDir = this._directoryStack.shift();
				this._completedStack.push(this._currentDir);
				this._currentDir.addEventListener(FileListEvent.DIRECTORY_LISTING, this.onDirectoryListing);
				this._currentDir.getDirectoryListingAsync();
				return;
			}//if					
			//Number of directories remaining to be searched: this._directoryStack.length
			//Directories already searched: this._completedStack.length
			for (var count:uint = 0; count < fileList.length; count++) {
				var currentFile:File = fileList[count] as File;
				if (currentFile.isDirectory) {
					//Current search entry is a directory so push on stack if not already searched
					if (this._completedStack.indexOf(currentFile)<0) {
						this._directoryStack.push(currentFile);
					}//if
				} else {							
					if (currentFile.name == this._fileName) {
						//File was found! Do something with currentFile here; maybe dispatch an event?
						return;
					}//if
				}//else
			}//for
			if (this._directoryStack.length == 0) {
				//File was not found. We're done.
				return;
			}//if			
			this._currentDir = this._directoryStack.shift();
			this._completedStack.push(this._currentDir);
			this._currentDir.addEventListener(FileListEvent.DIRECTORY_LISTING, this.onDirectoryListing);
			this._currentDir.getDirectoryListingAsync();
		}//onDirectoryListing

		/**
		 * Prunes all non-directory items from the supplied File vector array.
		 * 
		 * @param	stackVector An array of File items.
		 * @return  	A copy of the input vector array with only the directory items included.
		 */
		private function pruneNonDirectories(stackVector:Vector.<File>):Vector.<File> {
			return (stackVector.filter(this.pruneNonDirFilter, this));
		}//pruneNonDirectories

		/**
		 * Filter function used in conjuction with a File vector's "filter" method.
		 * 
		 * @param	item The File item being analyzed.
		 * @param	index The index of the item currently being analyzed.
		 * @param	vector A reference to the File vector currently executing the "filter" method.
		 * 
		 * @return True if the supplied File item is a directory, false otherwise.
		 */
		private function pruneNonDirFilter(item:File, index:int, vector:Vector.<File>):Boolean {
			if (item.isDirectory) {
				return (true);
			} else {
				return (false);
			}//else
		}//pruneNonDirFilter		

		/**
		 * Resolves a native path string (for example "c:\Windows\", or relative root paths like "\" or "/"), to an
		 * ActionScript File object.
		 * 
		 * @param	nativePath The native path to convert to a File instance.
		 * 
		 * @return A File instance pointing to the native path specified, or null if something went horribly wrong.
		 */
		public static function resolveToFile(nativePath:String):File {
			try {
				var returnFile:File = File.userDirectory;
				if ((nativePath == "\\") || (nativePath == "/")) {
					//AIR won't resolve slashes as roots so we do a little guessing instead...
					var rootDirs:Array = File.getRootDirectories();
					returnFile = rootDirs[0] as File;		
				} else {			
					returnFile = returnFile.resolvePath(nativePath);
				}//else
				return (returnFile);
			} catch (err:*) {
				return (null);
			}//catch
			return (null);
		}//resolveToFile

	}//FileFinder class

}//package

After running this a few times I realized that it’s not as efficient as it could be, but it’s a good place to start. Everything runs asynchronously so it shouldn’t affect your application much, though in my experience there are still hiccups when accessing certain directories.

2 comments
  1. Cool!

    I like that it’s async but I was thinking it would be nice if you could off-load this to a worker so you could be more aggressive/faster about traversing the directories… however so far my attempts to get the File stuff to work at all in a worker have failed silently. Have you tried this?

    • I imagine that making this run as a Worker would be beneficial if you can figure out a suitable partitioning system — which files/directories will be searched by which Worker so as to avoid overlaps. However, I believe you’d run into physical limits pretty soon as your device is now doing dual (or triple, etc.) access to the file system, so the benefit of operating as a Worker may simply lie in its ability to operate independently from the main thread (avoiding “hiccups”). Either way, it’d be worth a try.

      On the subject of silent Workers, I’ve noticed that if a Worker SWF is compiled in “debug” mode (debugging enabled, traces included), it tends to do nothing (no errors, etc.) This may have to do with the way the virtual machine handles debug instructions (or it may be a conflict with local debugging sockets), but I haven’t been able to get around it except to export the Worker SWF with debugging and traces disabled. This doesn’t guarantee that the Worker will run properly, but at least you should now be able to see the code actually running.

Add Comment

Required fields are marked *. Your email address will not be published.