#region Copyright 2011-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.IO; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; namespace CSharpTest.Net.IO { /// /// Provides an efficient file/directory enumeration that behaves several orders faster than Directory.GetXxxx() methods. /// public class FindFile { #region Static data and helpers private static readonly char[] InvalidFilePathChars; private static readonly char[] InvalidFilePatternChars; static FindFile() { InvalidFilePathChars = Path.GetInvalidPathChars(); List set = new List(Path.GetInvalidFileNameChars()); set.Remove('*'); set.Remove('?'); InvalidFilePatternChars = set.ToArray(); } /// Enumerates the files directly in the directory specified public static void FilesIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, false, false, true); ff.FileFound = (o, a) => e(a); ff.Find(); } /// Enumerates the folders directly in the directory specified public static void FoldersIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, false, true, false); ff.FileFound = (o, a) => e(a); ff.Find(); } /// Enumerates the files and folders directly in the directory specified public static void FilesAndFoldersIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, false, true, true); ff.FileFound = (o, a) => e(a); ff.Find(); } /// Enumerates the files anywhere under the directory specified public static void AllFilesIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, true, false, true); ff.FileFound = (o, a) => e(a); ff.Find(); } /// Enumerates the folders anywhere under the directory specified public static void AllFoldersIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, true, true, false); ff.FileFound = (o, a) => e(a); ff.Find(); } /// Enumerates the files and folders anywhere under the directory specified public static void AllFilesAndFoldersIn(string directory, Action e) { FindFile ff = new FindFile(directory, STAR, true, true, true); ff.FileFound = (o, a) => e(a); ff.Find(); } #endregion #region Kernel32 internal static class Kernel32 { internal const int MAX_PATH = 260; internal const int MAX_ALTERNATE = 14; internal const int ERROR_FILE_NOT_FOUND = 2; internal const int ERROR_PATH_NOT_FOUND = 3; internal const int ERROR_ACCESS_DENIED = 5; [StructLayout(LayoutKind.Sequential)] public struct FILETIME { public uint dwLowDateTime; public uint dwHighDateTime; public DateTime ToDateTimeUtc() { return DateTime.FromFileTimeUtc(dwLowDateTime | ((long) dwHighDateTime << 32)); } }; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WIN32_FIND_DATA { public FileAttributes dwFileAttributes; public FILETIME ftCreationTime; public FILETIME ftLastAccessTime; public FILETIME ftLastWriteTime; public uint nFileSizeHigh; //changed all to uint from int, otherwise you run into unexpected overflow public uint nFileSizeLow; //| http://www.pinvoke.net/default.aspx/Structures/WIN32_FIND_DATA.html private uint dwReserved0; //| private uint dwReserved1; //v [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_PATH)] public char[] cFileName; [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_ALTERNATE)] private char[] cAlternateFileName; public bool IgnoredByName { get { return (cFileName[0] == ZERO) || (cFileName[0] == '.' && cFileName[1] == ZERO) || (cFileName[0] == '.' && cFileName[1] == '.' && cFileName[2] == ZERO) ; } } } public enum FINDEX_INFO_LEVELS { FindExInfoStandard = 0, FindExInfoBasic = 1 } public enum FINDEX_SEARCH_OPS { FindExSearchNameMatch = 0, FindExSearchLimitToDirectories = 1, FindExSearchLimitToDevices = 2 } [Flags] public enum FINDEX_ADDITIONAL_FLAGS { FindFirstExCaseSensitive = 1, FindFirstExLargeFetch = 2, } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern IntPtr FindFirstFileEx( IntPtr lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, out WIN32_FIND_DATA lpFindFileData, FINDEX_SEARCH_OPS fSearchOp, IntPtr lpSearchFilter, FINDEX_ADDITIONAL_FLAGS dwAdditionalFlags); [DllImport("kernel32", CharSet = CharSet.Unicode)] public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll")] public static extern bool FindClose(IntPtr hFindFile); } #endregion #region FileFoundEventArgs /// Provides a simple struct to capture file info, given by method public struct Info { /// Returns the parent folder's full path public string ParentPath { get { return Path.GetDirectoryName(FullPath); } } /// Gets or sets the full path of the file or folder public string FullPath { get; set; } /// Returns the file or folder name (with extension) public string Name { get { return Path.GetFileName(FullPath); } } /// Returns the extenion or String.Empty public string Extension { get { return Path.GetExtension(FullPath); } } /// Returns the UNC path to the parent folder public string ParentPathUnc { get { return (FullPath.StartsWith(@"\\")) ? ParentPath : (UncPrefix + ParentPath); } } /// Returns the UNC path to the file or folder public string FullPathUnc { get { return (FullPath.StartsWith(@"\\")) ? FullPath : (UncPrefix + FullPath); } } /// Gets or sets the length in bytes public long Length { get; set; } /// Gets or sets the file or folder attributes public FileAttributes Attributes { get; set; } /// Gets or sets the file or folder CreationTime in Utc public DateTime CreationTimeUtc { get; set; } /// Gets or sets the file or folder LastAccessTime in Utc public DateTime LastAccessTimeUtc { get; set; } /// Gets or sets the file or folder LastWriteTime in Utc public DateTime LastWriteTimeUtc { get; set; } } internal class Win32FindData { public char[] Buffer; public IntPtr BufferAddress; public Kernel32.WIN32_FIND_DATA Value; } /// /// Provides access to the file or folder information durring enumeration, DO NOT keep a reference to this /// class as it's meaning will change durring enumeration. /// public sealed class FileFoundEventArgs : EventArgs { private readonly Win32FindData _ff; private int _uncPrefixLength; private int _parentNameLength; private int _itemNameLength; private bool _cancelEnumeration; internal FileFoundEventArgs(Win32FindData ff) { _ff = ff; } internal void SetNameOffsets(int uncPrefixLength, int parentIx, int itemIx) { _parentNameLength = parentIx; _itemNameLength = itemIx; _uncPrefixLength = uncPrefixLength; } /// Returns the parent folder's full path public string ParentPath { get { return new String(_ff.Buffer, _uncPrefixLength, _parentNameLength - _uncPrefixLength); } } /// Returns the UNC path to the parent folder public string ParentPathUnc { get { return new String(_ff.Buffer, 0, _parentNameLength); } } /// Gets the full path of the file or folder public string FullPath { get { return new String(_ff.Buffer, _uncPrefixLength, _itemNameLength - _uncPrefixLength); } } /// Returns the UNC path to the file or folder public string FullPathUnc { get { return new String(_ff.Buffer, 0, _itemNameLength); } } /// Returns the file or folder name (with extension) public string Name { get { return new String(_ff.Buffer, _parentNameLength, _itemNameLength - _parentNameLength); } } /// Returns the extenion or String.Empty public string Extension { get { for(int ix = _itemNameLength; ix > _parentNameLength; ix--) if (_ff.Buffer[ix] == '.') return new String(_ff.Buffer, ix, _itemNameLength - ix); return String.Empty; } } /// Gets the length in bytes public long Length { get { return _ff.Value.nFileSizeLow | ((long)_ff.Value.nFileSizeHigh << 32); } } /// Gets the file or folder attributes public FileAttributes Attributes { get { return _ff.Value.dwFileAttributes; } } /// Gets the file or folder CreationTime in Utc public DateTime CreationTimeUtc { get { return _ff.Value.ftCreationTime.ToDateTimeUtc(); } } /// Gets the file or folder LastAccessTime in Utc public DateTime LastAccessTimeUtc { get { return _ff.Value.ftLastAccessTime.ToDateTimeUtc(); } } /// Gets the file or folder LastWriteTime in Utc public DateTime LastWriteTimeUtc { get { return _ff.Value.ftLastWriteTime.ToDateTimeUtc(); } } /// Returns true if the file or folder is ReadOnly public bool IsReadOnly { get { return (Attributes & FileAttributes.ReadOnly) != 0; } } /// Returns true if the file or folder is Hidden public bool IsHidden { get { return (Attributes & FileAttributes.Hidden) != 0; } } /// Returns true if the file or folder is System public bool IsSystem { get { return (Attributes & FileAttributes.System) != 0; } } /// Returns true if the file or folder is Directory public bool IsDirectory { get { return (Attributes & FileAttributes.Directory) != 0; } } /// Returns true if the file or folder is ReparsePoint public bool IsReparsePoint { get { return (Attributes & FileAttributes.ReparsePoint) != 0; } } /// Returns true if the file or folder is Compressed public bool IsCompressed { get { return (Attributes & FileAttributes.Compressed) != 0; } } /// Returns true if the file or folder is Offline public bool IsOffline { get { return (Attributes & FileAttributes.Offline) != 0; } } /// Returns true if the file or folder is Encrypted public bool IsEncrypted { get { return (Attributes & FileAttributes.Encrypted) != 0; } } /// /// Captures the current state as a structure. /// public Info GetInfo() { return new Info { FullPath = FullPath, Length = Length, Attributes = Attributes, CreationTimeUtc = CreationTimeUtc, LastAccessTimeUtc = LastAccessTimeUtc, LastWriteTimeUtc = LastWriteTimeUtc, }; } /// Gets or sets the Cancel flag to abort the current enumeration public bool CancelEnumeration { get { return _cancelEnumeration; } set { _cancelEnumeration = value; } } } #endregion private const string STAR = "*"; private const char SLASH = '\\'; private const char ZERO = '\0'; /// Returns the Unc path prefix used public const string UncPrefix = @"\\?\"; private readonly Win32FindData _ff; private char[] _fpattern; private int _baseOffset; private bool _recursive; private bool _includeFolders; private bool _includeFiles; private bool _isUncPath; /// Creates a FindFile instance. public FindFile() : this(UncPrefix, STAR, true, true, true) { } /// Creates a FindFile instance. public FindFile(string rootDirectory) : this(rootDirectory, STAR, true, true, true) { } /// Creates a FindFile instance. public FindFile(string rootDirectory, string filePattern) : this(rootDirectory, filePattern, true, true, true) { } /// Creates a FindFile instance. public FindFile(string rootDirectory, string filePattern, bool recursive) : this(rootDirectory, filePattern, recursive, true, true) { } /// Creates a FindFile instance. public FindFile(string rootDirectory, string filePattern, bool recursive, bool includeFolders) : this(rootDirectory, filePattern, recursive, includeFolders, true) { } /// Creates a FindFile instance. public FindFile(string rootDirectory, string filePattern, bool recursive, bool includeFolders, bool includeFiles) { if (String.IsNullOrEmpty(rootDirectory) || String.IsNullOrEmpty(filePattern)) throw new ArgumentException(); _ff = new Win32FindData(); _ff.BufferAddress = IntPtr.Zero; _ff.Buffer = new char[0x1000]; _ff.Value = new Kernel32.WIN32_FIND_DATA(); _recursive = recursive; _includeFolders = includeFolders; _includeFiles = includeFiles; BaseDirectory = rootDirectory; FilePattern = filePattern; } /// /// The event-handler to raise when a file or folder is found. /// public event EventHandler FileFound; /// /// Gets or sets the maximum number of allowed characters in a complete path, default = 4kb /// public int MaxPath { get { return _ff.Buffer.Length; } set { Array.Resize(ref _ff.Buffer, Check.InRange(value, Kernel32.MAX_PATH, 0x100000)); } } private int UncPrefixLength { get { return _isUncPath ? 4 : 0; } } /// Gets or sets the base directory to search within public string BaseDirectory { get { return new String(_ff.Buffer, UncPrefixLength, _baseOffset - UncPrefixLength); } set { if (value.IndexOfAny(InvalidFilePathChars) > 0) throw new InvalidOperationException("Invalid characters in path."); if (!value.StartsWith(@"\\")) value = UncPrefix + value; if (!value.EndsWith(@"\")) value += @"\"; _isUncPath = value.StartsWith(UncPrefix); value.CopyTo(0, _ff.Buffer, 0, _baseOffset = value.Length); } } /// /// Gets or sets the file pattern to match while enumerating files and folders. /// public string FilePattern { get { return new String(_fpattern); } set { if (value.IndexOfAny(InvalidFilePatternChars) >= 0) throw new InvalidOperationException("Invalid characters in pattern."); _fpattern = value.TrimStart(SLASH).ToCharArray(); } } /// Gets or sets the Recursive flag public bool Recursive { get { return _recursive; } set { _recursive = value; } } /// Gets or sets the IncludeFiles flag public bool IncludeFiles { get { return _includeFiles; } set { _includeFiles = value; } } /// Gets or sets the IncludeFolders flag public bool IncludeFolders { get { return _includeFolders; } set { _includeFolders = value; } } /// Gets or sets the RaiseOnAccessDenied flag, when set to true an 'Access Denied' can be raised public bool RaiseOnAccessDenied { get; set; } /// Performs the search raising the FileFound event for each entry matching the request public void Find(string pattern) { FilePattern = pattern; Find(); } /// Performs the search raising the FileFound event for each entry matching the request public void Find() { Check.NotNull(FileFound); GCHandle hdl = GCHandle.Alloc(_ff.Buffer, GCHandleType.Pinned); try { FileFoundEventArgs args = new FileFoundEventArgs(_ff); _ff.BufferAddress = hdl.AddrOfPinnedObject(); FindFileEx(args, _baseOffset); } finally { _ff.BufferAddress = IntPtr.Zero; hdl.Free(); } } private bool IsWild() { return (_fpattern.Length == 1 && _fpattern[0] == '*') || (_fpattern.Length == 3 && _fpattern[0] == '*' && _fpattern[1] == '.' && _fpattern[2] == '*') ; } private void FindFileEx(FileFoundEventArgs args, int slength) { Kernel32.FINDEX_INFO_LEVELS findInfoLevel = Kernel32.FINDEX_INFO_LEVELS.FindExInfoStandard; Kernel32.FINDEX_ADDITIONAL_FLAGS additionalFlags = 0; if (Environment.OSVersion.Version.Major >= 6) { //Ignore short-names findInfoLevel = Kernel32.FINDEX_INFO_LEVELS.FindExInfoBasic; //Use large fetch table additionalFlags = Kernel32.FINDEX_ADDITIONAL_FLAGS.FindFirstExLargeFetch; } _fpattern.CopyTo(_ff.Buffer, slength); _ff.Buffer[slength + _fpattern.Length] = ZERO; IntPtr hFile = Kernel32.FindFirstFileEx( _ff.BufferAddress, findInfoLevel, out _ff.Value, Kernel32.FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, additionalFlags); if ((IntPtr.Size == 4 && hFile.ToInt32() == -1) || (IntPtr.Size == 8 && hFile.ToInt64() == -1L)) { Win32Error(Marshal.GetLastWin32Error()); return; } bool traverseDirs = _recursive && IsWild(); try { do { int sposition = slength; for (int ix = 0; ix < Kernel32.MAX_PATH && sposition < _ff.Buffer.Length && _ff.Value.cFileName[ix] != 0; ix++) _ff.Buffer[sposition++] = _ff.Value.cFileName[ix]; if (sposition == _ff.Buffer.Length) throw new PathTooLongException(); if (!_ff.Value.IgnoredByName) { bool isDirectory = (_ff.Value.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; if ((_includeFolders && isDirectory) || (_includeFiles && !isDirectory)) { args.SetNameOffsets(UncPrefixLength, slength, sposition); FileFound(this, args); } if (traverseDirs && isDirectory) { _ff.Buffer[sposition++] = SLASH; FindFileEx(args, sposition); } } } while (!args.CancelEnumeration && Kernel32.FindNextFile(hFile, out _ff.Value)); } finally { Kernel32.FindClose(hFile); } // Recursive search for patterns other than '*' and '*.*' requires we enum directories again if (_recursive && !traverseDirs) { _ff.Buffer[slength] = '*'; _ff.Buffer[slength + 1] = ZERO; hFile = Kernel32.FindFirstFileEx( _ff.BufferAddress, findInfoLevel, out _ff.Value, Kernel32.FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, additionalFlags); if ((IntPtr.Size == 4 && hFile.ToInt32() == -1) || (IntPtr.Size == 8 && hFile.ToInt64() == -1L)) { Win32Error(Marshal.GetLastWin32Error()); return; } try { do { int sposition = slength; for (int ix = 0; ix < Kernel32.MAX_PATH && sposition < _ff.Buffer.Length && _ff.Value.cFileName[ix] != 0; ix++) _ff.Buffer[sposition++] = _ff.Value.cFileName[ix]; if (sposition == _ff.Buffer.Length) throw new PathTooLongException(); if (!_ff.Value.IgnoredByName) { bool isDirectory = (_ff.Value.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; if (isDirectory) { _ff.Buffer[sposition++] = SLASH; FindFileEx(args, sposition); } } } while (!args.CancelEnumeration && Kernel32.FindNextFile(hFile, out _ff.Value)); } finally { Kernel32.FindClose(hFile); } } } private void Win32Error(int errorCode) { switch(errorCode) { case Kernel32.ERROR_FILE_NOT_FOUND: case Kernel32.ERROR_PATH_NOT_FOUND: return; case Kernel32.ERROR_ACCESS_DENIED: if (!RaiseOnAccessDenied) return; goto default; default: throw new Win32Exception(errorCode); } } } }