遇到的问题
以前在学校的时候做过一个类似Bridge的图片浏览器,当时就对系统的文件树进行了显示。后面就没有涉及这一块的开发了。
最近想做一个文件夹大小统计的功能,在使用C#对文件进行遍历时,发现许多文件夹都会报System.UnauthorizedAccessException的错误。
最开始我是想通过判断文件夹的访问权限来避开这些未授权的文件夹。
我尝试了下面的C#代码
1 string path = @"xxx"; 2 string NtAccountName = System.Security.Principal.WindowsIdentity.GetCurrent().Name; 3 4 DirectoryInfo di = new DirectoryInfo(path); 5 DirectorySecurity acl = di.GetAccessControl(AccessControlSections.All); 6 AuthorizationRuleCollection rules = acl.GetAccessRules(true, true, typeof(NTAccount)); 7 8 foreach (AuthorizationRule rule in rules) 9 { 10 if (rule.IdentityReference.Value.Equals(NtAccountName, StringComparison.CurrentCultureIgnoreCase)) 11 { 12 var filesystemAccessRule = (FileSystemAccessRule)rule; 13 14 if ((filesystemAccessRule.FileSystemRights & FileSystemRights.Read) > 0 && filesystemAccessRule.AccessControlType != AccessControlType.Deny) 15 { 16 //success 17 } 18 else 19 { 20 //failed 21 } 22 } 23 }
但是在文件夹里DACL中,并不包含了当前用户对应的权限,所以会判断不准确。
后面我又查看了.NET的源码,发现里面会对权限进行判断,判断失败就会引发异常。
1 internal static void CheckPermissions(string displayPath, string fullPath, bool checkHost, FileSecurityStateAccess access = FileSecurityStateAccess.Read) 2 { 3 // You need read access to the directory to be returned back and write access to all the directories 4 // that you need to create. If we fail any security checks we will not create any directories at all. 5 // We attempt to create directories only after all the security checks have passed. This is avoid doing 6 // a demand at every level. 7 8 #if FEATURE_CORECLR 9 if (checkHost) 10 { 11 FileSecurityState state = new FileSecurityState( 12 access, 13 path: displayPath, 14 canonicalizedPath: GetDemandDir(fullPath, thisDirOnly: true)); 15 state.EnsureState(); // do the check on the AppDomainManager to make sure this is allowed 16 } 17 #else 18 // In full trust we want to avoid allocating another string via GetDemandDir() 19 if (CodeAccessSecurityEngine.QuickCheckForAllDemands()) 20 FileIOPermission.EmulateFileIOPermissionChecks(fullPath); 21 else 22 FileIOPermission.QuickDemand( 23 (FileIOPermissionAccess)access, 24 GetDemandDir(fullPath, thisDirOnly: true), 25 checkForDuplicates: false, 26 needFullPath: false); 27 #endif 28 }
最后直接使用try...catch...跳过,但运行不够理想。
回想起以前使用WinAPI去遍历文件,试了下,真的可以,也不会报错。
使用WinAPI实现
使用WinAPI去遍历文件主要涉及以下几个函数,FindFirstFile、FindNextFile和FindClose等。
FindFirstFile:搜索与特定名称匹配的文件或子目录
函数声明如下
1 HANDLE FindFirstFileW( 2 [in] LPCWSTR lpFileName, 3 [out] LPWIN32_FIND_DATAA lpFindFileData 4 );
参数:lpFileName
指定目录、路径,以及文件名。文件名可以包括通配符,例如星号(*)或问号(?)。此参数不应为NULL,无效的字符串(例如,空字符串或缺少终止空字符的字符串)。尾部以反斜杠(\)结尾。
如果字符串以通配符、句点(.)或目录名称结尾,那么用户必须对路径上的根目录和所有子目录具有访问权限。
参数:lpFindFileData
指向WIN32_FIND_DATA结构的指针,用于接收搜索到的文件或目录的信息。
WIN32_FIND_DATA结构定义如下:
1 typedef struct _WIN32_FIND_DATAW { 2 DWORD dwFileAttributes; //文件属性 3 FILETIME ftCreationTime; //指定创建文件或目录的时间 4 FILETIME ftLastAccessTime; //上次运行时间(文件)或创建时间(目录) 5 FILETIME ftLastWriteTime; //上次写入、截断或覆盖时间 6 DWORD nFileSizeHigh; //文件大小的高位 DWORD 值(以字节为单位) 7 DWORD nFileSizeLow; //文件大小的低位 DWORD 值(以字节为单位) 8 DWORD dwReserved0; //保留 9 DWORD dwReserved1; //保留 10 _Field_z_ WCHAR cFileName[ MAX_PATH ]; //文件的名称 11 _Field_z_ WCHAR cAlternateFileName[ 14 ]; //文件的可选名称 12 } WIN32_FIND_DATAW, *PWIN32_FIND_DATAW, *LPWIN32_FIND_DATAW;
返回值:HANDLE
如果函数成功,则返回值是在后续调用FindNextFile或 FindClose中使用的搜索句柄,lpFindFileData参数包含搜索到的第一个文件或目录的信息。
如果函数失败或无法从lpFileName参数的搜索字符串中找到文件,则返回值为INVALID_HANDLE_VALUE,并且lpFindFileData的内容是不确定的。
FindNextFile函数:继续搜索文件
函数声明如下
1 BOOL FindNextFileA( 2 [in] HANDLE hFindFile, 3 [out] LPWIN32_FIND_DATAA lpFindFileData 4 );
参数:hFindFile
指向由前一次调用FindFirstFile或 FindFirstFileEx函数返回的搜索句柄。
参数:lpFindData
指向WIN32_FIND_DATA结构的指针,该结构接收搜索到的文件或子目录的信息。
返回值:BOOL
如果函数成功,则返回TRUE,lpFindFileData参数包含搜索到的下一个文件或目录的信息。
如果函数失败,则返回FALSE,并且lpFindFileData的内容是不确定的。
FindClose函数:关闭由FindFirstFile函数打开的文件搜索句柄
函数声明如下:
1 BOOL FindClose( 2 [in, out] HANDLE hFindFile 3 );
参数:hFindFile
文件搜索句柄
返回值:BOOL
如果函数成功,则返回TRUE。
如果函数失败,则返回FALSE。
文件遍历的实现原理
1、构建搜索字符串,如我要遍历C:\\盘下的文件,构建搜索字符串为C:\\*.*(*是通配符,代表所有文件)
2、调用FindFirstFile函数,按照指定的搜索路径和类型进行搜索,搜索结果保存在由WIN32_FIND_DATA 结构体指针指向的内存中。结构体WIN32_FIND_DATA包含文件的名称、创建日期、属性、大小等信息。
3、根据WIN32_FIND_DATA结构体中的成员dwFileAttributes来判断搜索到文件属性,若值为FILE_ATTRIBUTE_DIRECTORY,则表示该文件为目录,再次构建搜索字符串,进行递归搜索,否则为为文件,保存成员cFileName值以备后续使用或直接输出。
4、对目录进行递归搜索时,需要对当前目录“.”和上一层目录“..”进行过滤,如果对这两个目录递归遍历,则会陷入无限搜索中,而且还会造成缓冲区溢出。
5、调用FindNextFile函数搜索下一个文件,根据返回值判断是否搜索到文件,若没有,则说明文件遍历结束,然后退出;若搜索到文件,则继续循环上面的操作,获取文件名,判断文件属性等。
6、搜索完毕后,调用FindClose函数文件关闭搜索句柄,并释放缓冲区资源。
看起来挺复杂的,总结起来就是,构建搜索字符串进行文件搜索,是文件就输出,并继续搜索下一个文件,是目录,再次构建搜索字符串进行上述过程。
C++实现代码:
1 VOID SearchFile(LPCTSTR pszDirectory) 2 { 3 // 搜索指定类型文件 4 DWORD dwBufferSize = 2048; 5 TCHAR* pszFileName = NULL; 6 TCHAR* pTempSrc = NULL; 7 WIN32_FIND_DATA findData{}; 8 BOOL bRet = FALSE; 9 10 // 申请动态内存 11 pszFileName = new TCHAR[dwBufferSize]; 12 pTempSrc = new TCHAR[dwBufferSize]; 13 14 // 构造搜索文件类型字符串, *.*表示搜索所有文件类型 15 wsprintf(pszFileName, L"%s\\*.*", pszDirectory); 16 17 // 搜索第一个文件 18 HANDLE hFile = FindFirstFile(pszFileName, &findData); 19 if (INVALID_HANDLE_VALUE != hFile) 20 { 21 do 22 { 23 // 要过滤掉 当前目录"." 和 上一层目录"..", 否则会不断进入死循环遍历 24 if ('.' == findData.cFileName[0]) 25 { 26 continue; 27 } 28 // 拼接文件路径 29 wsprintf(pTempSrc, L"%s\\%s", pszDirectory, findData.cFileName); 30 // 判断是否是目录还是文件 31 if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 32 { 33 // 目录, 则继续往下递归遍历文件 34 SearchFile(pTempSrc); 35 } 36 else 37 { 38 // 文件 39 wprintf_s(L"%s\n", pTempSrc); 40 } 41 42 // 搜索下一个文件 43 } while (FindNextFile(hFile, &findData)); 44 } 45 46 // 关闭文件句柄 47 FindClose(hFile); 48 // 释放内存 49 delete[]pTempSrc; 50 pTempSrc = NULL; 51 delete[]pszFileName; 52 pszFileName = NULL; 53 }
这里我直接遍历 C盘下的文件
1 int main() 2 { 3 SearchFile("C:\\"); 4 }
运行效果如下:
C#实现代码:
C#中调用跟C++中一致,做好相应数据类型的映射即可
首先需要声明P/Invoke签名
1 [DllImport("Kernel32.dll",CharSet = CharSet.Unicode)] 2 public static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); 3 4 [DllImport("Kernel32.dll",CharSet= CharSet.Unicode)] 5 public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData); 6 7 [DllImport("Kernel32.dll")] 8 public static extern bool FindClose(IntPtr hFindFile);
定义数据类型
1 public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); 2 public static readonly uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010; 3 4 public struct FILETIME 5 { 6 public uint dwLowDateTime; 7 public uint dwHighDateTime; 8 } 9 10 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 11 public struct WIN32_FIND_DATA 12 { 13 public System.IO.FileAttributes dwFileAttributes; 14 15 public FILETIME ftCreationTime; 16 17 public FILETIME ftLastAccessTime; 18 19 public FILETIME ftLastWriteTime; 20 21 public uint nFileSizeHigh; 22 23 public uint nFileSizeLow; 24 25 public uint dwReserved0; 26 27 public uint dwReserved1; 28 29 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] 30 public string cFileName; 31 32 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] 33 public string cAlternateFileName; 34 }
觉得麻烦的,可以直接引用 Nuget包,我习惯了手写,所以就自己撸了
1 NuGet\Install-Package Vanara.PInvoke.Kernel32 -Version 4.0.0-beta
调用代码如下:
1 public static void EnumerateSubDirectory(string dir) 2 { 3 // 搜索指定类型文件 4 string pszFileName; 5 string pTempSrc; 6 WIN32_FIND_DATA FileData = new WIN32_FIND_DATA(); 7 8 // 构造搜索文件类型字符串, *.*表示搜索所有文件类型 9 pszFileName = $"{dir}\\*.*"; 10 11 // 搜索第一个文件 12 IntPtr hFile = FindFirstFile(pszFileName, out FileData); 13 if (INVALID_HANDLE_VALUE != hFile) 14 { 15 do 16 { 17 // 要过滤掉 当前目录"." 和 上一层目录"..", 否则会不断进入死循环遍历 18 if ('.' == FileData.cFileName[0]) 19 { 20 continue; 21 } 22 // 拼接文件路径 23 pTempSrc = $"{dir}\\{FileData.cFileName}"; 24 // 判断是否是目录还是文件 25 if (FileData.dwFileAttributes == System.IO.FileAttributes.Directory) 26 { 27 // 目录, 则继续往下递归遍历文件 28 EnumerateSubDirectory(pTempSrc); 29 } 30 else 31 { 32 // 文件 33 Console.WriteLine(pTempSrc); 34 } 35 36 // 搜索下一个文件 37 } while (FindNextFile(hFile, out FileData)); 38 } 39 40 // 关闭文件句柄 41 FindClose(hFile); 42 }
参考资料:
https://learn.microsoft.com/zh-cn/windows/win32/fileio/listing-the-files-in-a-directory