using System.Collections.Generic; using System.IO; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Security.Permissions; using System.Text.RegularExpressions; using System; using System.Diagnostics.CodeAnalysis; using System.Collections; namespace Sleis.Utility { /// /// File system enumerator. This class provides an easy to use, efficient mechanism for searching a list of /// directories for files matching a list of file specifications. The search is done incrementally as matches /// are consumed, so the overhead before processing the first match is always kept to a minimum. /// public class FileSystemEnumerator : DisposableBase, IEnumerable { [Flags] public enum EReturnTypes { eReturnFiles = 0x0001, eReturnDirectories = 0x0002, eReturnAll = (eReturnFiles | eReturnDirectories) } /// /// Constructor. /// /// Semicolon- or comma-delimitted list of paths to search. /// Semicolon- or comma-delimitted list of wildcard filespecs to match. /// If true, subdirectories are searched. public FileSystemEnumerator(string pathsToSearch, string fileTypesToMatch, bool includeSubDirs, EReturnTypes inReturnTypes) { _scopes = new Stack(); _returnFiles = (inReturnTypes & EReturnTypes.eReturnFiles) == EReturnTypes.eReturnFiles; _returnDirectories = (inReturnTypes & EReturnTypes.eReturnDirectories) == EReturnTypes.eReturnDirectories; // check for nulls if (string.IsNullOrEmpty(pathsToSearch)) throw new ArgumentNullException("pathsToSearch"); if (string.IsNullOrEmpty(fileTypesToMatch)) throw new ArgumentNullException("fileTypesToMatch"); // make sure spec doesn't contain invalid characters if (fileTypesToMatch.IndexOfAny(new char[] { ':', '<', '>', '/', '\\' }) >= 0) throw new ArgumentException("invalid cahracters in wildcard pattern", "fileTypesToMatch"); _includeSubDirs = includeSubDirs; _paths = pathsToSearch.Split(new char[] { ';', ',' }); string[] specs = fileTypesToMatch.Split(new char[] { ';', ',' }); _fileSpecs = new List(specs.Length); foreach (string spec in specs) { // trim whitespace off file spec and convert Win32 wildcards to regular expressions string pattern = spec .Trim() .Replace(".", @"\.") .Replace("*", @".*") .Replace("?", @".?") ; _fileSpecs.Add( new Regex("^" + pattern + "$", RegexOptions.IgnoreCase) ); } } /// /// Information that's kept in our stack for simulated recursion /// private struct SearchInfo { /// /// Find handle returned by FindFirstFile /// public SafeFindHandle Handle; /// /// Path that was searched to yield the find handle. /// public string Path; /// /// Constructor /// /// Find handle returned by FindFirstFile. /// Path corresponding to find handle. public SearchInfo(SafeFindHandle h, string p) { Handle = h; Path = p; } } /// /// Stack of open scopes. This is a member (instead of a local variable) /// to allow Dispose to close any open find handles if the object is disposed /// before the enumeration is completed. /// private Stack _scopes; /// /// Array of paths to be searched. /// private string[] _paths; /// /// Array of regular expressions that will detect matching files. /// private List _fileSpecs; private bool _returnFiles; private bool _returnDirectories; /// /// If true, sub-directories are searched. /// private bool _includeSubDirs; #region IDisposable implementation protected override void OnDispose(bool inIsDisposing) { if (inIsDisposing) { while (_scopes.Count > 0) { SearchInfo si = _scopes.Pop(); si.Handle.Close(); } } } #endregion /// /// Get an enumerator that returns all of the files that match the wildcards that /// are in any of the directories to be searched. /// /// An IEnumerable that returns all matching files one by one. /// The enumerator that is returned finds files using a lazy algorithm that /// searches directories incrementally as matches are consumed. IEnumerator IEnumerable.GetEnumerator() { return Matches(); } IEnumerator IEnumerable.GetEnumerator() { return Matches(); } private IEnumerator Matches() { Stack pathsToSearch = new Stack(_paths); FindData findData = new FindData(); string path, fileName; while (0 != pathsToSearch.Count) { path = pathsToSearch.Pop().Trim(); using (SafeFindHandle handle = FindFirstFile( Path.Combine(path, "*"), findData)) { if (!handle.IsInvalid) { do { fileName = findData.fileName; if (string.IsNullOrEmpty(fileName)) continue; if (string.Equals(fileName, ".", StringComparison.Ordinal)) continue; if (string.Equals(fileName, "..", StringComparison.Ordinal)) continue; if (0 != ((int)FileAttributes.Directory & findData.fileAttributes)) { if (_returnDirectories) { foreach (Regex fileSpec in _fileSpecs) { if (fileSpec.IsMatch(fileName)) { yield return Path.Combine(path, fileName); break; } } } if (_includeSubDirs) { pathsToSearch.Push(Path.Combine(path, fileName)); } } else { if (_returnFiles) { foreach (Regex fileSpec in _fileSpecs) { if (fileSpec.IsMatch(fileName)) { yield return Path.Combine(path, fileName); break; } } } } } while (FindNextFile(handle, findData)); } } } } /// /// Structure that maps to WIN32_FIND_DATA /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible"), StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Auto)] private sealed class FindData { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int fileAttributes; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int creationTime_lowDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int creationTime_highDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int lastAccessTime_lowDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int lastAccessTime_highDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int lastWriteTime_lowDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int lastWriteTime_highDateTime; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int nFileSizeHigh; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int nFileSizeLow; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int dwReserved0; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public int dwReserved1; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public String fileName; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public String alternateFileName; } /// /// SafeHandle class for holding find handles /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] private sealed class SafeFindHandle : Microsoft.Win32.SafeHandles.SafeHandleMinusOneIsInvalid { /// /// Constructor /// public SafeFindHandle() : base(true) { } /// /// Release the find handle /// /// true if the handle was released protected override bool ReleaseHandle() { return FindClose(handle); } } [DllImport("Kernel32.dll", CharSet = CharSet.Auto)] [SuppressMessage("Microsoft.Interoperability", "CA1401")] private static extern SafeFindHandle FindFirstFile(String fileName, [In, Out] FindData findFileData); [DllImport("kernel32", CharSet = CharSet.Auto)] [return: MarshalAs(UnmanagedType.Bool)] [SuppressMessage("Microsoft.Interoperability", "CA1401")] private static extern bool FindNextFile(SafeFindHandle hFindFile, [In, Out] FindData lpFindFileData); [DllImport("kernel32", CharSet = CharSet.Auto)] [return: MarshalAs(UnmanagedType.Bool)] [SuppressMessage("Microsoft.Interoperability", "CA1401")] private static extern bool FindClose(IntPtr hFindFile); } }