最近接触Windows文件备份相关的业务,从Win32 API入手对Windows文件系统的基本概念做一个总结。学习Windows文件系统最好方式还是阅读官方文档,本文只是对相关概念进行粗浅的介绍,权当抛砖引玉。
本文完整代码见:https://github.com/XUranus/FileSystemUtil
Win32 API Doc
官方文档。文档会详细描述每一个API的入参、出参、返回的字段含义和取值范围,部分还会给出Example程序。
Windows程序开发一般使用MSVC,和GCC有不少区别。为了更有效率的学习Windows文件系统,在介绍文件系统之前,本文先给Linux转Windows的开发者介绍一下Windows的编码和文档阅读方式,磨刀不误砍柴工。
UTF-8/UTF-16
涉及字符串的Win32 API一般提供两类接口:
- Ansi字符串接口(一般以
A
结尾) - UTF-16宽字符串接口(一般以
W
结尾)
例如GetFileAttributesA和GetFileAttributesW:typedef _Null_terminated_ CONST WCHAR *LPCWSTR, *PCWSTR;
typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
...
DWORD GetFileAttributesA(
[in] LPCSTR lpFileName
);
DWORD GetFileAttributesW(
[in] LPCWSTR lpFileName
);
LPCSTR
的类型是CHAR*
、即char*
,表示一个ANSI字符串的指针。而LPCWSTR
的类型是WCHAR*
、即wchar_t*
,表示一个UTF-16字符串指针。此外还提供不显式声明字符串类型的接口,例如GetFileAttributes
,他的宏定义如下:
可见这类接口会根据是否定义UNICODE
宏来决定使用哪种API。对于字符串可能包含Unicode字符的程序,尽量使用宽字符API,否则可能会调用失败!
ANSI版本的Window API中, 路径长度被限制在MAX_PATH
、即260个字符. 要拓展这个限制到23767宽字符(wide character), 需要调用这个函数的Unicode版本, 并且在路径的首部加上\\?\
,例如C:\Users\XUranus\Desktop
应转为\\?\C:\Users\XUranus\Desktop
再作为参数传入。本文将默认使用Unicode版本的API,并默认路径已经带上了\\?\
前缀。有关文件路径详见Naming Files, Paths, and Namespaces
C++程序一般使用UTF-8编码的std::string
来表示字符串,Linux接口一般使用UTF-8编码的字符串,而Windows是一个UTF-16的操作系统。如果程序需要跨平台,一般在公共业务部分用std::string
,因为UTF-8表示相同的含有Unicode特殊字符的字符串可能占用更少的空间,在网络IO和数据落盘时的开销也更小,而在涉及Windows接口的部分则最好使用UTF-16编码的std::wstring
。此时就不得不考虑std::string
和std::wstring
的转码问题。
C++11标准库提供了std::wstring
和std::wstring
的无损互转换工具std::codecvt_utf8_utf16
,用法如下:
std::wstring Utf8ToUtf16(const std::string& str)
{
using ConvertTypeX = std::codecvt_utf8_utf16<wchar_t>;
std::wstring_convert<ConvertTypeX> converterX;
std::wstring wstr = converterX.from_bytes(str);
return wstr;
}
std::string Utf16ToUtf8(const std::wstring& wstr)
{
using ConvertTypeX = std::codecvt_utf8_utf16<wchar_t>;
std::wstring_convert<ConvertTypeX> converterX;
return converterX.to_bytes(wstr);
}
对于UTF-8和UTF-16编码的概念和转换方法,本文就不再赘述,读者可以查询相关资料自行拓展。了解了编码相关的知识,就能知道Windows的两种API该如何使用,接下来说说Win32 API文档的阅读方式。
Win32 API Document
官方文档会根据头文件列出其中包含的API,例如FindFirstFileW,Syntax
中描述如下:HANDLE FindFirstFileW(
[in] LPCWSTR lpFileName,
[out] LPWIN32_FIND_DATAW lpFindFileData
);
表明了第一个参数lpFileName
是个LPCWSTR
类型的入参,而lpFindFileData
是个LPWIN32_FIND_DATAW
类型的出参,返回值是HANDLE
类型,表示的含义可以自Return Value
中查到。LPWIN32_FIND_DATAW
则是该接口获取的信息,其结构体构成也可在页面Parameters
中找到详细信息的链接。
在Remarks
区域,列举了该API调用的注意点,例如会提示你用FindClose
在调用成功后关闭API返回的句柄。
在Linux中一般用int fd
描述一个打开文件的描述符(File Descriptor),当fd < 0
往往意味调用失败。Windows中用HANDLE hFile
,实际类型是void*
来表示一个打开文件的句柄(File Handle),如果hFile
为INVALID_HANDLE_VALUE
则表示调用失败。
失败处理
几乎所有的操作都有可能失败,或者由于参数不合法,或者由于没有权限、没有资源。所以阅读Windows API的时候需要特别留意失败场景的处理方式。有时候一个步骤失败但之前已经分配了资源,这个时候Remark部分可能会提示你手动释放之前步骤准备的资源。再Linux下失败的操作往往可以通过errno
宏拿到错误码,在Windows下这个错误码用GetLastError()
获取,它能返回上一个DWORD类型的值标记操作失败的原因,具体参考:System Error Codes
文件属性
Windows文件拥有和Linux文件相似的属性信息,在Linux中一般用stat
来获取文件的struct stat
结构,里面包含文件的基本属性信息。Windows下获取文件属性的API有很多,但是大部分只能获取很小一部分属性。能获取比较完整的属性的API是GetFileInformationByHandle()
,它将一个文件/目录句柄作为参数,获取文件信息存储在BY_HANDLE_FILE_INFORMATION
结构中。
首先定义一个结构体StatResult
来描述一个文件属性信息对象,接下来将基于它逐步封装文件属性查询接口:class StatResult {
public:
StatResult(const std::wstring& wPath, const BY_HANDLE_FILE_INFORMATION& handleFileInformation);
private:
BY_HANDLE_FILE_INFORMATION m_handleFileInformation{};
std::wstring m_wPath; /* raw input path */
};
StatResult::StatResult(const std::wstring& wPath, const BY_HANDLE_FILE_INFORMATION& handleFileInformation)
: m_wPath(wPath)
{
memcpy_s(&m_handleFileInformation, sizeof(BY_HANDLE_FILE_INFORMATION),
&handleFileInformation, sizeof(BY_HANDLE_FILE_INFORMATION));
}
要获取文件/目录对应的BY_HANDLE_FILE_INFORMATION
结构体,就需要先拿到文件/目录的句柄。用CreateFileW()
打开一个文件并获取HANDLE
类型的句柄。HANDLE
类型的句柄用于指向Windows中打开的资源,它类似于Linux下的int
类型的文件描述符(File Descriptor)。Linux下常用int fd = open(fname, mode)
打开一个文件,且用完资源后需要用close(fd)
释放,同理,在Windows下也需要对打开的句柄用CloseHandle(handle)
进行关闭。Linux下fd < 0
常用于描述失效的句柄,而Windows下失效的句柄等于内置常量INVALID_HANDLE_VALUE
。无论在Linux下还是Windows下,句柄都不只局限于文件,对句柄的操作一定要检查句柄是否合法,用完句柄后也一定要及时释放。
CreateFileW
相关参数说明详见:CreateFileW function (fileapi.h)
用GetFileInformationByHandle
实现一个Windows下的stat
函数,他将BY_HANDLE_FILE_INFORMATION
信息封装在StatResult
结构体中:std::optional<StatResult> StatW(const std::wstring& wPath)
{
BY_HANDLE_FILE_INFORMATION handleFileInformation{};
HANDLE hFile = ::CreateFileW(
wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
0);
if (hFile == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
if (::GetFileInformationByHandle(hFile, &handleFileInformation) == 0) {
::CloseHandle(hFile);
return std::nullopt;
}
::CloseHandle(hFile);
return std::make_optional<StatResult>(wPath, handleFileInformation);
}BY_HANDLE_FILE_INFORMATION
的成员定义如下:typedef struct _BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD dwVolumeSerialNumber;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD nNumberOfLinks;
DWORD nFileIndexHigh;
DWORD nFileIndexLow;
} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION;
拥有了BY_HANDLE_FILE_INFORMATION
就可以获取文件的基本信息:
dwFileAttributes
:它类似于Linuxstat
获得的mode_t
类型,用符号位标记文件/目录的attributes,例如:目录、稀疏文件、归档文件、系统文件、隐藏文件、等ftCreationTime
:类似于Linuxstat
信息中的time_t st_ctime
字段,记录创建时间对应的Windows时间戳ftLastAccessTime
:类似于Linuxstat
信息中的time_t st_atime
字段,记录上次访问时间对应的Windows时间戳ftLastWriteTime
:类似于Linuxstat
信息中的time_t st_mtime
字段,记录上次修改时间对应的Windows时间戳dwVolumeSerialNumber
:类似于Linuxstat
信息中的st_rdev
字段,记录创建时间nFileSizeHigh
、nFileSizeLow
:类似于Linuxstat
信息中的st_size
字段,用高低字节记录文件的大小(单位bytes)nNumberOfLinks
:类似于Linuxstat
信息中的nlink
字段,记录硬链接数量nFileIndexHigh
、nFileIndexLow
:用高低字节记录文件/目录的index
值,index
是Windows文件系统中对标Linux文件系统中inode
的概念。区别是Linux中inode全局唯一,而Windows中index只在卷中唯一
Linux文件系统还有组ID和用户ID的概念,他们在Windows下默认为0
其中size
,index
被拆分成了高低字节,time
字段也是拆分为高低字节的结构体,它的定义如下:typedef struct _FILETIME {
DWORD dwLowDateTime;
DWORD dwHighDateTime;
} FILETIME, *PFILETIME, *LPFILETIME;
这类用双字(两个DWORD描述的)数据转uint64_t
可以构造LARGE_INTEGER
结构体,并通过QuadPart
属性来整合两个字段的最终值inline uint64_t CombineDWORD(DWORD low, DWORD high) {
LARGE_INTEGER li;
li.LowPart = low;
li.HighPart = high;
return li.QuadPart;
}
UNIX的时间戳从1970年1月1日开始,Windows的时间戳转UNIX时间戳需要减去0x019DB1DED53E8000
换算inline uint64_t ConvertWin32Time(DWORD low, DWORD high)
{
const uint64_t UNIX_TIME_START = 0x019DB1DED53E8000; /* January 1, 1970 (start of Unix epoch) in "ticks" */
const uint64_t TICKS_PER_SECOND = 10000000; /* a tick is 100ns */
LARGE_INTEGER li;
li.LowPart = low;
li.HighPart = high;
return li.QuadPart;
/* Convert ticks since 1/1/1970 into seconds */
return (li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND;
}
基于以上概念给StatResult
添加相关方法及实现:uint64_t StatResult::AccessTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftLastAccessTime.dwLowDateTime,
m_handleFileInformation.ftLastAccessTime.dwHighDateTime);
}
uint64_t StatResult::CreationTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftCreationTime.dwLowDateTime,
m_handleFileInformation.ftCreationTime.dwHighDateTime);
}
uint64_t StatResult::ModifyTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftLastWriteTime.dwLowDateTime,
m_handleFileInformation.ftLastWriteTime.dwHighDateTime);
}
uint64_t StatResult::UniqueID() const
{
return CombineDWORD(m_handleFileInformation.nFileIndexLow,
m_handleFileInformation.nFileIndexHigh);
}
uint64_t StatResult::Size() const
{
return CombineDWORD(m_handleFileInformation.nFileSizeLow,
m_handleFileInformation.nFileSizeHigh);
}
uint64_t StatResult::DeviceID() const
{
return static_cast<uint64_t>(m_handleFileInformation.dwVolumeSerialNumber);
}
uint64_t StatResult::LinksCount() const
{
return static_cast<uint64_t>(m_handleFileInformation.nNumberOfLinks);
}
DWORD描述的dwFileAttributes
可以与一系列系统常量相与来判断文件是否是某种类型:bool StatResult::IsDirectory() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; }
bool StatResult::IsArchive() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE) != 0; }
bool StatResult::IsCompressed() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0; }
bool StatResult::IsEncrypted() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_ENCRYPTED) != 0; }
bool StatResult::IsSparseFile() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; }
bool StatResult::IsHidden() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) != 0; }
bool StatResult::IsOffline() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) != 0; }
bool StatResult::IsReadOnly() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0; }
bool StatResult::IsSystem() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0; }
bool StatResult::IsTemporary() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY) != 0; }
bool StatResult::IsNormal() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_NORMAL) != 0; }
bool StatResult::IsReparsePoint() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; }
相关FILE_ATTRIBUTE_XXX
常量详见:File Attribute Constants
上述我们尝试实现Windows上对于Linux下stat
的替换方案,用Windows API重新封装了一个StatW
。实际上Windows也提供了stat
方法:stat functions,提供了Linux下stat
一样的参数列表,返回结构struct stat
也有一样的成员结构。只不过该方法只是用于给Linux程序迁移到Windows提供遍历,它并不能获取完整的Windows文件信息。
卷与驱动器
Windows文件系统有卷与驱动器的概念,其中卷(Volume)和Linux中卷的概念一致,卷可以是物理卷、也可以是逻辑卷,卷可能是本地卷、也可能是网络卷(SMB),可能是可读写的,也可能是只读的。
每个卷对应唯一的卷名(VolumeName),每个卷又属于唯一的设备(Device),每个卷可能被挂载在若干数量的路径上。他我们先定义一个Win32VolumeDetail
类,一步步了解卷的相关属性的获取:class Win32VolumesDetail {
public:
Win32VolumesDetail(const std::wstring& wVolumeName);
std::wstring VolumeNameW() const;
std::optional<std::wstring> GetVolumeDeviceNameW();
std::optional<std::vector<std::wstring>> GetVolumePathListW();
private:
std::wstring m_wVolumeName;
};
首先通过API FindFirstVolumeW()
和FindNextVolumeW()
可以遍历所有的卷。VolumeName最大长度不会超过路径的最大长度,可以用Win32 API预制宏MAX_PATH
定义。FindFirstVolume
成功执行会返回第一个卷的句柄,通过它和FindNextVolumeW()
可以遍历并列出所有的卷名(VolumeName)。
constexpr auto VOLUME_BUFFER_MAX_LEN = MAX_PATH; |
该方法只能查询本地卷,不能查询网络卷。
拿到的卷名形如\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\
,由\\?\Volume
和一串GUID构成。有了卷名就可以查询设备和挂载点了。API QueryDosDeviceW
用于根据卷名查询对应设备名,设备名同样最大不超过MAX_PATH
,代码如下:std::optional<std::wstring> Win32VolumesDetail::GetVolumeDeviceNameW()
{
if (m_wVolumeName.size() < 4 ||
m_wVolumeName[0] != L'\\' ||
m_wVolumeName[1] != L'\\' ||
m_wVolumeName[2] != L'?' ||
m_wVolumeName[3] != L'\\' ||
m_wVolumeName.back() != L'\\') { /* illegal volume name */
return std::nullopt;
}
std::wstring wVolumeParam = m_wVolumeName;
wVolumeParam.pop_back(); /* QueryDosDeviceW does not allow a trailing backslash */
wVolumeParam = wVolumeParam.substr(4);
WCHAR deviceNameBuf[DEVICE_BUFFER_MAX_LEN] = L"";
DWORD charCount = ::QueryDosDeviceW(wVolumeParam.c_str(), deviceNameBuf, ARRAYSIZE(deviceNameBuf));
if (charCount == 0) {
return std::nullopt;
}
return std::make_optional<std::wstring>(deviceNameBuf);
}
查询到的设备名形如:\Device\HarddiskVolume3
,由设备类型和序号组成。
一个卷可能由0个、1个或者多个挂载点,可以用GetVolumePathNamesForVolumeNameW
获得:std::optional<std::vector<std::wstring>> Win32VolumesDetail::GetVolumePathListW()
{
/* https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths */
if (m_wVolumeName.size() < 4 ||
m_wVolumeName[0] != L'\\' ||
m_wVolumeName[1] != L'\\' ||
m_wVolumeName[2] != L'?' ||
m_wVolumeName[3] != L'\\' ||
m_wVolumeName.back() != L'\\') { /* illegal volume name */
return std::nullopt;
}
std::vector<std::wstring> wPathList;
PWCHAR devicePathNames = nullptr;
DWORD charCount = MAX_PATH + 1;
bool success = false;
while (true) {
devicePathNames = (PWCHAR) new BYTE[charCount * sizeof(WCHAR)];
if (!devicePathNames) { /* failed to malloc on heap */
return std::nullopt;
}
success = ::GetVolumePathNamesForVolumeNameW(
m_wVolumeName.c_str(),
devicePathNames,
charCount,
&charCount
);
if (success || ::GetLastError() != ERROR_MORE_DATA) {
break;
}
delete[] devicePathNames;
devicePathNames = nullptr;
}
if (success) {
for (PWCHAR nameIdx = devicePathNames;
nameIdx[0] != L'\0';
nameIdx += ::wcslen(nameIdx) + 1) {
wPathList.push_back(std::wstring(nameIdx));
}
}
if (devicePathNames != nullptr) {
delete[] devicePathNames;
devicePathNames = nullptr;
}
return std::make_optional<std::vector<std::wstring>>(wPathList);
}
查询到的挂载点是Windows文件路径,例如:C:\
,D:\
,E:\dir\mount\
(挂载点可以不是根目录)
驱动器(Driver)是区别于卷的Windows特有的概念,驱动器有26个字母盘符可供选择。其中A
、B
盘是早期用于标记软盘,如今驱动器一般从C
开始分配。不同于Posix路径以/
作为文件系统的根目录,Windows每个驱动器都有自己的根目录例如C:\
、D:\
。一般每个磁盘系统会分配一个驱动器,如果驱动器盘符耗尽,可以把磁盘挂载在某个驱动器的某个空白的NTFS卷的目录下例如E:\dir\mount
。Windows文件系统路径一般以反斜杠(Backslash)作为分隔符(Path Separator),而Posix路径一般以左斜杠(Slash)作为分隔符。Win32 API一般也接受非标准的路径例如:C:\User/XUranus/Desktop
,但一些C++标准库对Windows路径有着严格要求,必须写作C:\User\XUranus\Desktop
,否则会调用失败。
驱动器分配可以在“磁盘管理”中右击某个卷选择“更改驱动器号和路径”,可以为卷删除、或者添加一个或多个盘符或挂载路径。对于装载到某个“空白的NTFS卷目录”,实际上是给那个目录创建了一个MountPoint类型的指向点(REPARSE POINT),我们将在下一章详解REPARSE POINT。与Linux文件系统的挂载不同,这种装载类似于将这个空白目录修改成了软连接,会导致文件系统成环!。
Windows提供API GetLogicalDriveStrings
用于获得所有已分配的驱动器卷标:std::vector<std::wstring> GetWin32DriverListW()
{
std::vector<std::wstring> wdrivers;
DWORD dwLen = ::GetLogicalDriveStrings(0, nullptr); /* the length of volumes str */
if (dwLen <= 0) {
return wdrivers;
}
wchar_t* pszDriver = new wchar_t[dwLen];
::GetLogicalDriveStringsW(dwLen, pszDriver);
wchar_t* pDriver = pszDriver;
while (*pDriver != '\0') {
std::wstring wDriver = std::wstring(pDriver);
wdrivers.push_back(wDriver);
pDriver += wDriver.length() + 1;
}
delete[] pszDriver;
pszDriver = nullptr;
pDriver = nullptr;
return wdrivers;
}
返回结果形如:{"C:\","D:\","E:\"}
目录遍历
Linux下用Posix接口遍历目录的流程是:
- 用
opendir()
打开一个目录,获取一个struct dirent*
类型的句柄 - 用
readdir()
传入之前的struct dirent*
句柄并返回下一个句柄
循环该过程直到struct dirent*
为nullptr
。
Windows下遍历目录也有类似的过程,和之前封装StatResult
模拟stat
一样,这里来封装一个OpenDirEntry
模拟struct dirent
:class OpenDirEntry
{
public:
OpenDirEntry(
const std::string& dirPath,
const WIN32_FIND_DATAW& findFileData,
const HANDLE& fileHandle);
bool IsDirectory() const;
std::wstring NameW() const;
bool Next();
/* disable copy/assign construct */
OpenDirEntry(const OpenDirEntry&) = delete;
OpenDirEntry operator = (const OpenDirEntry&) = delete;
~OpenDirEntry();
private:
std::wstring m_dirPath;
HANDLE m_fileHandle = nullptr;
WIN32_FIND_DATAW m_findFileData;
};OpenDirEntry
描述目录遍历过程中的一个入口,提供IsDirectory
判断入口是否是目录,NameW()
返回入口的名称,Next()
返回是否遍历结束。WIN32_FIND_DATAW
是用于存放遍历过程入口信息的结构,HANDLE
指向当前遍历入口的句柄:OpenDirEntry::OpenDirEntry(
const std::string& dirPath,
const WIN32_FIND_DATAW& findFileData,
const HANDLE& fileHandle)
: m_dirPath(Utf8ToUtf16(dirPath)), m_findFileData(findFileData), m_fileHandle(fileHandle) {}
Windows文件遍历用到的API是FindFirstFileW()
和FindNextFileW()
。FindFirstFileW()
接受一个路径的模式串,如果能找到一个复合的遍历入口则返回对应的入口句柄。FindNextFileW()
基于当前句柄返回下一个入口对应的句柄,如果找不到则返回INVALID_HANDLE_VALUE
。遍历过程中当前入口的信息存放在WIN32_FIND_DATAW m_findFileData
中:
Windows支持模式串作为遍历参数,例如
C:\Users\XUranus\Desktop\*.*
用于描述C:\Users\XUranus\Desktop
下所有的目录和文件。详见FindFirstFileA function (fileapi.h)
std::optional<OpenDirEntry> OpenDir(const std::string& wPath) |
遍历完成必须用FindClose()
手动关闭打开目录的句柄,可以用RAII实现:OpenDirEntry::~OpenDirEntry()
{
if (m_fileHandle != nullptr && m_fileHandle != INVALID_HANDLE_VALUE) {
::FindClose(m_fileHandle);
m_fileHandle = nullptr;
}
}
WIN32_FIND_DATAW
定义如下:typedef struct _WIN32_FIND_DATAW {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
_Field_z_ WCHAR cFileName[ MAX_PATH ];
_Field_z_ WCHAR cAlternateFileName[ 14 ];
DWORD dwFileType;
DWORD dwCreatorType;
WORD wFinderFlags;
} WIN32_FIND_DATAW, *PWIN32_FIND_DATAW, *LPWIN32_FIND_DATAW;
可以看到它拥有和BY_HANDLE_FILE_INFORMATION
类似的成员,通过它可以实现判断当前入口的属性、名称、大小等信息:bool OpenDirEntry::IsDirectory() const
{
return (m_findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}
std::string OpenDirEntry::Name() const
{
return Utf16ToUtf8(std::wstring(m_findFileData.cFileName));
}
文件IO
先回顾一下Linux下读取文件IO的流程:
int fd = open(fname, mode)
获取文件的描述符- 用
read(fd, buf, sizeof(buf))
将数据读取buffer - 将
write(fd, buf, len)
将数据从buffer写入文件 - IO完成后用
close(fd)
释放资源
Windows下遵循类似的流程:
HANDLE hFile = CreateFileW()
获取打开文件句柄ReadFile
将文件部分读入bufferWriteFile
将buffer写入文件- IO完成后用
CloseHandle(hFile)
释放资源
基于上述逻辑实现一个拷贝文件的逻辑:bool Win32CopyFileW(const std::wstring& wSrcPath, const std::wstring& wDstPath)
{
const int DEFAULT_BUFF_SIZE = 1024;
char buff[DEFAULT_BUFF_SIZE] = "\0";
/* open file for read */
HANDLE hInFile = ::CreateFileW(
wSrcPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (hInFile == INVALID_HANDLE_VALUE) {
return false;
}
/* open file for write */
HANDLE hOutFile = ::CreateFileW(
wDstPath.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
CREATE_NEW,
0,
nullptr);
if (hOutFile == INVALID_HANDLE_VALUE) {
::CloseHandle(hInFile);
return false;
}
LARGE_INTEGER sizeEx;
::GetFileSizeEx(hInFile, &sizeEx);
uint64_t bytesCopied = 0;
uint64_t size = sizeEx.QuadPart;
/* copy file */
while (size != bytesCopied) {
uint64_t bytesLeft = size - bytesCopied;
nbytes = bytesLeft < sizeof(buff) ? bytesLeft : sizeof(buff);
if (!::ReadFile(hInFile, buff, nbytes, nullptr, nullptr)) {
/* read failed */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return false;
}
DWORD nWritten = 0;
if (!::WriteFile(hOutFile, buff, nbytes, &nWritten, nullptr)) {
/* write failed */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return false;
}
}
/* copy success */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return true;
}
打开文件后默认文件的读写指针从0开始,读写n后字节后当前读写指针偏移量会自动后移相应长度。Linux下如果想从指定偏移量读写文件可以用lseek
来改变文件读写指针的位置,在Windows下这个函数是SetFilePointer
相关参考资料:
ReadFile function (fileapi.h)
WriteFile function (fileapi.h)
SetFilePointer function (fileapi.h)