|This article implements a File Router, which monitors a path on the hard drive and moves files to appropriate locations based on matching certain patterns.|
Time Required: 1-3 hours
Software: Visual Studio Express Editions
This week's column was inspired by the many times I've come across catch-all folders on people's hard drives. Maybe you've got them too. Perhaps it's a documents folder where every file goes uncategorized. I also see downloads folders that contain Zip files, PDF documents, photos, installers and anything else downloaded. Internet Explorer and Firefox download to a single folder unless overridden. Many users either don't know how to manage folders or just don't spend the time to do so. From these observations I decided it would be nice to do something to reduce this burden, and thus, File Router was born.
File Router acts by monitoring a path on the hard drive, then moving files to appropriate locations based on matching certain patterns. The application is fairly basic, with plenty of opportunity for extension. That's a plus for you, not a cop-out for me!
The accompanying code is provided in both C# and VB. As with all Coding 4 Fun articles, you will need any of the Visual Studio Express Editions. The code samples in this article are shown in Visual Basic 2005; however, the downloadable source code is available in both VB 2005 and C# 2005. Beta 2 of the Express editions can be downloaded from https://msdn.microsoft.com/express.
The code is structured as a Windows Form application with a NotifyIcon to provide a presence in the System Tray. Monitoring the folder occurs using a FileSystemWatcher object. This handy addition to the Framework raises events when certain conditions are met in the file system. These can include file creation, rename, or even a change of contents.
The intent with File Router is to watch for new or renamed files in a given path. When these files are detected, the filename is compared to a list of user-entered expressions. Each expression contains one or more simple, comma-separated patterns such as ".jpg" or "budget." Notice that it isn't as simple as file extensions, though using a period will enable it to work that way.
In addition to the form object (used for settings), the project also includes non-visual controls such as the NotifyIcon and FileSystemWatcher, a FolderBrowserDialog, a ToolTip, and a ContextMenuStrip. I felt that some of the elements in the interface were non-intuitive with simple labels, so popup tooltips appear by many controls. The ContextMenuStrip appears when the user right-clicks on the system tray icon. It used to be that tray icons required a lot of plumbing to make them work. Adding the icon and menu is incredibly easy now.
The main form displays the settings. The actual settings are contained in the FileScannerSettings object. The fields in this object are defined as:
Shared SETTINGS_FILENAME As String = "FileRouter.settings" Dim _activeScan As Boolean = False Dim _sourcePath As String Dim _recursiveScan As Boolean = False Dim _mappings As Dictionary(Of String, FileMapping) = New Dictionary(Of String, FileMapping)()
ActiveScan specifies whether or not the source path is being monitored. File Router can just run on demand if desired. The dictionary of mappings is a generic type consisting of a string key and a FileMapping object value. Each FileMapping then, in turn, contains the expression string and a corresponding destination path. In addition to encapsulating the settings in-memory, this class also contains methods to manage its persistence. To save Settings you call the SaveSettings method, passing the object to save:
Public Shared Sub SaveSettings(ByVal fs As FileScannerSettings) Dim str As Stream = File.Open(SETTINGS_FILENAME, FileMode.Create) Dim bf As BinaryFormatter = New BinaryFormatter bf.Serialize(str, fs) str.Close() End Sub
Notice that all of the code required to save the object only takes up four lines. Of course, it would be good to add error handling as well.
Reading the settings is similarly straight-forward:
Public Shared Function GetSettings() As FileScannerSettings Dim str As Stream = Nothing Try str = File.Open(SETTINGS_FILENAME, FileMode.Open) Dim bf As BinaryFormatter = New BinaryFormatter Dim fs As FileScannerSettings = _ CType(bf.Deserialize(str), FileScannerSettings) Return fs Catch ' Most likely saved file does not exist Return New FileScannerSettings Finally If Not str Is Nothing Then str.Close() End If End Try End Function
Notice that this time there is some minimal error-handling. This is necessary because the file may not exist. In that case, the caller will get a fresh object.
Finally, the FileScanner object performs the actual work of scanning the folder (whether invoked on-demand or in response to a file change event), and matching files. The ScanFiles method simply retrieves the list of files in the source path and passes them to PerformMatch:
Public Sub ScanFiles() Dim so As SearchOption If _settings.RecursiveScan Then so = SearchOption.AllDirectories Else so = SearchOption.TopDirectoryOnly End If Dim files() As String =_ Directory.GetFiles(_settings.SourcePath, "*.*", so) For Each file As String In files PerformMatch(file) Next End Sub
Notice that the folder can be enumerated only at the top-level, or recursively. This option is also reflected in the FileSystemWatcher object when the application is in active monitoring mode. The PeformMatch method then checks the filename for a matching expression (case-sensitive through lower case conversions) and moves the file accordingly (comments removed):
Public Sub PerformMatch(ByVal fileName As String) Dim fileNameAlone As String = Path.GetFileName(fileName) Dim fileNameLower As String = fileNameAlone.ToLower() For Each mapping As FileMapping In _settings.FileMappings.Values Dim expressions() As String = mapping.Expression.Split(",") For Each expression As String In expressions If fileNameLower.Contains(expression) Then Try File.Move(fileName, _ Path.Combine( _ mapping.DestinationPath, fileNameAlone)) Return Catch ex As Exception RaiseEvent ErrorEncountered(fileNameAlone, ex) Continue For End Try End If Next Next End Sub
Each mapping is enumerated, then split by comma. Each sub-expression is then evaluated using the Contains method of the filename. If a match is found, the file is moved with the File.Move method. Notice the custom event for handling errors. This is a good decoupling, so the FileScanner class has no need to interact with the UI, and the scanner can continue with other files even after a file has an error. PerformMatch can also be called directly with a single filename as based on a file system event. .
- Error checking is fairly minimal throughout the application. For instance, file paths are not validated at all, so it is best to use the browse dialogs.
- Expressions could be more robust. Regular expressions or at least simple wildcard patterns would improve the situation as would lists of patterns per destination path, rather than a comma-based list.
- The visual list of expressions is very basic, not allowing edits. You must delete and re-add to make a change.
I enjoyed putting a number of pieces together to make this work. It has plenty of space for enhancement, but is definitely useable as-is. Download Visual C# 2005 Express Edition or Visual Basic 2005 Express Edition, then take this application for a spin. Get started at https://msdn.microsoft.com/express.