Created
August 27, 2020 16:14
-
-
Save Cologler/c3b168ced6253a4646b395207a9e439e to your computer and use it in GitHub Desktop.
fast directory enumerator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Runtime.ConstrainedExecution; | |
using System.Runtime.InteropServices; | |
using System.Security.Permissions; | |
using Microsoft.Win32.SafeHandles; | |
// backup for https://www.codeproject.com/Articles/38959/A-Faster-Directory-Enumerator | |
namespace CodeProject | |
{ | |
/// <summary> | |
/// Contains information about a file returned by the | |
/// <see cref="FastDirectoryEnumerator"/> class. | |
/// </summary> | |
[Serializable] | |
public class FileData | |
{ | |
/// <summary> | |
/// Attributes of the file. | |
/// </summary> | |
public readonly FileAttributes Attributes; | |
public DateTime CreationTime | |
{ | |
get { return this.CreationTimeUtc.ToLocalTime(); } | |
} | |
/// <summary> | |
/// File creation time in UTC | |
/// </summary> | |
public readonly DateTime CreationTimeUtc; | |
/// <summary> | |
/// Gets the last access time in local time. | |
/// </summary> | |
public DateTime LastAccesTime | |
{ | |
get { return this.LastAccessTimeUtc.ToLocalTime(); } | |
} | |
/// <summary> | |
/// File last access time in UTC | |
/// </summary> | |
public readonly DateTime LastAccessTimeUtc; | |
/// <summary> | |
/// Gets the last access time in local time. | |
/// </summary> | |
public DateTime LastWriteTime | |
{ | |
get { return this.LastWriteTimeUtc.ToLocalTime(); } | |
} | |
/// <summary> | |
/// File last write time in UTC | |
/// </summary> | |
public readonly DateTime LastWriteTimeUtc; | |
/// <summary> | |
/// Size of the file in bytes | |
/// </summary> | |
public readonly long Size; | |
/// <summary> | |
/// Name of the file | |
/// </summary> | |
public readonly string Name; | |
/// <summary> | |
/// Full path to the file. | |
/// </summary> | |
public readonly string Path; | |
/// <summary> | |
/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. | |
/// </summary> | |
/// <returns> | |
/// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. | |
/// </returns> | |
public override string ToString() | |
{ | |
return this.Name; | |
} | |
/// <summary> | |
/// Initializes a new instance of the <see cref="FileData"/> class. | |
/// </summary> | |
/// <param name="dir">The directory that the file is stored at</param> | |
/// <param name="findData">WIN32_FIND_DATA structure that this | |
/// object wraps.</param> | |
internal FileData(string dir, WIN32_FIND_DATA findData) | |
{ | |
this.Attributes = findData.dwFileAttributes; | |
this.CreationTimeUtc = ConvertDateTime(findData.ftCreationTime_dwHighDateTime, | |
findData.ftCreationTime_dwLowDateTime); | |
this.LastAccessTimeUtc = ConvertDateTime(findData.ftLastAccessTime_dwHighDateTime, | |
findData.ftLastAccessTime_dwLowDateTime); | |
this.LastWriteTimeUtc = ConvertDateTime(findData.ftLastWriteTime_dwHighDateTime, | |
findData.ftLastWriteTime_dwLowDateTime); | |
this.Size = CombineHighLowInts(findData.nFileSizeHigh, findData.nFileSizeLow); | |
this.Name = findData.cFileName; | |
this.Path = System.IO.Path.Combine(dir, findData.cFileName); | |
} | |
private static long CombineHighLowInts(uint high, uint low) | |
{ | |
return (((long)high) << 0x20) | low; | |
} | |
private static DateTime ConvertDateTime(uint high, uint low) | |
{ | |
long fileTime = CombineHighLowInts(high, low); | |
return DateTime.FromFileTimeUtc(fileTime); | |
} | |
} | |
/// <summary> | |
/// Contains information about the file that is found | |
/// by the FindFirstFile or FindNextFile functions. | |
/// </summary> | |
[Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto), BestFitMapping(false)] | |
internal class WIN32_FIND_DATA | |
{ | |
public FileAttributes dwFileAttributes; | |
public uint ftCreationTime_dwLowDateTime; | |
public uint ftCreationTime_dwHighDateTime; | |
public uint ftLastAccessTime_dwLowDateTime; | |
public uint ftLastAccessTime_dwHighDateTime; | |
public uint ftLastWriteTime_dwLowDateTime; | |
public uint ftLastWriteTime_dwHighDateTime; | |
public uint nFileSizeHigh; | |
public uint nFileSizeLow; | |
public int dwReserved0; | |
public int dwReserved1; | |
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] | |
public string cFileName; | |
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] | |
public string cAlternateFileName; | |
/// <summary> | |
/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. | |
/// </summary> | |
/// <returns> | |
/// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. | |
/// </returns> | |
public override string ToString() | |
{ | |
return "File name=" + cFileName; | |
} | |
} | |
/// <summary> | |
/// A fast enumerator of files in a directory. Use this if you need to get attributes for | |
/// all files in a directory. | |
/// </summary> | |
/// <remarks> | |
/// This enumerator is substantially faster than using <see cref="Directory.GetFiles(string)"/> | |
/// and then creating a new FileInfo object for each path. Use this version when you | |
/// will need to look at the attibutes of each file returned (for example, you need | |
/// to check each file in a directory to see if it was modified after a specific date). | |
/// </remarks> | |
public static class FastDirectoryEnumerator | |
{ | |
/// <summary> | |
/// Gets <see cref="FileData"/> for all the files in a directory. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <returns>An object that implements <see cref="IEnumerable{FileData}"/> and | |
/// allows you to enumerate the files in the given directory.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="path"/> is a null reference (Nothing in VB) | |
/// </exception> | |
public static IEnumerable<FileData> EnumerateFiles(string path) | |
{ | |
return FastDirectoryEnumerator.EnumerateFiles(path, "*"); | |
} | |
/// <summary> | |
/// Gets <see cref="FileData"/> for all the files in a directory that match a | |
/// specific filter. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <param name="searchPattern">The search string to match against files in the path.</param> | |
/// <returns>An object that implements <see cref="IEnumerable{FileData}"/> and | |
/// allows you to enumerate the files in the given directory.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="path"/> is a null reference (Nothing in VB) | |
/// </exception> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="filter"/> is a null reference (Nothing in VB) | |
/// </exception> | |
public static IEnumerable<FileData> EnumerateFiles(string path, string searchPattern) | |
{ | |
return FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly); | |
} | |
/// <summary> | |
/// Gets <see cref="FileData"/> for all the files in a directory that | |
/// match a specific filter, optionally including all sub directories. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <param name="searchPattern">The search string to match against files in the path.</param> | |
/// <param name="searchOption"> | |
/// One of the SearchOption values that specifies whether the search | |
/// operation should include all subdirectories or only the current directory. | |
/// </param> | |
/// <returns>An object that implements <see cref="IEnumerable{FileData}"/> and | |
/// allows you to enumerate the files in the given directory.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="path"/> is a null reference (Nothing in VB) | |
/// </exception> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="filter"/> is a null reference (Nothing in VB) | |
/// </exception> | |
/// <exception cref="ArgumentOutOfRangeException"> | |
/// <paramref name="searchOption"/> is not one of the valid values of the | |
/// <see cref="System.IO.SearchOption"/> enumeration. | |
/// </exception> | |
public static IEnumerable<FileData> EnumerateFiles(string path, string searchPattern, SearchOption searchOption) | |
{ | |
if (path == null) | |
{ | |
throw new ArgumentNullException("path"); | |
} | |
if (searchPattern == null) | |
{ | |
throw new ArgumentNullException("searchPattern"); | |
} | |
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) | |
{ | |
throw new ArgumentOutOfRangeException("searchOption"); | |
} | |
string fullPath = Path.GetFullPath(path); | |
return new FileEnumerable(fullPath, searchPattern, searchOption); | |
} | |
/// <summary> | |
/// Gets <see cref="FileData"/> for all the files in a directory that match a | |
/// specific filter. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <param name="searchPattern">The search string to match against files in the path.</param> | |
/// <returns>An object that implements <see cref="IEnumerable{FileData}"/> and | |
/// allows you to enumerate the files in the given directory.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="path"/> is a null reference (Nothing in VB) | |
/// </exception> | |
/// <exception cref="ArgumentNullException"> | |
/// <paramref name="filter"/> is a null reference (Nothing in VB) | |
/// </exception> | |
public static FileData[] GetFiles(string path, string searchPattern, SearchOption searchOption) | |
{ | |
IEnumerable<FileData> e = FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, searchOption); | |
List<FileData> list = new List<FileData>(e); | |
FileData[] retval = new FileData[list.Count]; | |
list.CopyTo(retval); | |
return retval; | |
} | |
/// <summary> | |
/// Provides the implementation of the | |
/// <see cref="T:System.Collections.Generic.IEnumerable`1"/> interface | |
/// </summary> | |
private class FileEnumerable : IEnumerable<FileData> | |
{ | |
private readonly string m_path; | |
private readonly string m_filter; | |
private readonly SearchOption m_searchOption; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="FileEnumerable"/> class. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <param name="filter">The search string to match against files in the path.</param> | |
/// <param name="searchOption"> | |
/// One of the SearchOption values that specifies whether the search | |
/// operation should include all subdirectories or only the current directory. | |
/// </param> | |
public FileEnumerable(string path, string filter, SearchOption searchOption) | |
{ | |
m_path = path; | |
m_filter = filter; | |
m_searchOption = searchOption; | |
} | |
#region IEnumerable<FileData> Members | |
/// <summary> | |
/// Returns an enumerator that iterates through the collection. | |
/// </summary> | |
/// <returns> | |
/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can | |
/// be used to iterate through the collection. | |
/// </returns> | |
public IEnumerator<FileData> GetEnumerator() | |
{ | |
return new FileEnumerator(m_path, m_filter, m_searchOption); | |
} | |
#endregion | |
#region IEnumerable Members | |
/// <summary> | |
/// Returns an enumerator that iterates through a collection. | |
/// </summary> | |
/// <returns> | |
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be | |
/// used to iterate through the collection. | |
/// </returns> | |
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() | |
{ | |
return new FileEnumerator(m_path, m_filter, m_searchOption); | |
} | |
#endregion | |
} | |
/// <summary> | |
/// Wraps a FindFirstFile handle. | |
/// </summary> | |
private sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid | |
{ | |
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] | |
[DllImport("kernel32.dll")] | |
private static extern bool FindClose(IntPtr handle); | |
/// <summary> | |
/// Initializes a new instance of the <see cref="SafeFindHandle"/> class. | |
/// </summary> | |
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] | |
internal SafeFindHandle() | |
: base(true) | |
{ | |
} | |
/// <summary> | |
/// When overridden in a derived class, executes the code required to free the handle. | |
/// </summary> | |
/// <returns> | |
/// true if the handle is released successfully; otherwise, in the | |
/// event of a catastrophic failure, false. In this case, it | |
/// generates a releaseHandleFailed MDA Managed Debugging Assistant. | |
/// </returns> | |
protected override bool ReleaseHandle() | |
{ | |
return FindClose(base.handle); | |
} | |
} | |
/// <summary> | |
/// Provides the implementation of the | |
/// <see cref="T:System.Collections.Generic.IEnumerator`1"/> interface | |
/// </summary> | |
[System.Security.SuppressUnmanagedCodeSecurity] | |
private class FileEnumerator : IEnumerator<FileData> | |
{ | |
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
private static extern SafeFindHandle FindFirstFile(string fileName, | |
[In, Out] WIN32_FIND_DATA data); | |
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
private static extern bool FindNextFile(SafeFindHandle hndFindFile, | |
[In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData); | |
/// <summary> | |
/// Hold context information about where we current are in the directory search. | |
/// </summary> | |
private class SearchContext | |
{ | |
public readonly string Path; | |
public Stack<string> SubdirectoriesToProcess; | |
public SearchContext(string path) | |
{ | |
this.Path = path; | |
} | |
} | |
private string m_path; | |
private string m_filter; | |
private SearchOption m_searchOption; | |
private Stack<SearchContext> m_contextStack; | |
private SearchContext m_currentContext; | |
private SafeFindHandle m_hndFindFile; | |
private WIN32_FIND_DATA m_win_find_data = new WIN32_FIND_DATA(); | |
/// <summary> | |
/// Initializes a new instance of the <see cref="FileEnumerator"/> class. | |
/// </summary> | |
/// <param name="path">The path to search.</param> | |
/// <param name="filter">The search string to match against files in the path.</param> | |
/// <param name="searchOption"> | |
/// One of the SearchOption values that specifies whether the search | |
/// operation should include all subdirectories or only the current directory. | |
/// </param> | |
public FileEnumerator(string path, string filter, SearchOption searchOption) | |
{ | |
m_path = path; | |
m_filter = filter; | |
m_searchOption = searchOption; | |
m_currentContext = new SearchContext(path); | |
if (m_searchOption == SearchOption.AllDirectories) | |
{ | |
m_contextStack = new Stack<SearchContext>(); | |
} | |
} | |
#region IEnumerator<FileData> Members | |
/// <summary> | |
/// Gets the element in the collection at the current position of the enumerator. | |
/// </summary> | |
/// <value></value> | |
/// <returns> | |
/// The element in the collection at the current position of the enumerator. | |
/// </returns> | |
public FileData Current | |
{ | |
get { return new FileData(m_path, m_win_find_data); } | |
} | |
#endregion | |
#region IDisposable Members | |
/// <summary> | |
/// Performs application-defined tasks associated with freeing, releasing, | |
/// or resetting unmanaged resources. | |
/// </summary> | |
public void Dispose() | |
{ | |
if (m_hndFindFile != null) | |
{ | |
m_hndFindFile.Dispose(); | |
} | |
} | |
#endregion | |
#region IEnumerator Members | |
/// <summary> | |
/// Gets the element in the collection at the current position of the enumerator. | |
/// </summary> | |
/// <value></value> | |
/// <returns> | |
/// The element in the collection at the current position of the enumerator. | |
/// </returns> | |
object System.Collections.IEnumerator.Current | |
{ | |
get { return new FileData(m_path, m_win_find_data); } | |
} | |
/// <summary> | |
/// Advances the enumerator to the next element of the collection. | |
/// </summary> | |
/// <returns> | |
/// true if the enumerator was successfully advanced to the next element; | |
/// false if the enumerator has passed the end of the collection. | |
/// </returns> | |
/// <exception cref="T:System.InvalidOperationException"> | |
/// The collection was modified after the enumerator was created. | |
/// </exception> | |
public bool MoveNext() | |
{ | |
bool retval = false; | |
//If the handle is null, this is first call to MoveNext in the current | |
// directory. In that case, start a new search. | |
if (m_currentContext.SubdirectoriesToProcess == null) | |
{ | |
if (m_hndFindFile == null) | |
{ | |
new FileIOPermission(FileIOPermissionAccess.PathDiscovery, m_path).Demand(); | |
string searchPath = Path.Combine(m_path, m_filter); | |
m_hndFindFile = FindFirstFile(searchPath, m_win_find_data); | |
retval = !m_hndFindFile.IsInvalid; | |
} | |
else | |
{ | |
//Otherwise, find the next item. | |
retval = FindNextFile(m_hndFindFile, m_win_find_data); | |
} | |
} | |
//If the call to FindNextFile or FindFirstFile succeeded... | |
if (retval) | |
{ | |
if (((FileAttributes)m_win_find_data.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory) | |
{ | |
//Ignore folders for now. We call MoveNext recursively here to | |
// move to the next item that FindNextFile will return. | |
return MoveNext(); | |
} | |
} | |
else if (m_searchOption == SearchOption.AllDirectories) | |
{ | |
//SearchContext context = new SearchContext(m_hndFindFile, m_path); | |
//m_contextStack.Push(context); | |
//m_path = Path.Combine(m_path, m_win_find_data.cFileName); | |
//m_hndFindFile = null; | |
if (m_currentContext.SubdirectoriesToProcess == null) | |
{ | |
string[] subDirectories = Directory.GetDirectories(m_path); | |
m_currentContext.SubdirectoriesToProcess = new Stack<string>(subDirectories); | |
} | |
if (m_currentContext.SubdirectoriesToProcess.Count > 0) | |
{ | |
string subDir = m_currentContext.SubdirectoriesToProcess.Pop(); | |
m_contextStack.Push(m_currentContext); | |
m_path = subDir; | |
m_hndFindFile = null; | |
m_currentContext = new SearchContext(m_path); | |
return MoveNext(); | |
} | |
//If there are no more files in this directory and we are | |
// in a sub directory, pop back up to the parent directory and | |
// continue the search from there. | |
if (m_contextStack.Count > 0) | |
{ | |
m_currentContext = m_contextStack.Pop(); | |
m_path = m_currentContext.Path; | |
if (m_hndFindFile != null) | |
{ | |
m_hndFindFile.Close(); | |
m_hndFindFile = null; | |
} | |
return MoveNext(); | |
} | |
} | |
return retval; | |
} | |
/// <summary> | |
/// Sets the enumerator to its initial position, which is before the first element in the collection. | |
/// </summary> | |
/// <exception cref="T:System.InvalidOperationException"> | |
/// The collection was modified after the enumerator was created. | |
/// </exception> | |
public void Reset() | |
{ | |
m_hndFindFile = null; | |
} | |
#endregion | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment