由于所做的课题需要得到电脑中的所有文件,并且需要像everything一样能对文件进行搜索归类,所以我就去研究了一下everything的原理,其中,everything是通过读取usn文件来获取整个盘符的所有文件的,今天来整理一下它是怎么实现的。(以下代码只做逻辑示例,所以不会是优美的代码。。。)
USN日志(USN Journal)是NTFS的一个特性,全称更新序列号码日志(UpdateSequenceNumberJournal),或称更改日志(Change Journal),相当于NTFS的秘书,它会记录下NTFS盘符内一切的改动,并储存为USN_RECORD的格式。
整个实现过程一共分为以下五步:
一、判断盘符是否为NTFS格式
前面说了,USN日志是NTFS的一个特性,所以这也展示了它的局限性-----只有NTFS下才能使用。我们可以通过函数GetVolumeInformation()获取相关的信息进行判断:
使用可参考:https://docs.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa
BOOL
可以看出,在上面的函数中,倒数第二项参数代表的就是盘符的格式类型,所以,我们可以通过使用这个函数的lpFileSystemNameBuffer参数来判断是否为NTFS格式,下面是代码示例:
char Root[] = "D:";
//判断驱动盘是否是NTFS格式
char sysNameBuf[MAX_PATH]={0};
int status= GetVolumeInformation(Root,
NULL, // 驱动盘名缓冲,这里我们不需要
0,
NULL,
NULL,
NULL,
sysNameBuf, // 驱动盘的系统名( FAT/NTFS)
MAX_PATH);
printf(" 文件系统名 : %sn" , Root);
if(0==strcmp(sysNameBuf,"NTFS")){
printf(" 该驱动盘是NTFS 格式 n" );
}
else{
printf(" 该驱动盘非 NTFS 格式 n" );
}
二、获取盘符句柄
获取盘符句柄使用CreateFileA函数,该函数创建或打开文件或I / O设备。最常用的I / O设备有:文件,文件流,目录,物理磁盘,卷,控制台缓冲区,磁带驱动器,通信资源,邮件槽和管道。该函数返回一个句柄,可用于根据文件或设备以及指定的标志和属性访问各种类型的I / O的文件或设备。
使用可参考:https://docs.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-createfilea
HANDLE CreateFileA(
LPCSTR lpFileName, //要创建或打开的文件或设备的名称
DWORD dwDesiredAccess, //请求访问文件或设备
DWORD dwShareMode, //请求的文件或设备共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向SECURITY_ATTRIBUTES 结构的指针,可以为NULL。
DWORD dwCreationDisposition, //对存在或不存在的文件或设备执行的操作。对文件以外的设备,此参数通常设置为OPEN_EXISTING。
DWORD dwFlagsAndAttributes, //文件或设备属性和标志,FILE_ATTRIBUTE_NORMAL是文件的最常见默认值。
HANDLE hTemplateFile //具有GENERIC_READ访问权限的模板文件的有效句柄,可以为NULL。
);
通过使用CreateFileA()函数就可以返回盘符句柄,下面是参考代码:
char fileName[MAX_PATH];
fileName[0] = '0';
// 传入的文件名必须为.C:的形式
strcpy_s(fileName, ".");
//char Root[] = "C:";
strcat_s(fileName, Root);
// 为了方便操作,这里转为string进行去尾
string fileNameStr = (string)fileName;
fileNameStr.erase(fileNameStr.find_last_of(":")+1);
printf("驱动盘地址: %sn", fileNameStr.data());
// 调用该函数需要管理员权限
HANDLE hVol = CreateFileA(fileNameStr.data(),
GENERIC_READ | GENERIC_WRITE, // 可以为0
FILE_SHARE_READ | FILE_SHARE_WRITE, // 必须包含有FILE_SHARE_WRITE
NULL, // 这里不需要
OPEN_EXISTING, // 必须包含OPEN_EXISTING, CREATE_ALWAYS可能会导致错误
FILE_ATTRIBUTE_READONLY, // FILE_ATTRIBUTE_NORMAL可能会导致错误
NULL); // 这里不需要
if(INVALID_HANDLE_VALUE!=hVol){
printf("获取驱动盘句柄成功n ");
}else{
printf("获取驱动盘句柄失败 —— handle:%x error:%dn", hVol, GetLastError());
}
三、初始化USN文件
USN Journal并非一开始就存在的,需要手动打开。我们可以使用函数DeviceIoControl()并通过参数FSCTL_CREATE_USN_JOURNAL来操作。
使用可参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winioctl/ni-winioctl-fsctl_create_usn_journal
DeviceIoControl()函数:
BOOL DeviceIoControl( //-----------操作完成,返回非零值
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
);
DeviceIoControl()函数通过参数FSCTL_CREATE_USN_JOURNAL来初始化USN文件:
BOOL
WINAPI
DeviceIoControl( (HANDLE) hDevice, // handle to volume
FSCTL_CREATE_USN_JOURNAL, // dwIoControlCode
(LPVOID) lpInBuffer, // input buffer
(DWORD) nInBufferSize, // size of input buffer
NULL, // lpOutBuffer
0, // nOutBufferSize
(LPDWORD) lpBytesReturned, // number of bytes returned
(LPOVERLAPPED) lpOverlapped ); // OVERLAPPED structure
下面是参考代码:
DWORD br;
CREATE_USN_JOURNAL_DATA cujd;
cujd.MaximumSize = 0; // 0表示使用默认值
cujd.AllocationDelta = 0; // 0表示使用默认值
status = DeviceIoControl(hVol,
FSCTL_CREATE_USN_JOURNAL,
&cujd,
sizeof(cujd),
NULL,
0,
&br,
NULL);
if(0!=status){
printf("初始化USN日志文件成功n");
}else{
printf("初始化USN日志文件失败 —— status:%x error:%dn", status, GetLastError());
}
四、获取USN日志基本信息(用于后续操作)
使用DeviceIoControl()函数通过参数FSCTL_QUERY_USN_JOURNAL来查询有关当前更新序列号(USN)的信息更改日志,其记录及其容量:
参考使用:https://docs.microsoft.com/zh-cn/windows/win32/api/winioctl/ni-winioctl-fsctl_query_usn_journal
BOOL
WINAPI
DeviceIoControl( (HANDLE) Device, // handle to volume
(DWORD) FSCTL_QUERY_USN_JOURNAL,// dwIoControlCode(LPVOID)
NULL, // lpInBuffer(DWORD)
0, // nInBufferSize(LPVOID)
lpOutBuffer, // output buffer
(DWORD) nOutBufferSize, // size of output buffer
(LPDWORD) lpBytesReturned, // number of bytes returned
(LPOVERLAPPED) lpOverlapped ); // OVERLAPPED structure
其中,输出缓冲区得到USN日志的基本信息,有以下结构:
typedef struct {
DWORDLONG UsnJournalID;
USN FirstUsn;
USN NextUsn;
USN LowestValidUsn;
USN MaxUsn;
DWORDLONG MaximumSize;
DWORDLONG AllocationDelta;
} USN_JOURNAL_DATA, *PUSN_JOURNAL_DATA;
下面是参考代码:
bool getBasicInfoSuccess = false;
USN_JOURNAL_DATA UsnInfo;
status = DeviceIoControl(hVol,
FSCTL_QUERY_USN_JOURNAL,
NULL,
0,
&UsnInfo,
sizeof(UsnInfo),
&br,
NULL);
if(0!=status){
printf("获取USN日志基本信息成功n");
}else{
printf("获取USN日志基本信息失败 —— status:%x error:%dn", status, GetLastError());
}
五、列出USN文件数据
使用DeviceIoControl()函数通过参数FSCTL_ENUM_USN_DATA来枚举两个指定边界之间的更新序列号(USN)数据,以获取主文件表(MFT)记录。每次调用FSCTL_ENUM_USN_DATA都会将后续调用的起始点检索为输出缓冲区中的第一个条目。
使用参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winioctl/ni-winioctl-fsctl_enum_usn_data
BOOL
WINAPI
DeviceIoControl( (HANDLE) hDevice, // handle to volume
(DWORD) FSCTL_ENUM_USN_DATA, // dwIoControlCode
(LPVOID) lpInBuffer, // input buffer
(DWORD) nInBufferSize, // size of input buffer
(LPVOID) lpOutBuffer, // output buffer
(DWORD) nOutBufferSize, // size of output buffer
(LPDWORD) lpBytesReturned, // number of bytes returned
(LPOVERLAPPED) lpOverlapped ); // OVERLAPPED structure);
通过步骤四可以得到USN_JOURNAL_DATA UsnInfo,通过UsnInfo可以得到USN的指定边界;最后把输出缓冲区的USN文件数据传到USN_RECORD,USN_RECORD有以下结构:
typedef struct {
DWORD RecordLength;
WORD MajorVersion;
WORD MinorVersion;
DWORDLONG FileReferenceNumber;
DWORDLONG ParentFileReferenceNumber;
USN Usn;
LARGE_INTEGER TimeStamp;
DWORD Reason;
DWORD SourceInfo;
DWORD SecurityId;
DWORD FileAttributes;
WORD FileNameLength;
WORD FileNameOffset;
WCHAR FileName[1];
} USN_RECORD, *PUSN_RECORD;
这里需要提供一个MTF_ENUM_DATA的结构作为参数,USN_RECORD会储存在MFT的表里,MTF_ENUM_DATA的作用就是制定一个范围。MTF_ENUM_DATA有以下结构:
typedef struct {
DWORDLONG StartFileReferenceNumber;
USN LowUsn;
USN HighUsn;
} MFT_ENUM_DATA, *PMFT_ENUM_DATA;
下面是参考代码:
MFT_ENUM_DATA med;
med.StartFileReferenceNumber = 0;
med.LowUsn = 0;//UsnInfo.FirstUsn;
med.HighUsn = UsnInfo.NextUsn;
CHAR buffer[BUF_LEN]; // 用于储存记录的缓冲 , 尽量足够地大
DWORD usnDataSize;
PUSN_RECORD UsnRecord;
while (0!=DeviceIoControl(hVol,
FSCTL_ENUM_USN_DATA,
&med,
sizeof (med),
buffer,
BUF_LEN,
&usnDataSize,
NULL))
{
DWORD dwRetBytes = usnDataSize - sizeof (USN);
UsnRecord = (PUSN_RECORD)(((PCHAR)buffer)+sizeof (USN));
//printf(" ********************************** n" );
while (dwRetBytes>0){
// 打印获取到的信息
const int strLen = UsnRecord->FileNameLength;
char fileName[MAX_PATH] = {0};
WideCharToMultiByte(CP_OEMCP,NULL,UsnRecord->FileName,strLen/2,fileName,strLen,NULL,FALSE);
printf("FileName: %sn" , fileName);
// 下面两个 filereferencenumber 可以用来获取文件的路径信息
printf("FileReferenceNumber: %xI64n" , UsnRecord->FileReferenceNumber);
printf("ParentFileReferenceNumber: %xI64n" , UsnRecord->ParentFileReferenceNumber);
printf("n" );
char *pathBuffer[BUF_LEN];
// 获取下一个记录
DWORD recordLen = UsnRecord->RecordLength;
dwRetBytes -= recordLen;
UsnRecord = (PUSN_RECORD)(((PCHAR)UsnRecord)+recordLen);
}
med.StartFileReferenceNumber = *(USN *)&buffer;
}
通过以上五步,就可以获得特定的盘符内的所有文件了。以上代码,输出了所有文件的文件名(FileName),文件引用号(FileReferenceNumber)以及父文件引用号(ParentFileReferenceNumber),接下来,我们就可以通过他们来得到文件的路径了。
得到文件路径:
首先,通过以上代码输出,我们可以得到,该文件的父文件引用号(ParentFileReferenceNumber)等于该文件的父文件的文件引用号(FileReferenceNumber),打个比方:
![28c789d3d1580e224baf0eea2422b599.png](https://img-blog.csdnimg.cn/img_convert/28c789d3d1580e224baf0eea2422b599.png)
如图,第一章.pptx和成绩表.xlsx的父文件引用号(ParentFileReferenceNumber)为12,而数学文件夹的文件引用号(FileReferenceNumber)也为12,所以第一章.pptx和成绩表.xlsx是在数学文件夹内的;而数学文件夹的父文件引用号(ParentFileReferenceNumber)为11,等于科目文件夹的文件引用号(FileReferenceNumber),所以,数学文件夹就在科目文件夹内。
按照以上这个规律,我们就能知道各个文件的路径了。
我的具体实现方法:
首先,我们要先知道所有文件的数据,把它存储起来(可以用map<key,value>,其中key值为FileReferenceNumber,value为文件信息对象);
然后遍历map,对于一个文件,不断根据ParentFileReferenceNumber向上找其父文件;
最后路径等于父文件路径+“”+当前文件名。
下面是一个单独的小例子:
FrnFilePath.cpp
#include<iostream>
#include<string>
using namespace std;
class FrnFilePath{
public :
string filename;
string frn;
string pfrn;
string path;
FrnFilePath(string filename,string frn,string pfrn,string path){
this->filename=filename;
this->frn=frn;
this->pfrn=pfrn;
this->path=path;
}
FrnFilePath(){}
};
fileInfo.cpp
//定义一个类,包含文件名,frn,pfrn
#include <string>
using namespace std;
class fileInfo{
public:
string filename;
string frn;
string pfrn;
fileInfo(string filename,string frn,string pfrn){
this->filename=filename;
this->frn=frn;
this->pfrn=pfrn;
}
};
main.cpp
#include<iostream>
#include <Windows.h>
#include <string>
#include<list>
#include <algorithm>
#include<map>
#include<stack>
#include"fileInfo.cpp"
#include"FrnFilePath.cpp"
using namespace std;
#define BUF_LEN 4096
list<fileInfo> result;
map <string,FrnFilePath> fileHash;
void fun3(fileInfo &fi){
FrnFilePath *ff=new FrnFilePath(fi.filename,fi.frn,fi.pfrn, "");
fileHash.insert(std::make_pair(fi.frn,*ff));
}
void clear(stack<string> treestack){
while(!treestack.empty())
{
treestack.pop();
}
}
static string BuildPath(FrnFilePath currentNode, FrnFilePath parentNode) //组合路径
{
return parentNode.path+""+currentNode.filename;
}
int main(){
string drivefrn="9";
FrnFilePath *ff=new FrnFilePath("D:",drivefrn,"","D:");
fileHash.insert(std::make_pair(drivefrn,*ff));
fileInfo *fi=new fileInfo("a","1","2");
result.push_back(*fi);
fi=new fileInfo("b","2","4");
result.push_back(*fi);
fi=new fileInfo("c","5","3");
result.push_back(*fi);
fi=new fileInfo("d","4","6");
result.push_back(*fi);
fi=new fileInfo("e","3","9");
result.push_back(*fi);
fi=new fileInfo("f","6","9");
result.push_back(*fi);
for_each(result.begin(), result.end(), fun3);
stack<string> treestack;
map<string,FrnFilePath>::iterator iter;
iter = fileHash.begin();
while(iter != fileHash.end()){
clear(treestack);
FrnFilePath currentValue = iter->second;
FrnFilePath *parent;
if ((currentValue.path.empty())&& !currentValue.pfrn.empty()&&fileHash.find(currentValue.pfrn)!=fileHash.end()){
FrnFilePath parentValue = fileHash[currentValue.pfrn];
while (parentValue.path.empty()&&!parentValue.pfrn.empty()&&fileHash.find(parentValue.pfrn)!=fileHash.end()){
FrnFilePath temp=currentValue;
currentValue = parentValue;
if (!currentValue.pfrn.empty()&&fileHash.find(currentValue.pfrn)!=fileHash.end()){
treestack.push(temp.frn);
parentValue = fileHash[currentValue.pfrn];
}
else{
parent=&parentValue;
parent=NULL;
break;
}
}
if (parent != NULL){
currentValue.path = BuildPath(currentValue, parentValue);
while (!treestack.empty()){
string walkedKey = treestack.top();
treestack.pop();
FrnFilePath walkedNode = fileHash[walkedKey];
FrnFilePath parentNode = fileHash[walkedNode.pfrn];
walkedNode.path = BuildPath(walkedNode, parentNode);
currentValue.path+=walkedNode.path;
}
iter->second.path=currentValue.path;
}
}
iter++;
}
iter = fileHash.begin();
while(iter != fileHash.end()){
cout<<iter->second.filename<<" "<<iter->second.path<<endl;
iter++;
}
}
输出为:
![1fd999237bd3b2af28d09f75b89c78d1.png](https://img-blog.csdnimg.cn/img_convert/1fd999237bd3b2af28d09f75b89c78d1.png)