Skip to content

Instantly share code, notes, and snippets.

@Maxstupo
Last active April 23, 2020 05:40
Show Gist options
  • Save Maxstupo/4281a39cf9502906b8e48cf6528d9a0c to your computer and use it in GitHub Desktop.
Save Maxstupo/4281a39cf9502906b8e48cf6528d9a0c to your computer and use it in GitHub Desktop.
C# FileScanner using FindFirstFile and FindNextFile with optional directory statistics
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
namespace Maxstupo.Utility {
public enum FileType {
File = 0,
Folder = 1
}
public sealed class FileResult {
/// <summary>Gets the absolute path to this file.</summary>
public string Path { get; }
/// <summary>Gets the size of this file in bytes.</summary>
public long Size { get; }
public FileAttributes Attributes { get; }
public DateTime CreationTime { get; }
public DateTime LastWriteTime { get; }
public DateTime LastAccessTime { get; }
public FileType Type { get; }
public bool IsFolder => Type == FileType.Folder;
public int Depth { get; }
public DirectoryStats Stats { get; }
public FileResult(string path, long filesize, FileAttributes attributes, DateTime creationTime, DateTime lastWriteTime, DateTime lastAccessTime, FileType type, int depth, DirectoryStats stats = null) {
Path = path;
Size = filesize;
Attributes = attributes;
CreationTime = creationTime;
LastWriteTime = lastWriteTime;
LastAccessTime = lastAccessTime;
Type = type;
Depth = depth;
Stats = stats;
}
}
public sealed class DirectoryStats {
public long Files { get; private set; }
public long Subdirectories { get; private set; }
public long Size { get; private set; }
public long TotalFiles { get; private set; }
public long TotalSubdirectories { get; private set; }
public long TotalSize { get; private set; }
public long Items => Files + Subdirectories;
public long TotalItems => TotalFiles + TotalSubdirectories;
public void AddDirectory(ref DirectoryStats stats) {
Subdirectories++;
TotalSubdirectories += stats.TotalSubdirectories + 1;
TotalFiles += stats.TotalFiles;
TotalSize += stats.TotalSize;
}
public void AddFile(long size) {
Files++;
TotalFiles++;
Size += size;
TotalSize += size;
}
}
// TODO: Check for valid method parameter input (e.g. If provided path is a directory and exists, etc..)
public static class Win32FileScanner {
private static readonly IntPtr invalidHandle = new IntPtr(-1);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct Win32FindData {
public FileAttributes dwFileAttributes;
public FileTime ftCreationTime;
public FileTime ftLastAccessTime;
public FileTime ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[StructLayout(LayoutKind.Sequential)]
private struct FileTime {
public uint dwLowDateTime;
public uint dwHighDateTime;
};
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindFirstFile(string lpFileName, out Win32FindData lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool FindNextFile(IntPtr hFindFile, out Win32FindData lpFindFileData);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FindClose(IntPtr hFindFile);
/// <summary>
/// Provides a enumerable of file results that contain a range of information about both files and directories discovered in the provided directory path.
/// </summary>
/// <param name="path">The folder path.</param>
/// <param name="rootStats">A cumulative stat object representing total files, folder, sizes, etc for the base directory provided.</param>
/// <param name="maxDepth">Maximum folder depth to recurse. Set -1 to disable max depth.</param>
public static IEnumerable<FileResult> EnumerateFileItems(string path, out DirectoryStats rootStats, int maxDepth = -1) {
rootStats = new DirectoryStats();
return ScanRecursive(Path.GetFullPath(path), maxDepth, 0, rootStats);
}
private static IEnumerable<FileResult> ScanRecursive(string path, int maxDepth, int depth, DirectoryStats parent) {
IntPtr handle = invalidHandle;
try {
handle = FindFirstFile($@"{path}\*", out Win32FindData findData);
if (handle != invalidHandle) {
do {
// Skip symlink (and junction?)
if (findData.dwFileAttributes.HasFlag(FileAttributes.ReparsePoint | FileAttributes.Directory) || !IsValidFile(findData))
continue;
string fullPath = Path.Combine(path, findData.cFileName);
DateTime creationTime = ToDateTime(findData.ftCreationTime);
DateTime lastWriteTime = ToDateTime(findData.ftLastWriteTime);
DateTime lastAccessTime = ToDateTime(findData.ftLastAccessTime);
if (findData.dwFileAttributes.HasFlag(FileAttributes.Directory)) { // Directory
if (maxDepth >= 0 && depth + 1 > maxDepth)
continue;
DirectoryStats stats = new DirectoryStats();
foreach (FileResult fileResult in ScanRecursive(fullPath, maxDepth, depth + 1, stats))
yield return fileResult;
parent.AddDirectory(ref stats);
yield return new FileResult(fullPath, 0, findData.dwFileAttributes, creationTime, lastWriteTime, lastAccessTime, FileType.Folder, depth, stats);
} else { // File
long filesize = GetFilesize(findData);
parent.AddFile(filesize);
yield return new FileResult(fullPath, filesize, findData.dwFileAttributes, creationTime, lastWriteTime, lastAccessTime, FileType.File, depth);
}
} while (FindNextFile(handle, out findData));
} else {
// Removed exception, as handle can be invalid if we dont have access.
// throw new DirectoryNotFoundException($"Failed to find directory: {path}");
}
} finally {
FindClose(handle);
}
}
/// <summary>
/// A barebones version of EnumerateFiles() provides only the path and not statistics. Only provides files and not directories.
/// </summary>
/// <param name="maxDepth">Maximum folder depth to recurse. Set -1 to disable max depth.</param>
public static IEnumerable<string> EnumerateFilepaths(string path, int maxDepth = -1) {
return ScanRecursiveFilepath(Path.GetFullPath(path), maxDepth, 0);
}
private static IEnumerable<string> ScanRecursiveFilepath(string path, int maxDepth, int depth) {
IntPtr handle = invalidHandle;
try {
handle = FindFirstFile($@"{path}\*", out Win32FindData findData);
if (handle != invalidHandle) {
do {
// Skip symlink (and junction?)
if (findData.dwFileAttributes.HasFlag(FileAttributes.ReparsePoint | FileAttributes.Directory) || !IsValidFile(findData))
continue;
string fullPath = Path.Combine(path, findData.cFileName);
if (findData.dwFileAttributes.HasFlag(FileAttributes.Directory)) { // Directory
if (maxDepth >= 0 && depth + 1 > maxDepth)
continue;
foreach (string filePath in ScanRecursiveFilepath(fullPath, maxDepth, depth + 1))
yield return filePath;
// yield return fullPath;
} else { // File
yield return fullPath;
}
} while (FindNextFile(handle, out findData));
} else {
// Removed exception, as handle can be invalid if we dont have access.
// throw new DirectoryNotFoundException($"Failed to find directory: {path}");
}
} finally {
FindClose(handle);
}
}
private static bool IsValidFile(Win32FindData findData) {
return !findData.cFileName.Equals(".") && !findData.cFileName.Equals("..");
}
private static long GetFilesize(Win32FindData findData) {
return findData.nFileSizeLow + (long)findData.nFileSizeHigh * uint.MaxValue;
}
/// <summary>
/// Converts the provided Win32 FileTime struct into a .NET DateTime struct.
/// </summary>
private static DateTime ToDateTime(FileTime fileTime) {
byte[] highBytes = BitConverter.GetBytes(fileTime.dwHighDateTime);
Array.Resize(ref highBytes, 8);
long longValue = BitConverter.ToInt64(highBytes, 0);
longValue <<= 32;
longValue |= fileTime.dwLowDateTime;
return DateTime.FromFileTime(longValue);
}
// TEMP: Main method for testing scanner.
static void Main(string[] args) {
foreach (string file in EnumerateFilepaths(@"C:\", -1)) {
Console.WriteLine(file);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment