频道分类

delphi NTFS USN 磁盘文件搜索

作者:admin 来源: 日期:2020/3/10 23:37:32 人气: 标签:

 
  USN是Update Service Number Journal or Change Journal的英文缩写,直译为“更新序列号”,是对NTFS卷里所修改过的信息进行相关记录的功能。当年微软发布Windows 2000时,建立NTFS 5.0的同时,加入了一些新功能和改进了旧版本的文件系统,为它请来了一位可靠的秘书,它可以在分区中设置监视更改的文件和目录的数量,记录下监视对象修改时间和修改内容。没错,它就是USN日志。当这个功能启用时,对于每一个NTFS卷,当发生有关添加、删除和修改文件的信息时,NTFS都使用USN日志记录下来。


  真高效!探访秘书的工作

  NTFS秘书——USN日志的工作方式,相对来说很简单,所以非常的高效。它开始的时候是一个空文件,包括NTFS每个卷的信息。每当NTFS卷有改变的时候,所改变的信息会马上被添加到这个文件里。这其中,每条修改的记录都使用特定符号来标识为日志形式,也就是USN日志。每条日志,记录了包括文件名、文件信息做出的改变。怎样在系统中让秘书开始干活儿呢?如图2所示,在NTFS分区的图标上右击选择“属性”,勾选圈中部分即可。


  忠诚的秘书只为NTFS效劳

  USN秘书不仅工作高效,而且非常的忠诚,虽然这种忠诚看起来有点迫不得已。日志里包括发生了什么变化(添加、删除或其他操作),但并不会记录数据或其他变化的细节,所以它只能工作在NTFS文件系统中。


  看到上面的描述,你也许还是比较难以理解,那么就举个例子说明一下。USN日志为什么不能在FAT32文件系统下运用呢?就像钢笔不能在宣纸上记录,只能在普通纸上记录一样。USN日志相当于一本书的索引,当然书里面内容发生添加、修改或删除的时候,USN日志会记录下来何时做了修改,并使用特定序列号来标识,但它并不会记录里面具体修改了什么东西,所以索引文件很小。而当你想查找某一篇文章时,你就不用一页一页去翻书,可以直接通过查找USN日志(也就是建立的索引)就知道这篇文章是否存在。


我在第一次使用 Everything 时,对其速度确实感到惊讶,后来了解到是通过操作 USN 实现的,并且有一定的局限性(只有 NTFS 下才能使用)。USN的使用(Everything的快不单是用了USN,还需要建立索引)

整个实现分为 6 步:
1.       判断驱动盘是否为 NTFS 格式
2.       获取驱动盘句柄
3.       初始化 USN 日志文件
4.       获取 USN 基本信息
5.       列出 USN 日志的所有数据
6.       删除 USN 日志文件


对于 NTFS 磁盘文件,可以通过遍历USN日志来搜索,可以非常快查找文件,上图为我的电脑  160多万 文件,大概为 10秒

以下代码在 Delphi 10.3 通过, 有兴趣的同学们 可以直接复制 使用。

注意:请使用管理员权限 运行

主程序调用

procedure TForm1.Button1Click(Sender: TObject);
var
  Strs : TStringList;
begin

  Strs:= TStringList.Create;
  Strs.BeginUpdate;
  SearchNTFS('D', Strs);
  Strs.SaveToFile(ExtractFilePath(ParamStr(0))+ ' D盘文件.txt', TEncoding.UTF8);
  Strs.EndUpdate;
  Strs.Free;

  Showmessage('搜索完成');
end;









/****************************************************

  wwww.DelphiFmx.com   赵客,    转载请注明出处

****************************************************/

unit uSearchNTFS;

interface


uses
  System.Classes;

function SearchNTFS(const ADiskName: WideChar;  AFiles: TStrings):Boolean;


implementation

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Generics.Collections, Vcl.Dialogs;

/***************  wwww.DelphiFmx.com   赵客 ***************/

type
  _PARTITION_INFORMATION = record
    StartingOffset :LARGE_INTEGER;// 指定分区开始的驱动器上的字节偏移量
    PartitionLength:LARGE_INTEGER;// 指定分区的长度(以字节为单位)
    HiddenSectors: Cardinal;      // 指定隐藏扇区的数量
    PartitionNumber: Cardinal;    // 指定分区号
    PartitionType: Byte;          // 分区类型
    BootIndicator: Boolean;       // TRUE时,指示此分区是该设备的可引导(活动)分区。为FALSE时,该分区不可引导
    RecognizedPartition: Boolean; // TRUE时,表示系统识别分区的类型。为FALSE时,系统无法识别分区的类型
    RewritePartition: Boolean;    // TRUE时,表明分区信息已更改。为FALSE时,分区信息未更改
  end;
  PARTITION_INFORMATION  =  _PARTITION_INFORMATION;
  PPARTITION_INFORMATION = ^_PARTITION_INFORMATION;

  // 单条USN记录结构信息结构
  _USN_RECORD = record
    RecordLength: DWORD;                 // 该条USN记录长度
    MajorVersion: WORD;                  // 主版本
    MinorVersion: WORD;                  // 次版本
    FileReferenceNumber: DWORDLONG;      // 文件引用数
    ParentFileReferenceNumber: DWORDLONG;// 父文件引用数
    Usn: Int64;                          // USN(一般为int64类型)
    TimeStamp: LARGE_INTEGER;            // 时间戳
    Reason: DWORD;                       // 原因
    SourceInfo: DWORD;                   // 源信息
    SecurityId: DWORD;                   // 安全
    FileAttributes: DWORD;               // 文件属性(文件或目录)
    FileNameLength: WORD;                // 文件名长度
    FileNameOffset: WORD;                // 文件名偏移量
    FileName: PWideChar;                 // 文件名第一位的指针
  end;
  USN_RECORD = _USN_RECORD;
  PUSN_RECORD=^_USN_RECORD;

  // USN日志信息结构
  _USN_JOURNAL_DATA=  record
    UsnJournalID: DWORDLONG;   // USN日志ID
    FirstUsn: Int64;           // 第一条USN记录的位置
    NextUsn: Int64;            // 下一条USN记录将要写入的位置
    LowestValidUsn: Int64;     // 最小的有效的USN(FistUSN小于该值)
    MaxUsn: Int64;             // USN最大值
    MaximumSize: DWORDLONG;    // USN日志最大大小(按Byte算)
    AllocationDelta: DWORDLONG;// USN日志每次创建和释放的内存字节数
  end;
  USN_JOURNAL_DATA  = _USN_JOURNAL_DATA;
  PUSN_JOURNAL_DATA =^_USN_JOURNAL_DATA;

  // 创建USN日志的结构
  _CREATE_USN_JOURNAL_DATA = record
    MaximumSize: DWORDLONG;    // NTFS文件系统分配给USN日志的最大大小(字节)
    AllocationDelta: DWORDLONG;// USN日志每次创建和释放的内存字节数
  end;
  CREATE_USN_JOURNAL_DATA  =  _CREATE_USN_JOURNAL_DATA;
  PCREATE_USN_JOURNAL_DATA = ^_CREATE_USN_JOURNAL_DATA;

  // 删除USN日志的结构
  _DELETE_USN_JOURNAL_DATA = record
    UsnJournalID: DWORDLONG;// USN日志ID
    DeleteFlags: DWORD;     // 删除标志
  end;
  DELETE_USN_JOURNAL_DATA  =  _DELETE_USN_JOURNAL_DATA;
  PDELETE_USN_JOURNAL_DATA = ^_DELETE_USN_JOURNAL_DATA;

  // 遍历USN记录时的结构
  _MFT_ENUM_DATA = record
    StartFileReferenceNumber: DWORDLONG;// 开始文件引用数,第一次调用必须为0
    LowUsn: Int64;  // 最小USN,第一次调用,最好为0
    HighUsn: Int64; // 最大USN
  end;
  MFT_ENUM_DATA  =  _MFT_ENUM_DATA;
  PMFT_ENUM_DATA = ^_MFT_ENUM_DATA;

  // 获取USN日志变更的结构
  _READ_USN_JOURNAL_DATA= record
    StartUsn: Int64;           // 变更的USN记录开始位置,即第一次读取USN日志的LastUsn值。
    ReasonMask: DWORD;         // 原因标识
    ReturnOnlyOnClose: DWORD;  // 只有在记录关闭时才返回
    Timeout: DWORDLONG;        // 延迟时间
    BytesToWaitFor: DWORDLONG; // 当USN日志大小大于该值时返回
    UsnJournalID: DWORDLONG;   // USN日志ID
  end;
  READ_USN_JOURNAL_DATA  =  _READ_USN_JOURNAL_DATA;
  PREAD_USN_JOURNAL_DATA = ^_READ_USN_JOURNAL_DATA;

  // 文件定义
  _FileInfo = record
    FileName: String; // 文件名称
    FileId: UInt64;   // 文件的ID
    ParentId: UInt64; // 文件的父ID
  end;
  TFileInfo =  _FileInfo;
  PFileInfo = ^_FileInfo;


// TStringList 按数值排序函数
function MySort(List: TStringList; Index1, Index2: Integer): Integer;
var
  L, R: Int64;
begin
  L := PFileInfo(List.Objects[Index1])^.FileId;
  R := PFileInfo(List.Objects[Index2])^.FileId;
  if L < R then begin
    Result := -1
  end
  else if L = R then begin
    Result := 0
  end else begin
    Result := 1;
  end;
end;


// 获取文件全路径,包含路径和文件名
procedure GetFullFileName(const DiskName: WideChar; Files: TStringList);
var
  ArrU64: TArray;
  Idx     : Integer;
  ParentId: UInt64;
  Item: Integer;
begin
  // 将 FileList 按 FileReferenceNumber 数值排序
  Files.Sorted := False;
  Files.CustomSort(MySort);

  // 将排序好的 FileReferenceNumber 复制到 UInt64 数组列表中,便于下面进行快速查找
  SetLength(ArrU64, Files.Count);
  for Idx := 0 to Files.Count - 1 do begin
    ArrU64[Idx] := PFileInfo(Files.Objects[Idx])^.FileId;
  end;

  // 获取每一个文件全路径名称
  for Idx := 0 to Files.Count - 1 do begin
    ParentId := PFileInfo(Files.Objects[Idx])^.ParentId;
    while TArray.BinarySearch(ArrU64, ParentId, Item) do begin
      ParentId  := PFileInfo(Files.Objects[Item])^.ParentId;
      Files.Strings[Idx] := PFileInfo(Files.Objects[Item])^.FileName + '\' + Files.Strings[Idx];
    end;
    Files.Strings[Idx] := (DiskName + ':\' + Files.Strings[Idx]);
  end;
end;


function SearchNTFS(const ADiskName: WideChar;  AFiles: TStrings): Boolean;
const
  Len_Buf        = 440 * 1024; // 500
  PARTITION_IFS  = $07;
  USN_DeleteFlags= $00000001;
var
  lpMaxiLen, lpFileFlags: DWORD;
  Path: Array [0..MAX_PATH-1] of WideChar;

  ErrStr : String;

  hTmp : THandle;
  hFile: THandle;

  pInfo : PARTITION_INFORMATION;
  dwRet: DWORD;

  CUSN : CREATE_USN_JOURNAL_DATA; //创建
  USN  : USN_JOURNAL_DATA;
  DUSN : DELETE_USN_JOURNAL_DATA; //删除

  MEnum  : MFT_ENUM_DATA;
  Int64SZ: UInt;
  Buf : array[0 .. Len_Buf-1] of WideChar;
  UsnRecord: PUSN_RECORD;
  AFileName: String;

  pFile: PFileInfo;
  Idx: Integer;
begin
  Result := False;
  // 1.是否是NTFS磁盘格式
  GetVolumeInformation(PWideChar(ADiskName+':\') // 磁盘驱动器代码字符串
                      , nil        // 磁盘驱动器卷标名称
                      , 0          // 磁盘驱动器卷标名称长度
                      , nil        // 磁盘驱动器卷标序列号
                      , lpMaxiLen  // 系统允许的最大文件名长度
                      , lpFileFlags// 文件系统标识
                      , Path       // 文件操作系统名称
                      , MAX_PATH   // 文件操作系统名称长度
                      );

  if SameText(Path, 'NTFS') then begin
    // 2.打开卷 (需要管理员权限) 管理员权限启动方法 请参考  http://delphifmx.com/zh-hans/node/6 
    hTmp := 0;
    hFile:= Winapi.Windows.CreateFile(PWideChar('\\.\'+ ADiskName+':')
                                      , Generic_Read + Generic_Write      // 打开卷 操作方式(读或写)
                                      , File_Share_Read + File_Share_Write// 共享方式
                                      , nil
                                      , Open_Existing                     // 文件创建方法
                                      , File_Attribute_ReadOnly           // 文件属性
                                      , hTmp);
    if hFile = INVALID_HANDLE_VALUE then begin
      ErrStr := SysErrorMessage(GetLastError());
      Vcl.Dialogs.ShowMessage(ErrStr);
      Exit;
    end;
    // 3.初始化USN日志文件
    if not DeviceIoControl(hFile, FSCTL_CREATE_USN_JOURNAL, @CUSN, Sizeof(CUSN), nil, 0, dwRet, nil) then begin
      ErrStr := SysErrorMessage(GetLastError());
      Vcl.Dialogs.ShowMessage(ErrStr);
      Exit;
    end;
    // 4.获取USN日志基本信息
    if not DeviceIoControl(hFile, FSCTL_QUERY_USN_JOURNAL, nil, 0, @USN, Sizeof(USN), dwRet, nil) then begin
      ErrStr := SysErrorMessage(GetLastError());
      Vcl.Dialogs.ShowMessage(ErrStr);
      Exit;
    end;
    // 5.枚举USN日志文件中的所有记录
    MEnum.StartFileReferenceNumber := 0;
    MEnum.LowUsn  := 0;
    MEnum.HighUsn := USN.NextUsn;
    Int64SZ:= Sizeof(Int64);
    while DeviceIoControl(hFile, FSCTL_ENUM_USN_DATA, @MEnum, Sizeof(MEnum), @Buf, Len_Buf, dwRet, nil) do begin
      // 找到第一个 USN 记录
      UsnRecord := PUSN_RECORD( Integer(@Buf) + Int64SZ );
      while dwRet > 60 do begin
        // 获取文件名称
        AFileName := PWideChar(Integer(UsnRecord) + UsnRecord^.FileNameOffset);
        AFileName := Copy(AFileName, 1, UsnRecord^.FileNameLength div 2);
        // 将文件信息添加到列表中
        pFile           := AllocMem(Sizeof(TFileInfo)); // 注意要释放内存
        pFile^.FileName := AFileName;
        pFile^.FileId   := UsnRecord^.FileReferenceNumber;
        pFile^.ParentId := UsnRecord^.ParentFileReferenceNumber;
        AFiles.AddObject(AFileName, TObject(pFile));
        // 获取下一个 USN 记录
        if UsnRecord.RecordLength > 0 then begin
          Dec(dwRet, UsnRecord.RecordLength)
        end else begin
          Break;
        end;
        UsnRecord := PUSN_RECORD(Cardinal(UsnRecord) + UsnRecord.RecordLength);
      end;
      Move(Buf, MEnum, Int64SZ);
    end;
    // 7.获取文件全路径,包含路径和文件名
    GetFullFileName(ADiskName, TStringList(AFiles));
    // 8.释放内存
    for Idx := AFiles.Count - 1 Downto 0 do begin
      FreeMem(PFileInfo(AFiles.Objects[Idx]));
    end;
    // 9.删除USN日志文件信息
    DUSN.UsnJournalID := USN.UsnJournalID;
    DUSN.DeleteFlags  := USN_DeleteFlags;
    DeviceIoControl(hFile, FSCTL_DELETE_USN_JOURNAL, @DUSN, Sizeof(DUSN), nil, 0, dwRet, nil);
    CloseHandle(hFile);
    Result := True;
  end;

end;

end.

来源http://delphifmx.com/node/13