首先获取文件部分实现基本分为6步:

1.       判断驱动盘是否为 NTFS 格式

2.       获取驱动盘句柄

3.       初始化 USN 日志文件

4.       获取 USN 基本信息

5.       列出 USN 日志的所有数据

6.       删除 USN 日志文件

百度很久出来的大佬写的部分:

​https://blog.csdn.net/xexiyong/article/details/16903389?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-16903389-blog-52369406.pc_relevant_aa&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-16903389-blog-52369406.pc_relevant_aa&utm_relevant_index=6​

下面为本人实现过程,基本测试都可以通过

1.判断是否为NTFS格式

我们需要用到GetVolumeInformationA函数

属性如下:

我们只需要用到倒数第二个

GetVolumeInformation(
lpRootPathName: PChar; { 磁盘驱动器代码字符串}
lpVolumeNameBuffer: PChar; { 磁盘驱动器卷标名称}
nVolumeNameSize: DWORD; { 磁盘驱动器卷标名称长度}
lpVolumeSerialNumber: PDWORD; { 磁盘驱动器卷标序列号}
var lpMaximumComponentLength : DWORD; { 系统允许的最大文件名长度}
var lpFileSystemFlags : DWORD; { 文件系统标识}
lpFileSystemNameBuffer: PChar; { 文件操作系统名称}
nFileSystemNameSize: DWORD{ 文件操作系统名称长度 }
) : BOOL;

如果检索所有请求的信息,则返回值为非零。如果未检索所有请求的信息,则返回值为零。

//1.判读是否为NTFS盘
bool isNTFS = false;
char sysNameBuf[MAX_PATH] = { 0 };//接收缓冲区
int status = GetVolumeInformationA(volName, //文件路径
NULL, //下面的我们都不需要获取
0, //
NULL, //
NULL, //
NULL, //
sysNameBuf, //文件系统名称 是我们获取ntfs需要的
MAX_PATH); //名称长度
printf(" 文件系统名 : %s\n", volName);
//if (0 == status)
//{
if (0 == strcmp(sysNameBuf, "NTFS")) {
cout << "磁盘为NTFS格式" << endl;
isNTFS = true;
}
else
cout << "此盘非NTFS格式" << endl;
// }

2.获取驱动盘句柄

我们需要用到CreateFileA函数,该函数返回一个句柄。可用于根据文件或设备以及指定的标志和属性访问各种类型的I/O文件或设备

HANDLE CreateFile(
LPCTSTR lpFileName, // 指向文件名的指针
DWORD dwDesiredAccess, // 访问模式(写 / 读) 如果为 GENERIC_READ 表示允许对设备进行读访问;如果为 GENERIC_WRITE 表示允许对设备进行写访问(可组合使用);如果为零,表示只允许获取与一个设备有关的信息
DWORD dwShareMode, // 共享模式 零表示不共享; FILE_SHARE_READ 和 / 或 FILE_SHARE_WRITE 表示允许对文件进行共享访问
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性的指针
DWORD dwCreationDisposition, // 如何创建
DWORD dwFlagsAndAttributes, // 文件属性
HANDLE hTemplateFile // 用于复制文件句柄
);
//2.获取驱动盘句柄
char fileName[MAX_PATH];
fileName[0] = '\0';//感觉没什么用 下面strcpy_s都被替换了
bool getHandleSucces = false;
// 传入的文件名必须为\\.\C:的形式
strcpy_s(fileName, "\\\\.\\");
strcat_s(fileName, volName);

string fileNameStr = (string)fileName;
//删除c:后面不需要的部分
fileNameStr.erase(fileNameStr.find_last_of(":") + 1);
cout << "驱动盘地址:" << fileNameStr << endl;

//函数用管理员身份打开才能成功,写完先生成文件后在文件夹debug找到.exe以管理员身份运行
HANDLE handl = CreateFileA(fileNameStr.data(), //需要打开文件的名字
GENERIC_READ | GENERIC_WRITE, //允许对设备进行读写
FILE_SHARE_READ | FILE_SHARE_WRITE, //允许对文件进行共享访问
NULL, //使用默认安全属性
OPEN_EXISTING, //文件必须已经存在,有设备提出要求
FILE_ATTRIBUTE_READONLY, //文件属性只读
NULL); //不复制句柄

if (INVALID_HANDLE_VALUE != handl)
{
printf("获取句柄成功\n");
getHandleSucces = true;
}
else
cout << "获取驱动盘失败" << endl;

不用管理员身份运行的情况下都是显示失败,用管理员身份后就显示成功了

Everything尝试实现(没写完)_#include


3.初始化 USN 日志文件

需要使用函数DeviceIoControl,USN Journal需要手动打开

DeviceIoControl的使用说明​​:https://blog.51cto.com/u_15525394/5893321​

使用dwIoControlCode中的FSCTL_CREATE_USN_JOURNAL参数

BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice, //执行操作的设备句柄
_In_ DWORD dwIoControlCode, //操作的控制代码
_In_opt_ LPVOID lpInBuffer, //指向输入缓冲区的指针,数据格式取决于dwloControlCode,若不需要输入数据可为NULL
_In_ DWORD nInBufferSize, //输入缓冲区以字节为单位的大小
_Out_opt_ LPVOID lpOutBuffer, //指向输出缓冲区的指针同IpInbuffer
_In_ DWORD nOutBufferSize, //输出缓冲区以字节为单位大小,单位字节
_Out_opt_ LPDWORD lpBytesReturned, //指向一个指针变量的指针,接收存储在输出缓冲区的数据大小,如果输出缓冲区太小无法接收数据为0
_Inout_opt_ LPOVERLAPPED lpOverlapped //指向OVERLAPPED结构的指针
);
3.初始化USN日志文件
bool initUsnJournalSuccess = false;
DWORD br;
CREATE_USN_JOURNAL_DATA cujd;
cujd.MaximumSize = 0;//使用默认值
cujd.AllocationDelta = 0;//使用默认值
status = DeviceIoControl(handl, //句柄
FSCTL_CREATE_USN_JOURNAL, //打开日志
&cujd, //指向输入缓冲区的指针
sizeof(cujd), //数据大小
NULL, //输出缓冲区指针
0, //输出缓存区大小
&br, //指向接收缓冲区指针
NULL); //

if (0 != status)
{
cout << "初始化UNSN日志成功" << endl;
initUsnJournalSuccess = true;
}
else
cout << "初始化UNSN日志失败" << endl;

Everything尝试实现(没写完)_句柄_02


4. 获取 USN 基本信息

同样需要用到DeviceIoControl函数中的FSCTL_QUERY_USN_JOURNAL参数。来查询有关当前更新序列号的信息更改日志

还需要这个结构USN_JOURNAL_DATA,微软官方解释:

​​​https://learn.microsoft.com/zh-cn/windows/win32/api/winioctl/ns-winioctl-usn_journal_data_v1​

typedef struct {
DWORDLONG UsnJournalID;
USN FirstUsn;
USN NextUsn;
USN LowestValidUsn;
USN MaxUsn;
DWORDLONG MaximumSize;
DWORDLONG AllocationDelta;
WORD MinSupportedMajorVersion;
WORD MaxSupportedMajorVersion;
DWORD Flags;
DWORDLONG RangeTrackChunkSize;
LONGLONG RangeTrackFileSizeThreshold;
} USN_JOURNAL_DATA_V2, *PUSN_JOURNAL_DATA_V2;
4.获取日志操作
bool getBasicInfoSuccess = false;
USN_JOURNAL_DATA UsnInfo;
status = DeviceIoControl(handl,
FSCTL_QUERY_USN_JOURNAL,
NULL, 0,
&UsnInfo,
sizeof(UsnInfo),
&br,
NULL);
if (0 != status)
{
cout << "获取日志成功" << endl;
getBasicInfoSuccess = true;
}
else
{
cout << "获取USN日志基本信息失败 —— status:" << status << " error:" << GetLastError() << endl;
}

Everything尝试实现(没写完)_#include_03

5.列出 USN 日志的所有数据(这边暂时有问题,有WideCharToMultiByte参数暂时有堆栈的问题)

还是需要使用 DeviceIoControl函数,使用其中的FSCTL_ENUM_USN_DATA参数来枚举两个指定边界之间的更新序列号数据,以获取MFT记录。

其中的FirstUsn 和NextUsn 就分别对应了LowUsn 和HighUsn.

typedef struct {
DWORDLONG StartFileReferenceNumber;
USN LowUsn;
USN HighUsn;
WORD MinMajorVersion;
WORD MaxMajorVersion;
} MFT_ENUM_DATA_V1, *PMFT_ENUM_DATA_V1;

WideCharToMultiByte参数:

​https://www.cnblogs.com/vranger/p/3793123.html​​​

int WideCharToMultiByte( 
UINT CodePage, //指定执行转换的代码页,这个参数可以为系统已安装或有效的任何代码页所给定的值
DWORD dwFlags, //指定如何处理没有转换的字符,但不设此参数函数会运行的更快一些,我都是把它设为0。
LPWSTR lpWideCharStr, //待转换的宽字符串。
int cchWideChar, //待转换宽字符串的长度,-1表示转换到字符串结尾。
LPCSTR lpMultiByteStr, //接收转换后输出新串的缓冲区。
int cchMultiByte, //输出缓冲区大小,如果为0,lpMultiByteStr将被忽略,函数将返回所需缓冲区大小而不使用lpMultiByteStr。
LPCSTR lpDefaultChar, //指向字符的指针, 在指定编码里找不到相应字符时使用此字符作为默认字符代替。如果为NULL则使用系统默认字符。对于要求此参数为NULL的dwFlags而使用此参数,函数将失败返回并设置错误码ERROR_INVALID_PARAMETER。
PBOOL pfUsedDefaultChar 开关变量的指针,用以表明是否使用过默认字符。
);
5.列出usn文件
MFT_ENUM_DATA med;
med.StartFileReferenceNumber = 0;
med.LowUsn = 0;//对应UsnInfo的first参数
med.HighUsn = UsnInfo.NextUsn;//对应UsnInfo中的NEXT参数
char buffer[BUF_LEN];//存储记录缓冲
DWORD usnDataSize;
PUSN_RECORD UsnRecord;
while (0 != DeviceIoControl(handl,
FSCTL_ENUM_USN_DATA,
&med,
sizeof(med),
buffer,
BUF_LEN ,
&usnDataSize,
NULL));
{
DWORD dwRetBytes = usnDataSize - sizeof(USN);
UsnRecord = (PUSN_RECORD)(((PCHAR)buffer) + sizeof(USN));

while (dwRetBytes > 0)
{
//打印获取到的信息
const int strLen = UsnRecord->FileNameLength;
char fileName[MAX_PATH] = { 0 };
WideCharToMultiByte(CP_OEMCP,//当前系统OEM代码页,一种原始设备制造商硬件扫描码
NULL,//一般为0,运行效率更快
UsnRecord->FileName,//代转换的宽字符串
strLen / 2,//代转换字符串长度
fileName,//接收转换后输出新串的缓冲区
strLen,//输出缓冲区大小
NULL,//系统默认字符
false);//开关变量的指针
printf("FileName:%s\n", fileName);
//获取文件路径信息
printf("FileReferenceNumber:%xI64\n", UsnRecord->FileReferenceNumber);
printf("ParentFileReferenceNumber:%xI64\n", UsnRecord->ParentFileReferenceNumber);
printf("\n");
char* pathBuffer[BUF_LEN];

//获取下一个记录
DWORD recordLen = UsnRecord->RecordLength;
dwRetBytes -= recordLen;
UsnRecord = (PUSN_RECORD)(((PCHAR)UsnRecord) + recordLen);
}
med.StartFileReferenceNumber = *(USN*)&buffer;
}


首先,通过以上代码输出,我们可以得到,该文件的父文件引用号(ParentFileReferenceNumber)等于该文件的父文件的文件引用号(FileReferenceNumber),类似于linux中的父子进程。

我们要先知道所有文件的数据,把它存储起来(可以用map<key,value>,其中key值为FileReferenceNumber,value为文件信息对象),然后遍历map,对于一个文件,不断根据ParentFileReferenceNumber向上找其父文件;

最后路径等于父文件路径+“\\”+当前文件名。

fileInfo.cpp

#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;
}
};

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->path = path;
this->pfrn = pfrn;
}
FrnFilePath(){}
};

主函数:

#include"fileInfo.cpp"
#include"FrnFilePath.cpp"
#include<list>
#include<map>
#include<stack>


list<fileInfo> result;
map <string, FrnFilePath> fileHash;

//把测试值插入map
void fun(fileInfo& fi)
{
FrnFilePath* ff = new FrnFilePath(fi.filename, fi.frn, fi.pfrn,"");
fileHash.insert(make_pair(fi.frn, *ff));
}

//
void clear(stack<string> treestack)
{
while (!treestack.empty())
treestack.pop();
}
//把路径组合起来
static string BuildPath(string currentNode, string parentNode)
{
return parentNode + "\\" + currentNode;
}


int main()
{
//先插入磁盘"D:" 当前为9,无父目录,路径"D:"
string drivefrn = "9";
FrnFilePath* ff = new FrnFilePath("D:", drivefrn, "", "D:");
fileHash.insert(make_pair(drivefrn, *ff));

//插入a,b,c,d,e,f测试数据
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);
//把a,b,c,d,e,f都插入map中
for_each(result.begin(), result.end(), fun);

//需要一个栈来存储暂时用不到的子目录
stack<string> treestack;

auto iter = fileHash.begin();
//把map都遍历一遍
while (iter != fileHash.end())
{
//每次开始都清空栈以防万一
clear(treestack);
//当前位置
FrnFilePath current = iter->second;
//如果当前位置路径为空,且有父目录就进去
if (current.path.empty() && !current.pfrn.empty())
{
//取到父目录信息
FrnFilePath parent = fileHash[current.pfrn];
//如果父目录路径为空,且父目录还有父目录
while (parent.path.empty() && !parent.pfrn.empty())
{
//定义零时变量,把当前目录变成父目录
FrnFilePath temp = current;
current = parent;
//如果当前目录路径为空,且有目录
if (current.path.empty() && !current.pfrn.empty())
{
//把第存储的子目录插入栈中,然后把父目录变成改变过的当前目录的父目录
treestack.push(temp.frn);
parent = fileHash[current.pfrn];
}
}
//当前目录路径名
current.path = BuildPath(current.filename, parent.path);
//如果栈不为空就一直继续
while (!treestack.empty())
{
//取头去头
string head = treestack.top();
treestack.pop();
FrnFilePath headNode = fileHash[head];
//拼接目录
current.path = BuildPath(headNode.filename, current.path);
}
iter->second.path = current.path;
}
iter++;
}

iter = fileHash.begin();
while (iter != fileHash.end())
{
cout << iter->second.filename << " " << iter->second.path << endl;
iter++;
}

}


Everything尝试实现(没写完)_#include_04