/
lasersquad
/
XFinder
Обзор
Документация
Войти
/
lasersquad
/
XFinder
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/FileCache.pas
1 966 строк
64 KB
lasersquad0
.
25 июн 2026, 17:23
25 июн 2026, 17:23
33aa4d0
Код
Авторство
О чём код?
Unit FileCache; interface uses System.Classes, System.SysUtils, Windows, DynamicArray, DynamicArrays, Hash, SortedArray, ObjectsCache, CacheItem; type TFileTypes = (ftFile, ftDir, ftTemp, ftArchive, ftReadOnly, ftHidden, ftSystem, ftDevice, ftSymbolic, ftCompressed, ftEncrypted, ftOffline, ftSparse, ftPinned, ftNotIndexed, ftVirtual, ftAll); TFileTypeNames = array [TFileTypes] of string; TFileSystemStat = array [TFileTypes] of Cardinal; TFileSystemStatIndex = array [Ord(Low(TFileTypes)).. Ord(High(TFileTypes))] of TFileTypes; TFileSystemStatRecord = record Stat : TFileSystemStat; Index: TFileSystemStatIndex; end; TStatRecord = record Directories: Cardinal; Archive: Cardinal; ReadOnly: Cardinal; Files: Cardinal; Hidden: Cardinal; Temporary: Cardinal; Devices: Cardinal; System: Cardinal; Symbolic: Cardinal; Compressed:Cardinal; Encrypted: Cardinal; Offline: Cardinal; Sparse: Cardinal; end; TError = record ErrCode: Integer; // ErrCode=0 means 'no error', any other value is signal about error ErrText: array [0..255] of Char; Important: Byte; // if <>0 tells the processing function that this error is important function HasError: Boolean; function IsImportant: Boolean; procedure Create(code: Integer; msg: string); procedure SetMsg(msg: string); function GetMsg: string; property Msg: string read GetMsg write SetMsg; end; IIndexingProgress = class procedure Start(Notes: string; P100: Integer = -1); virtual; abstract; // defines Max value for progress. -1 means that value for 100% progress is unknown procedure Finish; virtual; abstract; function Progress(Prgress: Integer): Boolean; virtual; abstract; // Result=False stops process if indexing takes too long time procedure ReportError(Error: TError); virtual; abstract; end; // if True is returned as a result of this function that means 'stop searching' TFNCSearchResult = function({FullPath: string;} FileData: TCacheItem): Boolean of object; TSearchResult = (srOK, srWrongPath, srNoIndexData, srCancelled); TFileSizeCompare = (fscEquals, fscMore, fscLess); TSearchDateType = (sdNone, sdCreated, sdModified, sdLastAccess); TSearchFilter = record //StartFrom: string; SearchStr: string; SearchStrUpper: string; // optimization for case insensitive search ExactSearch: Boolean; WildcardSearch: Boolean; CaseSensitive: Boolean; SearchByFileSize: Boolean; FileSize: UInt64; FileSizeCmpType: TFileSizeCompare; SearchByDateType: TSearchDateType; DateFrom: TFileTime; DateTo: TFileTime; SearchByAttributes: Boolean; Attributes: Cardinal; end; // We need THarray here because we store TCacheItems as sequential pieces of memory // TCacheItem objects are created on these pieces of memory using InitInstance(pointer) calls // This is done to reduce memory fragmentation because we need to store many small TCacheItems TLevelType = THArray; PFileLevel = ^TFileLevel; PPFileLevel = ^PFileLevel; TVolumeCache = class type THArrayString = THArrayG<string>; private FName: string; FCacheData: THArrayG<TLevelType>; FProgressListeners: THArrayG<IIndexingProgress>; FIndexedDateTime: TDateTime; // datetime when Volume cache data has been created (indexed). FExecTime: Cardinal; FExclFolders: THArraySorted<string>; class var // workaround // we can pass only simple function pointer to MFT DLL (cannot pass function of object). See MFTCallbackFunc. // that is why we have to define volume cache class variable // NOTE that will NOT work if we want to read volumes in parallel in different threads FVolCache: TVolumeCache; FMakePathCache: TObjectsCache<THArrayString>; // optimization, global array to hold path items before converting into full path string procedure Serialize(OStream: TStream); procedure Deserialize(IStream: TStream); overload; procedure Deserialize(IStream: PPFileLevel; Count: Cardinal); overload; procedure SaveTo(const fileName: string); function CompareProcString(item1, item2: string): Integer; function AddLevel(level: Cardinal): TLevelType; function AddRootItem(var fileData: TWin32FindData):TCacheItemRef; function AddItem(parent: Cardinal; var fileData:TWin32FindData; itemLevel: Cardinal; doSearch: Boolean = False): TCacheItemRef; function AddFullPath(const Path: string): TCacheItemRef; function GetItem(itemRef: TCacheItemRef): TCacheItem; overload; procedure FillFileData(const filePath: string; var fileData: TWin32FindData); function ReadDirectory(const currDir: TFileName; parent: TCacheItemRef; ShowProgress: Boolean): UInt64; procedure NotifyStart(Notes: string); procedure NotifyFinish; function NotifyProgress(Progrs: Integer): Boolean; procedure NotifyError(Error: TError); //procedure CloseFindHandles; //constructor CreatePrivate; //class procedure FreeInst; procedure StatSort(var Stat: TFileSystemStatRecord); class function MFTCallbackFunc(progress: Integer): Integer; static; public class constructor Create; class destructor Destroy; constructor Create; destructor Destroy; override; procedure SerializeTo(const FileName:string); //procedure DeserializeFrom(const FileName:string); procedure Clear; function Size: UInt64; function Count: Cardinal; function GetItem(Level: Cardinal; Index: Cardinal): TCacheItem; overload; function LevelCount(Level: Cardinal): Cardinal; function Levels(): Cardinal; // function ReadFileSystem(const Volumes: TArray<string>): UInt64; function ReadVolume(Volume: string; ExclusionsList: TArray<string>): UInt64; function ReadVolumeFast(Volume: string; ExclusionsList: TArray<string>): UInt64; function MakePathString(ref: TCacheItemRef): string; overload; function MakePathString(itemLevel, itemIndex: Cardinal): string; overload; function Search(Filter: TSearchFilter; Callback: TFNCSearchResult): TSearchResult; procedure PrintLevelsStat(list: TStrings); procedure PrintAllItems(list: TStrings); function GetStat: TFileSystemStatRecord; procedure PrintStat(stat: TFileSystemStat; list: TStrings); // integrity checks procedure CheckThatParentIsDirectory; function CheckHangingDirectories: THArrayG<string>; procedure CheckLevelsDataIsCorrect; procedure CheckFileDatesAreCorrect; property IndexedDateTime: TDateTime read FIndexedDateTime; property VolName: string read FName; property ExecTime: Cardinal read FExecTime; end; TVolumeExecData = record VolumeName: string; ExecTime: Cardinal; VolSize: UInt64; ItemsCount: Cardinal; end; TCache = class private class var GInstance: TCache; // single instance of cache class var GInstance2: TCache; private type TIndexFileSignaure = array [0..3] of Byte; private const FILE_SIGNATURE: TIndexFileSignaure = (Ord('F'), Ord('I'), Ord('N'), Ord('D')); FILE_VERSION : TIndexFileSignaure = (Ord('0'), Ord('1'), Ord('0'), Ord('1')); private FVolumeData: THash<string, TVolumeCache>; FProgressListeners: THArrayG<IIndexingProgress>; FIndexFileSaveDate: TDateTime; // datetime when index file was saved, valid only after loading index file. procedure Serialize(OStream: TStream); procedure Deserialize(IStream: TStream); // procedure SaveTo(const fileName: string); constructor CreatePrivate; destructor Destroy; override; class destructor FreeInst; // this will be automatically called by Delphi to free resources public constructor Create; // raises an exception to avoid creating other instances of cache class procedure FreeInst2; class function Instance: TCache; class function NewInstance: TCache; class procedure Swap; class function HasNewInstance: Boolean; procedure SerializeTo(const FileName:string); procedure DeserializeFrom(const FileName:string); procedure Clear; overload; // clears data of all volumes procedure Clear(Volume: string); overload; // clears specified volume only function VolumesCount: Cardinal; function GetVolume(Volume: string): TVolumeCache; function VolumePresent(Volume: string): TVolumeCache; function GetVolumes: TArray<string>; function GetOrCreateVolume(Volume: string): TVolumeCache; function GetExecData: TArray<TVolumeExecData>; function GetVolumeNamesAsString: string; procedure ReadVolume(Volume: string; ExclusionsList: TArray<string>); procedure ReadVolumeFast(Volume: string; ExclusionsList: TArray<string>); //procedure ReadVolumesFast(Volumes: TArray<string>; ExclusionsList: TArray<string>); function Search(Filter: TSearchFilter; Callback: TFNCSearchResult): TSearchResult; procedure AddProgressListener(listener: IIndexingProgress); procedure RemoveProgressListener(listener: IIndexingProgress); // integrity checks procedures procedure CheckHangingDirectories; procedure CheckThatParentIsDirectory; procedure CheckLevelsDataTsCorrect; procedure CheckFileDatesAreCorrect; function GetStat(Volume: string): TFileSystemStatRecord; property IndexFileSaveDate: TDateTime read FIndexFileSaveDate; end; MFT_INDEX = packed record low: UInt32; // The low part of the file number. high: UInt16; // The high part of the file number. seq: UInt16; // The sequence number of MFT record. end; // 8 bytes // MFT record number structure. MFT_REF = packed record case Byte of 0: (sId: MFT_INDEX); 1: (Id: UInt64); end; // 8 bytes NTFS_DUP_INFO = packed record CreateTime: TFileTime; //UInt64; // 0x00 File creation file. ModifyTime: TFileTime; //UInt64; // 0x08 File modification time. ModifyAttrTime: TFileTime; //UInt64; // 0x10 Last time any attribute was modified. LastAccessTime: TFileTime; //UInt64; // 0x18 File last access time. AllocSize: UInt64; // 0x20 Data attribute allocated size (for unnamed $DATA attribute), multiple of cluster size. FileSize: UInt64; // 0x28 Actual data attribute size <= AllocSize. FileAttrib: UInt32; // 0x30 Standard DOS attributes & more. ea_size: UInt16; // 0x34 Packed EAs. reparse: UInt16; // 0x36 Used by Reparse. end; // 0x38= 56 bytes // Filename attribute structure (0x30). Resident only ATTR_FILE_NAME = packed record ParentDir: MFT_REF; // 0x00 reference to MFT record for parent directory. dup: NTFS_DUP_INFO; // 0x08 FileNameLen: UInt8; // 0x40 File name length in words for unicode. NameType: UInt8; // 0x41 File name type: POSIX=0, UNICODE=1, DOS=2, BOTH=3 end; // 0x42=66 bytes CACHE_ITEM = packed record FParent: UInt32; FLevel: UInt32; FFilesCount: Int32; FMFTRecID: MFT_REF; // MFT Id of this file FileAttr:ATTR_FILE_NAME; // 66 bytes. must be last field in FILELIST_ITEM because it has variable size function Size: UInt32; function Name: string; end; // 82 bytes long PCACHE_ITEM = ^CACHE_ITEM; TFileLevel = packed record FCount: UInt32; FStart: PCACHE_ITEM; end; var FileTypeNames: TFileTypeNames = ('File', 'Directory', 'Temporary', 'Archive', 'ReadOnly', 'Hidden', 'System', 'Device', 'Symbolic Link', 'Compressed', 'Encrypted', 'Offline', 'Sparse', 'Pinned', 'NotIndexed', 'Virtual', 'UNDEF'); implementation uses System.Math, System.Generics.Defaults, Functions, MaskSearch, Hash2, Logger; const MAX_LEVELS = 100; const MAX_LEVEL_DIRS = 10_000; const TEST_DIR_MASK = NTFS_DIRECTORY_MASK OR FILE_ATTRIBUTE_DIRECTORY; const FILE_VALID_ATTRIBUTES = (2*FILE_ATTRIBUTE_ENCRYPTED - 1) OR NTFS_DIRECTORY_MASK; {const SUM_OF_ATTRS = FILE_ATTRIBUTE_READONLY + FILE_ATTRIBUTE_HIDDEN + FILE_ATTRIBUTE_SYSTEM + FILE_ATTRIBUTE_DIRECTORY + FILE_ATTRIBUTE_ARCHIVE + FILE_ATTRIBUTE_DEVICE + FILE_ATTRIBUTE_NORMAL + FILE_ATTRIBUTE_TEMPORARY + FILE_ATTRIBUTE_SPARSE_FILE + FILE_ATTRIBUTE_REPARSE_POINT + FILE_ATTRIBUTE_COMPRESSED + FILE_ATTRIBUTE_OFFLINE + FILE_ATTRIBUTE_NOT_CONTENT_INDEXED + FILE_ATTRIBUTE_ENCRYPTED;} //////////////////////////////////// // Common Functions /////////////////////////////////// function IS_DOT_DIR(dirName: PWideChar): Boolean; begin Result := ( (dirName[0] = '.') AND (dirName[1] = #0) ) OR ( (dirName[0] = '.') AND (dirName[1] = '.') AND (dirName[2] = #0) ); end; function IsDirectory(var FileData: TWin32FindData): Boolean; begin IsDirectory := (FileData.dwFileAttributes AND FILE_ATTRIBUTE_DIRECTORY) > 0; end; function IsReparse(var FileData: TWin32FindData): Boolean; begin IsReparse := (FileData.dwFileAttributes AND FILE_ATTRIBUTE_REPARSE_POINT) > 0; end; //////////////////////////////////// // TVolumeCache methods /////////////////////////////////// function TVolumeCache.CompareProcString(item1, item2: string): Integer; begin Result := CompareText(item1, item2); end; {$IFOPT J+} {$DEFINE WAS_WRITABLECONST_ON} {$ENDIF} {$WRITEABLECONST ON} // needed for ProgressCounter static variable function TVolumeCache.ReadDirectory(const CurrDir: TFileName; Parent: TCacheItemRef; ShowProgress: Boolean): UInt64; const FIND_FIRST_EX_LARGE_FETCH = $00000002; ProgressCounter: Integer = 0; // this works as static variable inside a procedure //strBld: TStringBuilder = TStringBuilder.Create(100); var DirSize: UInt64; ItemsCount: Cardinal; // number of files and dirs in current directory SearchDir: string; fileData: TWin32FindData; hFind: THandle; //tmp: LARGE_INTEGER; Err: TError; begin Assert(sizeof(UInt64) = 8); Result := 0; // bypass directories from Exclude list if FExclFolders.QuickFind(CompareProcString, CurrDir) > 0 then Exit; DirSize := 0; //tmp.QuadPart := 0; Assert(CurrDir[length(CurrDir)] = PathDelim); // currDir should end on '\' SearchDir := CurrDir + '*'; // add prefix to extend path string to 32767 symbols ZeroMemory(@fileData, sizeof(fileData)); // this will be true only for the first ReadDirectory call in reccursion because for all other calls ShowProgress=false if ShowProgress then begin ProgressCounter := 0; NotifyStart(CurrDir); end; hFind := FindFirstFileEx(PChar(SearchDir), FindExInfoBasic, @fileData, FindExSearchNameMatch, nil, FIND_FIRST_EX_LARGE_FETCH); //hFind := Windows.FindFirstFile(PChar(searchDir), fileData); if hFind = INVALID_HANDLE_VALUE then begin Err.ErrCode := GetLastError(); if Err.ErrCode = ERROR_ACCESS_DENIED then begin // special processing for ERROR_ACCESS_DENIED error Err.Msg := 'Access denied: ' + CurrDir; Err.Important := 0; // do not show message box with this error, just log it NotifyError(Err); // this call logs error message into log file and shows messagebox with error message var item := GetItem(Parent); item.FDenied := True; // set flag that we cannot enter into this folder because of permission denied. end else // other than ERROR_ACCESS_DENIED error encountered begin Err.Important := 1; // show message box about this error Err.Msg := 'ERROR in FindFirstFileEx: ' + CurrDir + ' GetLastError: ' + IntToStr(Err.ErrCode); NotifyError(Err); // this callback logs message into log file and shows messagebox with error message end; Result := DirSize; Exit; end; Assert(fileData.cFileName[0] <> #0); ItemsCount := 0; // bypass dirs '.' and '..' if NOT IS_DOT_DIR(fileData.cFileName) then begin ItemsCount := 1; var itemRef := AddItem(parent.ItemIndex, fileData, Parent.ItemLevel + 1); if IsDirectory(fileData) then begin if NOT IsReparse(fileData) then DirSize := ReadDirectory(CurrDir + fileData.cFileName + PathDelim, itemRef, False); end else begin DirSize := MakeFileSize(fileData.nFileSizeHigh, fileData.nFileSizeLow); end; end; while(True) do begin if ShowProgress then begin Inc(ProgressCounter); // if user pressed cancel then we raise an exception to be able to exit from all reccursive ReadDirectory calls. if NOT NotifyProgress(ProgressCounter) then raise EOperationCancelled.Create('User aborted.'); end; if FindNextFile(hFind, fileData) then begin Assert(fileData.cFileName[0] <> #0); if IS_DOT_DIR(fileData.cFileName) then continue; Inc(ItemsCount); var itemRef := AddItem(parent.ItemIndex, fileData, Parent.ItemLevel + 1); if IsDirectory(fileData) then begin if NOT IsReparse(fileData) then DirSize := DirSize + ReadDirectory(CurrDir + fileData.cFileName + PathDelim, itemRef, False); end else begin DirSize := DirSize + MakeFileSize(fileData.nFileSizeHigh, fileData.nFileSizeLow); end; end else begin // error in FindNextFile Err.ErrCode := GetLastError(); if Err.ErrCode = ERROR_NO_MORE_FILES then break; // this is NOT an error Err.Msg := 'ERROR in FindNexFile: ' + CurrDir + ' GetLastError: ' + IntToStr(Err.ErrCode); Err.Important := 1; //Err.ErrCode <> ERROR_ACCESS_DENIED; NotifyError(Err); // this callback logs message into log file and shows messagebox with error message break; end end; var item := GetItem(Parent); item.FFileSize := DirSize; item.FFileCount := ItemsCount; // FFindHandles.AddValue(hFind); Windows.FindClose(hFind); // weird, this call takes too much time for some reason. It is called for each scanned directory. if ShowProgress then NotifyFinish; Result := DirSize; end; {$IFDEF WAS_WRITABLECONST_ON} {$WRITEABLECONST ON} {$UNDEF WAS_WRITABLECONST_ON} {$ELSE} {$WRITEABLECONST OFF} {$ENDIF} procedure TVolumeCache.FillFileData(const FilePath: string; var FileData: TWin32FindData); var //hf: THandle; //fileSize: LARGE_INTEGER; data: TWin32FileAttributeData; res: LongBool; begin //fileSize.QuadPart := 0; //FileData.dwFileAttributes := Windows.GetFileAttributes(PChar(FilePath)); res := Windows.GetFileAttributesEx(PChar(FilePath), GetFileExInfoStandard, @data); if res = True then begin FileData.dwFileAttributes := data.dwFileAttributes; FileData.ftCreationTime := data.ftCreationTime; FileData.ftLastAccessTime := data.ftLastAccessTime; FileData.ftLastWriteTime := data.ftLastWriteTime; FileData.nFileSizeHigh := data.nFileSizeHigh; FileData.nFileSizeLow := data.nFileSizeLow; end else begin TLogger.WarnFmt('[GetFileAttributesEx] failed with error: %d', [GetLastError]); end; // we need file handle first to get file time and file size {hf := Windows.CreateFile(PChar(FilePath), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL OR FILE_FLAG_BACKUP_SEMANTICS, 0); try if hf = INVALID_HANDLE_VALUE then begin raise EInOutError.Create(SysErrorMessage(GetLastError()), FilePath); end; Windows.GetFileTime(hf, @FileData.ftCreationTime, @FileData.ftLastAccessTime, @FileData.ftLastWriteTime); Windows.GetFileSizeEx(hf, fileSize.QuadPart); FileData.nFileSizeHigh := DWORD(fileSize.HighPart); FileData.nFileSizeLow := DWORD(fileSize.LowPart); finally Windows.CloseHandle(hf); end; } end; // finds empty dirs - dirs that do not contain any files and subdirs // calcs count of empty dirs and finds a directory with max number of files in it. // on the way it does some integrity checks function TVolumeCache.CheckHangingDirectories: THArrayG<string>; var i, j : Cardinal; item, parent: TCacheItem; table: THash2<Cardinal, Cardinal, Cardinal>; pValue: THash2<Cardinal, Cardinal, Cardinal>.PointerV; begin table := THash2<Cardinal, Cardinal, Cardinal>.Create; Result := THArrayG<string>.Create; try table.SetValue(0, 0, 0); // root is C: set counter for it for i := 1 to FCacheData.Count - 1 do begin // start from 1 here because we look for parent var lv := FCacheData[i]; for j := 0 to lv.Count - 1 do begin item := TCacheItem(lv.GetAddr(j)); //if item.FDenied then Assert(item.IsDirectory); // FDenied can be set for both directories (access denied) and files (when any other error occurred) // bypass denied dirs if item.IsDirectory AND NOT item.FDenied AND NOT item.IsReparsePoint then begin // bypass symbolic links pValue := table.GetValuePointer(i, j); if pValue = nil then table.SetValue(i, j, 0); // if item is directory then set its links counter to zero end; parent := GetItem(i - 1, item.FParent); Assert(parent.IsDirectory); pValue := table.GetValuePointer(i - 1, item.FParent); Assert((pValue <> nil) OR parent.IsReparsePoint); // cannot be nil because we have already marked all previous level dirs with zero counter if Assigned(pValue) then Inc(pValue^); // increase links counter end; end; var mx1: Cardinal := 0; var mx2: Cardinal := 0; var mxDir1, mxDir2: string; // creates a list of empty dirs in Result for i := 1 to table.Count do begin for j := 1 to table.Count(i) do begin pValue := table.GetValuePointer(i - 1, j - 1); if Assigned(pValue) then begin item := GetItem(i - 1, j - 1); Assert(pValue^ = item.FFileCount); if pValue^ = 0 then begin Result.AddValue(MakePathString(i - 1, j - 1)); end; if mx1 < pValue^ then begin mx1 := pValue^; // find dir with maximum child items mxDir1 := MakePathString(i - 1, j - 1); end; if mx2 < item.FFileCount then begin mx2 := item.FFileCount; mxDir2 := MakePathString(i - 1, j - 1); end; end; end; end; Assert(mx1 = mx2); var cnt := Result.Count; // comment these two lines to write list of empty dirs into log file Result.Clear; Result.AddValue('Empty folders count: ' + cnt.ToString {Result.Count.ToString}); Result.AddValue('[1] Maximum items in Dir: ' + mx1.ToString + ' - ' + mxDir1); // Result.AddValue('[2] Maximum items in Dir: ' + mx2.ToString + ' - ' + mxDir2); finally table.Free; end; end; procedure TVolumeCache.CheckThatParentIsDirectory; var i, j: Cardinal; item, parent: TCacheItem; begin for i := 1 to FCacheData.Count - 1 do begin var lv := FCacheData[i]; for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); // Parent of any item must be a directory parent := GetItem(i - 1, item.FParent); Assert(parent.IsDirectory); end; end; end; procedure TVolumeCache.CheckLevelsDataIsCorrect; var i, j: Cardinal; item: TCacheItem; begin // check that all items contain correct Level data. for i := 1 to FCacheData.Count do begin var lv := FCacheData[i - 1]; for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); Assert(item.FLevel = i - 1); Assert(item.FFileAttrs > 0); // zero file attr is replaced by NORMAL attribute if read by FAST method. //Assert( NOT ((item.FFileAttrs > SUM_OF_ATTRS) AND (item.FFileAttrs < NTFS_DIRECTORY_MASK)) ); Assert( ((item.FFileAttrs AND TEST_DIR_MASK) = 0) OR ((item.FFileAttrs AND TEST_DIR_MASK) = TEST_DIR_MASK) ); // both DIR bits either set or both cleared end; end; end; procedure TVolumeCache.CheckFileDatesAreCorrect; var i, j: Cardinal; item: TCacheItem; begin // check that each item contain correct date fields. for i := 1 to FCacheData.Count do begin var lv := FCacheData[i - 1]; for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); if item.IsDirectory AND NOT item.IsReparsePoint AND NOT item.FDenied then Assert(item.FFileCount >= 0) else Assert(item.FFileCount = -1); // -1 is to differ from empty folders where FFileCount=0 Assert(item.FFileAttrs > 0); Assert(item.FFileName <> ''); Assert(item.FDisplayName <> ''); Assert(item.FDisplayName = item.FFileName); Assert(PInt64(@item.FCreationTime)^ <> 0); Assert(PInt64(@item.FLastAccessTime)^ <> 0); Assert(PInt64(@item.FModifiedTime)^ <> 0); // check size of files only, because directory size is size of all files inside directory if NOT item.IsDirectory then Assert(item.FFileSize < UInt64(100)*1024*1024*1024); // check that all sizes are less than 100G end; end; end; { class procedure TVolumeCache.FreeInst; begin if Assigned(GInstance) then FreeAndNil(GInstance); end; } procedure TVolumeCache.Clear; var item: TCacheItem; begin if FCacheData.Count = 0 then Exit; for var i: Cardinal := 0 to FCacheData.Count - 1 do begin var lv := FCacheData[i]; for var j: Cardinal := 0 to lv.Count - 1 do begin item := TCacheItem(lv.GetAddr(j)); item.CleanupInstance; end; lv.Free; end; FCacheData.Clear; FIndexedDateTime := 0; // reset indexed time end; { procedure TVolumeCache.CloseFindHandles; var i: Cardinal; begin for i := 1 to FFindHandles.Count do Windows.FindClose(FFindHandles[i - 1]); FFindHandles.Clear; end; } function TVolumeCache.Count: Cardinal; begin Result := 0; if FCacheData.Count = 0 then Exit; for var i: Cardinal := 0 to FCacheData.Count - 1 do Result := Result + FCacheData[i].Count; end; class constructor TVolumeCache.Create; begin FMakePathCache := TObjectsCache<THArrayString>.Create(3, false); end; constructor TVolumeCache.Create; begin FCacheData := THArrayG<TLevelType>.Create; FCacheData.SetCapacity(MAX_LEVELS); FProgressListeners := nil; //THArrayG<IIndexingProgress>.Create; FExclFolders := THArraySorted<string>.Create(TIStringComparer.Ordinal); FIndexedDateTime := 0; // default value, because index is not created yet end; destructor TVolumeCache.Destroy; begin Clear; FreeAndNil(FCacheData); FreeAndNil(FExclFolders); end; class destructor TVolumeCache.Destroy; begin FreeAndNil(FMakePathCache); end; ///////////////////////////////////// // Auxiliary functions for Search //////////////////////////////////// // Filter passed by reference intentionally to avoid unnessesary copy its data during function call function CheckForFileSize(var Filter: TSearchFilter; FileSize: UInt64{; IsDir: Boolean}): Boolean; begin Result := False; case Filter.FileSizeCmpType of fscEquals: Result := FileSize = Filter.FileSize; fscMore: Result := FileSize > Filter.FileSize; fscLess: Result := FileSize < Filter.FileSize; end; end; // Filter passed by reference intentionally to avoid unnessesary copy its data during function call // True if Filter.SearchStr is a substring of FileName function CheckForFileName(var Filter: TSearchFilter; GrepList: TStringList; FileName, FileNameUpper: string): Boolean; begin //if GrepList = nil then begin //if (Pos('*', Filter.SearchStr) > 0) OR (Pos('?', Filter.SearchStr) > 0 ) then begin //if (StrScan(PChar(Filter.SearchStr), '*') <> nil) OR (StrScan(PChar(Filter.SearchStr), '?') <> nil) then begin if Filter.WildcardSearch then begin if Filter.CaseSensitive then Result := WildcardMatch(FileName, Filter.SearchStr) else Result := WildcardMatch(FileNameUpper, Filter.SearchStrUpper); end else begin if Filter.ExactSearch then // look for full name instead of substr if Filter.CaseSensitive then Result := Filter.SearchStr = FileName else Result := Filter.SearchStrUpper = FileNameUpper else if Filter.CaseSensitive // look for substr then Result := Pos(Filter.SearchStr, FileName) > 0 else Result := Pos(Filter.SearchStrUpper, FileNameUpper) > 0; end { else begin // use mask search if Filter.CaseSensitive then Result := cmpmask(FileName, GrepList) else Result := cmpmask(FileNameUpper, GrepList); end;} end; // Filter passed by reference intentionally to avoid unnessesary copy its data during function call function CheckForDate(var Filter: TSearchFilter; item: TCacheItem): Boolean; begin case Filter.SearchByDateType of sdModified: Result := (CompareFileTime(item.FModifiedTime, Filter.DateFrom) >= 0) AND (CompareFileTime(item.FModifiedTime, Filter.DateTo) <= 0); sdCreated: Result := (CompareFileTime(item.FCreationTime, Filter.DateFrom) >= 0) AND (CompareFileTime(item.FCreationTime, Filter.DateTo) <= 0); sdLastAccess: Result := (CompareFileTime(item.FLastAccessTime, Filter.DateFrom) >= 0) AND (CompareFileTime(item.FLastAccessTime, Filter.DateTo) <= 0); sdNone: Result := False; else Result := False; end; end; // Filter passed by reference intentionally to avoid unnessesary copy its data during function call function CheckForAttributes(var Filter: TSearchFilter; FileAttributes: Cardinal): Boolean; begin Result := (FileAttributes AND Filter.Attributes) > 0; end; // Filter passed by reference intentionally to avoid unnessesary copy its data during function call // if GrepList=nil use substr search otherwise use mask search functions function ApplyFilter(var Filter: TSearchFilter; GrepList: TStringList; Item: TCacheItem): Boolean; begin Result := False; // by default means 'not found' if Filter.SearchStr <> '' then if NOT CheckForFileName(Filter, GrepList, Item.FFileName, Item.FUpperCaseName) then Exit; if Filter.SearchByFileSize then if NOT CheckForFileSize(Filter, Item.FFileSize{, IsDirectory(Item)}) then Exit; if Filter.SearchByDateType <> sdNone then if NOT CheckForDate(Filter, item) then Exit; if Filter.SearchByAttributes then if NOT CheckForAttributes(Filter, item.FFileAttrs) then Exit; Result := True; end; function TVolumeCache.Search(Filter: TSearchFilter; Callback: TFNCSearchResult): TSearchResult; var //startArray: THArrayG<string>; //GrepList: TStringList; i, j, len: Cardinal; //Found: Boolean; item: TCacheItem; begin if FCacheData.Count = 0 then Exit(srNoIndexData); // determine whether we need to make "whole words" search Filter.ExactSearch := False; len := Length(Filter.SearchStr); if len > 2 then begin // check for surrounding double quotes and single quotes if (Filter.SearchStr[1] = '"') AND (Filter.SearchStr[len] = '"') OR (Filter.SearchStr[1] = '''') AND (Filter.SearchStr[len] = '''') then begin Filter.ExactSearch := True; Filter.SearchStr := Copy(Filter.SearchStr, 2, len - 2); end; end; //if (Pos('*', Filter.SearchStr) > 0) OR (Pos('?', Filter.SearchStr) > 0 ) then begin Filter.WildcardSearch := (StrScan(PChar(Filter.SearchStr), '*') <> nil) OR (StrScan(PChar(Filter.SearchStr), '?') <> nil); Filter.SearchStrUpper := AnsiUpperCase(Filter.SearchStr); //Found := False; //startArray := THArrayG<string>.Create; {GrepList := nil; try // check if filter str has wildcards if (Pos('*', Filter.SearchStr) > 0) OR (Pos('?', Filter.SearchStr) > 0 ) then begin //GrepList := TStringList.Create; if Filter.CaseSensitive // compile filters into GrepList then GrepList := CompileMask(Filter.SearchStr) else GrepList := CompileMask(Filter.SearchStrUpper); end; } {StringToArray(Filter.StartFrom, startArray, '\'); if StartArray.Count > 0 then begin // verify that index DB contains all folders in Filter.StartFrom path for i := 0 to StartArray.Count - 1 do begin var lev := FCacheData[i]; Found := False; for j := 0 to lev.Count - 1 do begin item := lev.GetAddr(j); if CompareText(item.FUpperCaseName, startArray[i]) = 0 then begin Found := True; break; end; end; if NOT Found then break; //looks like StartFrom is not found in index DB. end; if NOT Found then Exit(srWrongPath); end; } for i := 1{startArray.Count} to FCacheData.Count do begin // bypass Filter.StartFrom folders because each level contain only one folder from StartFrom path var lv := FCacheData[i - 1]; Assert(lv.Count > 0); for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); if ApplyFilter(Filter, nil{GrepList}, item) then begin //if item.IsDirectory then begin // build PathString only for directories if item.FPath = '' then item.FPath := MakePathString(i - 1, j - 1); if NOT Callback(item) then Exit(srCancelled); //end else begin // if NOT Callback(MakePathString(i - 1, j - 1), item) then Exit(srCancelled); //end; end; end; end; Result := srOK; //finally //startArray.Free; // FreeCompiledMask(GrepList); //end; end; procedure TVolumeCache.Serialize(OStream: TStream); var i, j: Cardinal; item: TCacheItem; begin OStream.WriteData<TDateTime>(FIndexedDateTime); // write datetime of latest index file update WriteStringToStream(OStream, FName); OStream.WriteData<Cardinal>(FCacheData.Count); for i := 1 to FCacheData.Count do begin var lv := FCacheData[i - 1]; OStream.WriteData<Cardinal>(lv.Count); for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); item.Serialize(OStream); end; end; end; procedure TVolumeCache.Deserialize(IStream: TStream); var i, j, start: Cardinal; CacheSize, levelSize: Cardinal; level: TLevelType; item: TCacheItem; begin start := GetTickCount; Clear; IStream.ReadData<TDateTime>(FIndexedDateTime); FName := ReadStringFromStream(IStream); IStream.ReadData<Cardinal>(CacheSize); if cacheSize = 0 then begin // empty volume is OK. just write a warning into log file TLogger.InfoFmt('[TVolumeCache.Deserialize] cache size read from index file is zero for volume "%s"!', [FName]); end; FCacheData.SetCapacity(CacheSize); for i := 1 to CacheSize do begin level := TLevelType.Create; level.ItemSize := Cardinal(TCacheItem.InstanceSize); FCacheData.AddValue(level); IStream.ReadData<Cardinal>(levelSize); Assert(levelSize > 0); level.AddFillValues(levelSize); for j := 1 to levelSize do begin item := TCacheItem(TCacheItem.InitInstance(level.GetAddr(j-1))); // init object by specitied address item.Create; // call constructor on object instantiated at specified address item.Deserialize(IStream); // level.Add(item); end; end; FExecTime := GetTickCount - start; end; function CACHE_ITEM.Size: UInt32; begin Result := sizeof(CACHE_ITEM) + FileAttr.FileNameLen * sizeof(WChar); end; function CACHE_ITEM.Name: string; var p: PChar; begin p := PChar(NativeInt(@Self) + sizeof(CACHE_ITEM)); SetString(Result, p, fileAttr.FileNameLen); end; procedure TVolumeCache.Deserialize(IStream: PPFileLevel; Count: Cardinal); var fileCache: PPFileLevel; origLevel: PFileLevel; origItem: PCACHE_ITEM; level: TLevelType; item: TCacheItem; j, i: Cardinal; start{, err, len}: Cardinal; begin start := GetTickCount; Clear; FIndexedDateTime := Now; fileCache := IStream; for i := 1 to Count do begin origLevel := fileCache^; TLogger.DebugFmt('[TVolumeCache.Deserialize] reading level %d', [i]); // avoid empty levels if origLevel.FCount > 0 then begin level := TLevelType.Create; level.ItemSize := Cardinal(TCacheItem.InstanceSize); FCacheData.AddValue(level); origItem := origLevel.FStart; level.AddFillValues(origLevel.FCount); for j := 1 to level.Count do begin item := TCacheItem(TCacheItem.InitInstance(level.GetAddr(j - 1))); // init object by specitied address item.Create; // call constructor on object instantiated at specified address item.FParent := origItem.FParent; item.FLevel := origItem.FLevel; Assert((origItem.FileAttr.dup.FileAttrib AND FILE_ATTRIBUTE_DIRECTORY) = 0); // in NTFS this attr bit is always zero // some files have FileAttrib=0 for some reason. WINAPI never return zero attrs. // it sets NORMAL bit for such files. Below we do the same. if (origItem.FileAttr.dup.FileAttrib AND FILE_VALID_ATTRIBUTES) = 0 then begin item.FFileAttrs := item.FFileAttrs OR FILE_ATTRIBUTE_NORMAL; end else begin // move NTFS_DIR flag/bit ($10000000) into DOS_DIR place, because all WINAPI functions return this flag in DOS_DIR place ($00000010) item.FFileAttrs := origItem.FileAttr.dup.FileAttrib OR ((origItem.FileAttr.dup.FileAttrib AND NTFS_DIRECTORY_MASK) shr 24); end; Assert(item.FFileAttrs > 0); // both DIR bits either set or both cleared Assert( ((item.FFileAttrs AND TEST_DIR_MASK) = 0) OR ((item.FFileAttrs AND TEST_DIR_MASK) = TEST_DIR_MASK) ); item.FCreationTime := origItem.FileAttr.dup.CreateTime; item.FLastAccessTime := origItem.FileAttr.dup.LastAccessTime; item.FModifiedTime := origItem.FileAttr.dup.ModifyTime; item.FFileSize := origItem.FileAttr.dup.FileSize; item.FFileName := origItem.Name; item.FDisplayName := item.FFileName; item.FUpperCaseName := AnsiUpperCase(item.FFileName); item.FDenied := False; item.FFileCount := origItem.FFilesCount; item.FIconIndex := 0; Assert(PInt64(@item.FCreationTime)^ > 0); Assert(PInt64(@item.FLastAccessTime)^ > 0); Assert(PInt64(@item.FModifiedTime)^ > 0); Assert(Length(item.FFileName) > 0); origItem := PCACHE_ITEM(NativeInt(origItem) + origItem^.Size); end; end; fileCache := PPFileLevel(NativeInt(fileCache) + SizeOf(PPFileLevel)); end; FExecTime := GetTickCount - start; end; function TVolumeCache.GetItem(Level, Index: Cardinal): TCacheItem; begin Result := TCacheItem(FCacheData[Level].GetAddr(Index)); end; function TVolumeCache.GetItem(itemRef: TCacheItemRef): TCacheItem; begin Result:= TCacheItem(FCacheData.GetValue(itemRef.ItemLevel).GetAddr(itemRef.ItemIndex)); end; function TVolumeCache.LevelCount(Level: Cardinal): Cardinal; begin Result := FCacheData[Level].Count; end; function TVolumeCache.Levels(): Cardinal; begin Result := FCacheData.Count; end; { class function TVolumeCache.Instance: TVolumeCache; begin if NOT Assigned(GInstance) then GInstance := TVolumeCache.CreatePrivate; Result := GInstance; end; class function TVolumeCache.NewInstance: TVolumeCache; begin Result := TVolumeCache.CreatePrivate; end; class function TVolumeCache.Swap(NewInstance: TVolumeCache): TVolumeCache; begin Result := GInstance; GInstance := NewInstance; end; } function TVolumeCache.AddLevel(level: Cardinal): TLevelType; begin Assert(level <= FCacheData.Count); if level < FCacheData.Count then begin Result := FCacheData[level]; end else begin Result := TLevelType.Create; Result.ItemSize := Cardinal(TCacheItem.InstanceSize); Result.SetCapacity(MAX_LEVEL_DIRS); FCacheData.AddValue(Result); // pointer is added to FCacheData because TCacheItem is class end; end; function TVolumeCache.AddRootItem(var fileData: TWin32FindData): TCacheItemRef; var level: TLevelType; i: Cardinal; item: TCacheItem; begin level := AddLevel(0); i := 0; // for root item we always need to do search // it is posible to have several root items on level 0 while i < level.Count do begin if TCacheItem(level.GetAddr(i)).FFileName = fileData.cFileName then break; Inc(i); end; if i < level.Count then begin // fount root item item := TCacheItem(level.GetAddr(i)); Result := TCacheItemRef.Create(item.FLevel, i); Exit; end; // need to create new root items item := TCacheItem(TCacheItem.InitInstance(level.AddFillValues(1))); item.Create(0, 0, fileData); Result.ItemLevel := 0; Result.ItemIndex := level.Count - 1; end; function TVolumeCache.AddItem(parent: Cardinal; var fileData: TWin32FindData; itemLevel: Cardinal; doSearch: Boolean = False): TCacheItemRef; var item: TCacheItem; begin var level := AddLevel(itemLevel); item := TCacheItem(TCacheItem.InitInstance(level.AddFillValues(1))); item.Create(parent, itemLevel, fileData); Result.ItemLevel := itemLevel; Result.ItemIndex := level.Count - 1; end; function TVolumeCache.AddFullPath(const Path: string): TCacheItemRef; var pathArray: THArrayG<string>; pathArrayAccum: THArrayG<string>; fileData: TWin32FindData; lv: Cardinal; parent: TCacheItemRef; begin pathArray := THArrayG<string>.Create; pathArrayAccum := THArrayG<string>.Create; try StringToArray(Path, pathArray, PathDelim); StringToArrayAccum(Path, pathArrayAccum, PathDelim); ZeroMemory(@fileData, sizeof(fileData)); lstrcpy(fileData.cFileName, PWideChar(pathArray[0])); FillFileData(pathArrayAccum[0], fileData); parent := AddRootItem(fileData); Assert(parent.ItemLevel = 0); for lv := 1 to pathArray.Count - 1 do begin Assert(lv = parent.ItemLevel + 1); lstrcpy(fileData.cFileName, PWideChar(pathArray[lv])); FillFileData(pathArrayAccum[lv], fileData); parent := AddItem(parent.ItemIndex, fileData, lv, True); end; finally pathArray.Free; pathArrayAccum.Free; end; Result := parent; end; procedure TVolumeCache.SerializeTo(const FileName: string); var mout: TMemoryStream; begin mout := TMemoryStream.Create; try Serialize(mout); mout.SaveToFile(FileName); finally mout.Free; end; end; function TVolumeCache.Size: UInt64; begin if Count = 0 then Exit(0); Result := GetItem(0, 0).FFileSize; end; {procedure TVolumeCache.DeserializeFrom(const FileName: string); var msin: TMemoryStream; begin msin := TMemoryStream.Create; try if FileExists(FileName) then begin //TODO: shall we raise an exception or return false in case index file is not found? msin.LoadFromFile(FileName); Deserialize(msin); end else begin Logger.LogFmt('Index file is not found or not accessible ().', [FileName]); end; finally msin.Free; end; end;} procedure TVolumeCache.SaveTo(const FileName: string); var fout:TFileStream; ii, k: Integer; i,j, index: Cardinal; sitem, item: TCacheItem; pathStr: string; path: THArrayG<TCacheItem>; begin fout := TFileStream.Create(fileName, fmCreate); path := THArrayG<TCacheItem>.Create; path.SetCapacity(MAX_LEVELS); try for i := FCacheData.Count - 1 downto 0 do begin var level := FCacheData[i]; for j := 0 to level.Count - 1 do begin sitem := TCacheItem(level.GetAddr(j)); path.Clear(); path.AddValue(sitem); index := sitem.FParent; ii := Integer(i) - 1; // ii variable need to be signed Integer while ii >= 0 do begin item := GetItem(Cardinal(ii), index); path.AddValue(item); index := item.FParent; Dec(ii); end; pathStr := path[path.Count - 1].FFileName; for k := Integer(path.Count) - 2 downto 0 do begin pathStr := pathStr + PathDelim + path[Cardinal(k)].FFileName; end; fout.Write(PChar(pathStr)^, ByteLength(pathStr){*sizeof(Char)}); fout.Write(PChar(sLineBreak)^, ByteLength(sLineBreak){*sizeof(Char)}); end; end; finally fout.Free; path.Free; end; end; function TVolumeCache.ReadVolume(Volume: string; ExclusionsList: TArray<string>): UInt64; var i: Int64; str: string; begin var start := GetTickCount; Clear; FName := Volume; //TODO: may be list of exclusions should be Volume agnostic somehow??? // fill ExclFolders with values related to the specified Volume only for i := 1 to Length(ExclusionsList) do begin str := ExclusionsList[i - 1]; if str.StartsWith(Volume) then begin str := IncludeTrailingPathDelimiter(str); // ensures that trailing backslash present for proper comparing in ReadDirectory method FExclFolders.AddValue(str); end; end; var startItemRef := AddFullPath(Volume); Volume := '\\?\' + Volume; Result := ReadDirectory(Volume, startItemRef, True); // Volume looks like C:\ (includes trailing path delimiter - this is ok) FExecTime := GetTickCount - start; FIndexedDateTime := Now; end; { TError } procedure TError.SetMsg(msg: string); begin Assert(Length(msg) < 255); // one symbol is for zero terminator StrPCopy(ErrText, msg); end; function TError.GetMsg: string; begin Result := ErrText; end; procedure TError.Create(code: Integer; msg: string); begin Assert(Length(msg) < 255); // one symbol is for zero terminator ErrCode := code; StrPCopy(ErrText, msg); end; function TError.HasError; begin Result := ErrCode <> 0; end; function TError.IsImportant: Boolean; begin IsImportant := Important <> 0; end; type TCallback = function(progress: Integer): Integer; // function from MFT DLL {$IFDEF CPUX64} function ReadVolumeDirect(Volume: PChar; ExclFolders: PChar; var Cnt: Cardinal; var Data: Pointer; Callback: TCallback): TError; stdcall; external 'MFTReaderDLL_64.dll' name 'ReadVolume'; {$ELSE} {$IFDEF CPUX86} function ReadVolumeDirect(Volume: PChar; ExclFolders: PChar; var Cnt: Cardinal; var Data: Pointer; Callback: TCallback): TError; stdcall; external 'MFTReaderDLL.dll' name 'ReadVolume'; {$ELSE} {$MESSAGE ERROR 'Neither CPUX64 nor CPUX86 is defined.'} {$ENDIF} {$ENDIF} class function TVolumeCache.MFTCallbackFunc(progress: Integer): Integer; begin TLogger.InfoFmt('Callback called. Progress: %d', [progress]); FVolCache.NotifyProgress(progress); Result := 1; // not used at the moment end; function TVolumeCache.ReadVolumeFast(Volume: string; ExclusionsList: TArray<string>): UInt64; var fileCache: PPFileLevel; i: Int64; cnt: Cardinal; str: string; err: TError; begin FVolCache := self; var start := GetTickCount; Result := 0; NotifyStart(Volume); Clear; FName := Volume; //TODO: may be list of exclusions should be Volume agnostic somehow??? // fill ExclFolders with values related to the specified Volume only for i := 1 to Length(ExclusionsList) do begin str := ExclusionsList[i - 1]; if str.StartsWith(Volume) then begin str := IncludeTrailingPathDelimiter(str); // ensure trailing backslash present for proper comparing in ReadDirectory method FExclFolders.AddValue(str); end; end; //Volume := ExcludeTrailingPathDelimiter(Volume); NotifyProgress(50); // call DLL function err := ReadVolumeDirect(PChar(Volume), PChar(Volume), cnt, Pointer(fileCache), MFTCallbackFunc); if err.HasError then begin TLogger.ErrorFmt('Error loading volume %s. Error code: %d. Error msg: %s.', [Volume, err.ErrCode, err.Msg]); // we are not calling NotifyError here because failover mechanizm // if ReadVolumeFast fails then app call ReadVolume and silently tries to read data by ReadVolume. //NotifyError(err); raise EInOutError.Create(err.ErrText, Volume); end else begin TLogger.Info('[ReadVolumeDirect] Finished successfully'); end; Deserialize(fileCache, cnt); // this call makes FIndexedDateTime = Now; NotifyFinish(); FExecTime := GetTickCount - start; end; { function TVolumeCache.ReadFileSystem(const Volumes: TArray<string>): UInt64; var i, cnt: Cardinal; begin Clear; // remove previous cache data cnt := Length(Volumes); for i := 0 to cnt - 1 do begin var startItemRef := AddFullPath(Volumes[i]); Result := ReadDirectory(Volumes[i], startItemRef, True); end; FModified := True; end; } function TVolumeCache.MakePathString(ref: TCacheItemRef):string; begin Result := MakePathString(ref.ItemLevel, ref.ItemIndex); end; function TVolumeCache.MakePathString(itemLevel, itemIndex: Cardinal): string; var k, ii: Integer; PathBuilder: THArrayString; begin PathBuilder := FMakePathCache.GetItem; // items are cleared below before returning to cache try var item := GetItem(ItemLevel, ItemIndex); if itemLevel = 0 then begin // we asked for root level item, return it and exit Result := item.FFileName; Exit; end; PathBuilder.AddValue(item.FFileName); var index := item.FParent; ii := Integer(ItemLevel - 1); // ii variable needs to be signed Integer while ii >= 0 do begin item := GetItem(Cardinal(ii), index); PathBuilder.AddValue(item.FFileName); //TODO: may be we need to change this algorithm somehow index := item.FParent; Dec(ii); end; Result := PathBuilder[PathBuilder.Count - 1]; for k := Integer(PathBuilder.Count) - 2 downto 0 do begin Result := Result + PathDelim + PathBuilder[Cardinal(k)]; end; finally PathBuilder.Clear; // memory is not de-allocated, only reset Count to zero. FMakePathCache.PutItem(PathBuilder); // return object back to cache end; end; procedure TVolumeCache.NotifyFinish; begin if Assigned(FProgressListeners) then for var i := 1 to FProgressListeners.Count do FProgressListeners[i - 1].Finish; end; function TVolumeCache.NotifyProgress(Progrs: Integer): Boolean; begin Result := False; // cancel by default if Assigned(FProgressListeners) then for var i := 1 to FProgressListeners.Count do if NOT FProgressListeners[i - 1].Progress(Progrs) then Exit; Result := True; // everything is ok return true end; procedure TVolumeCache.NotifyStart(Notes: string); begin if Assigned(FProgressListeners) then for var i := 1 to FProgressListeners.Count do FProgressListeners[i - 1].Start(Notes); end; procedure TVolumeCache.NotifyError(Error: TError); begin if Assigned(FProgressListeners) then for var i := 1 to FProgressListeners.Count do FProgressListeners[i - 1].ReportError(Error); end; procedure TVolumeCache.PrintLevelsStat(list: TStrings); begin list.Add(Format('Levels : %u', [FCacheData.Count])); var sum: Cardinal := 0; for var i: Cardinal := 0 to FCacheData.Count - 1 do begin var item := FCacheData[i]; list.Add(Format('Level %u : %u', [i, item.Count])); sum := sum + item.Count; end; list.Add(Format('SUMM of Levels : ', [sum])); end; procedure TVolumeCache.PrintAllItems(list: TStrings); var i, j: Cardinal; sitem: TCacheItem; pathStr: string; begin for i := 1 to FCacheData.Count do begin var level := FCacheData[Cardinal(i - 1)]; for j := 1 to level.Count do begin sitem := TCacheItem(level.GetAddr(j - 1)); if sitem.IsDirectory then begin pathStr := MakePathString(i - 1, j - 1); list.Add(Format('%s \t %u', [pathStr, sitem.FFileSize])); end; end; end; end; function TVolumeCache.GetStat(): TFileSystemStatRecord; var i,j: Cardinal; item: TCacheItem; countedItems: Cardinal; counted: Boolean; begin ZeroMemory(@Result, sizeof(Result)); if FCacheData.Count = 0 then Exit; //var totalItems: Cardinal := 0; countedItems := 0; for i := 1 to FCacheData.Count do begin var lv := FCacheData[i - 1]; // totalItems := totalItems + lv.Count; for j := 1 to lv.Count do begin item := TCacheItem(lv.GetAddr(j - 1)); Assert( ((item.FFileAttrs AND TEST_DIR_MASK) = 0) OR ((item.FFileAttrs AND TEST_DIR_MASK) = TEST_DIR_MASK) ); counted := False; if (item.FFileAttrs AND FILE_ATTRIBUTE_DIRECTORY) > 0 then begin Inc(Result.Stat[ftDir]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_ARCHIVE) > 0 then begin Inc(Result.Stat[ftArchive]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_READONLY) > 0 then begin Inc(Result.Stat[ftReadOnly]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_NORMAL) > 0 then begin Inc(Result.Stat[ftFile]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_HIDDEN) > 0 then begin Inc(Result.Stat[ftHidden]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_TEMPORARY) > 0 then begin Inc(Result.Stat[ftTemp]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_SYSTEM) > 0 then begin Inc(Result.Stat[ftSystem]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_DEVICE) > 0 then begin Inc(Result.Stat[ftDevice]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_REPARSE_POINT)> 0 then begin Inc(Result.Stat[ftSymbolic]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_COMPRESSED) > 0 then begin Inc(Result.Stat[ftCompressed]);counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_ENCRYPTED) > 0 then begin Inc(Result.Stat[ftEncrypted]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_OFFLINE) > 0 then begin Inc(Result.Stat[ftOffline]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_SPARSE_FILE) > 0 then begin Inc(Result.Stat[ftSparse]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_VIRTUAL) > 0 then begin Inc(Result.Stat[ftVirtual]); counted := True; end; if (item.FFileAttrs AND FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) > 0 then begin Inc(Result.Stat[ftNotIndexed]); counted := True; end; //if (item.FFileData.dwFileAttrs AND FILE_ATTRIBUTE_PINNED) > 0 then begin Inc(Result[ftPinned]); counted := true; end; if counted then Inc(countedItems) else raise Exception.Create('Uncounted file type encontered!'); //list.Add(Format('Missing file attribute : %s : %u', [item.FFileData.cFileName, item.FFileData.dwFileAttributes])); end; end; Result.Stat[ftAll] := countedItems; StatSort(Result); end; procedure TVolumeCache.StatSort(var Stat: TFileSystemStatRecord); var i, j: Cardinal; k, val, L, R: Cardinal; valInd: TFileTypes; begin for k := Low(Stat.Index) to High(Stat.Index) do Stat.Index[k] := TFileTypes(k); L := Low(Stat.Index); R := High(Stat.Index); for i := L + 1 to R do begin j := i; valInd := Stat.Index[i]; val := Stat.Stat[valInd]; while (j > L) AND (Stat.Stat[Stat.Index[j - 1]] < val) do begin // '<' means sorting in reverse order Stat.Index[j] := Stat.Index[j - 1]; Dec(j); end; Stat.Index[j] := valInd; end; end; procedure TVolumeCache.PrintStat(stat: TFileSystemStat; list: TStrings); begin list.Add(Format('Total number of files and dirs : %u', [stat[ftAll]])); // list.Add(Format('Total without dirs : %u', [totalItems - stat[ftDir]])); list.Add(Format('Directories : %u', [stat[ftDir]])); list.Add(Format('Read Only : %u', [stat[ftReadOnly]])); list.Add(Format('Archive : %u', [stat[ftArchive]])); list.Add(Format('Hidden : %u', [stat[ftHidden]])); list.Add(Format('Temporary : %u', [stat[ftTemp]])); list.Add(Format('System : %u', [stat[ftSystem]])); list.Add(Format('Devices : %u', [stat[ftDevice]])); list.Add(Format('Symbolic : %u', [stat[ftSymbolic]])); list.Add(Format('Compressed : %u', [stat[ftCompressed]])); list.Add(Format('Encrypted : %u', [stat[ftEncrypted]])); list.Add(Format('Offline : %u', [stat[ftOffline]])); list.Add(Format('Sparse : %u', [stat[ftSparse]])); list.Add(Format('Normal : %u', [stat[ftFile]])); list.Add(Format('Pinned : %u', [stat[ftPinned]])); list.Add(Format('NOT Indexed : %u', [stat[ftNotIndexed]])); //list.Add(Format('Remaining : %u', [totalItems - countedItems])); end; { TCache } procedure TCache.CheckHangingDirectories; var i, j: Integer; emptyDirs: THArrayG<string>; begin TLogger.Debug('EMPTY DIRS:'); for i := 1 to FVolumeData.Count do begin emptyDirs := FVolumeData.GetPair(i - 1).Second.CheckHangingDirectories; for j := 1 to emptyDirs.Count do // write empty dirs to log file TLogger.Debug(emptyDirs.GetValue(j - 1)); emptyDirs.Free; end; end; procedure TCache.CheckLevelsDataTsCorrect; var i: Integer; begin for i := 1 to FVolumeData.Count do FVolumeData.GetPair(i - 1).Second.CheckLevelsDataIsCorrect; end; procedure TCache.CheckThatParentIsDirectory; var i: Integer; begin for i := 1 to FVolumeData.Count do FVolumeData.GetPair(i - 1).Second.CheckThatParentIsDirectory; end; procedure TCache.CheckFileDatesAreCorrect; var i: Integer; begin for i := 1 to FVolumeData.Count do FVolumeData.GetPair(i - 1).Second.CheckFileDatesAreCorrect; end; procedure TCache.Clear(Volume: string); begin FVolumeData.GetValue(Volume).Free; FVolumeData.Delete(Volume); end; procedure TCache.Clear; var i: Cardinal; begin for i := 1 to FVolumeData.Count do FVolumeData.GetPair(i - 1).Second.Free; FVolumeData.Clear; FIndexFileSaveDate := 0; // reset index file date end; constructor TCache.Create; begin raise ENoConstructException.Create('TCache instance cannot be directly constructed.'); end; constructor TCache.CreatePrivate; begin FVolumeData := THash<string, TVolumeCache>.Create; FProgressListeners := THArrayG<IIndexingProgress>.Create; FIndexFileSaveDate := 0; // default value, because index file is not loaded yet end; procedure TCache.Deserialize(IStream: TStream); var i, VolumesCnt: Cardinal; vol: TVolumeCache; FileSignature: TIndexFileSignaure; FileVersion : TIndexFileSignaure; begin IStream.Read(FileSignature, sizeof(FileSignature)); IStream.Read(FileVersion, sizeof(FileVersion)); if NOT CompareMem(@FileSignature[0], @FILE_SIGNATURE[0], sizeof(FileSignature)) then raise EBadFileFormat.Create('Index file format is incorrect.'); //TODO: index file version must be checked another way. Index file version may be older than version of FinderX app. if NOT CompareMem(@FileVersion[0], @FILE_VERSION[0], sizeof(FileVersion)) then raise EBadFileFormat.Create('Index file format is incorrect.'); Clear; IStream.ReadData<TDateTime>(FIndexFileSaveDate); IStream.ReadData<Cardinal>(VolumesCnt); if VolumesCnt = 0 then Exit; //TODO: possibly we can do it more effective - do not delete TVolumeCache cashes but just clear them and preserve allocated memory for i := 1 to VolumesCnt do begin vol := TVolumeCache.Create; vol.Deserialize(IStream); FVolumeData.SetValue(vol.FName, vol); end; end; // loads nothing if file does not exist and does not report error, that's ok. procedure TCache.DeserializeFrom(const FileName: string); var msin: TMemoryStream; begin msin := TMemoryStream.Create; try if FileExists(fileName) then begin msin.LoadFromFile(FileName); Deserialize(msin); end else begin TLogger.WarnFmt('Index file "%s" is not found or not accessible.', [FileName]); end; finally msin.Free; end; end; procedure TCache.Serialize(OStream: TStream); var i: Cardinal; begin OStream.Write(FILE_SIGNATURE, sizeof(FILE_SIGNATURE)); OStream.Write(FILE_VERSION, sizeof(FILE_VERSION)); FIndexFileSaveDate := Now; OStream.WriteData<TDateTime>(FIndexFileSaveDate); OStream.WriteData<Cardinal>(FVolumeData.Count); for i := 1 to FVolumeData.Count do FVolumeData.GetPair(i - 1).Second.Serialize(OStream); end; procedure TCache.SerializeTo(const FileName: string); var mout: TMemoryStream; begin mout := TMemoryStream.Create; try Serialize(mout); mout.SaveToFile(FileName); finally mout.Free; end; end; destructor TCache.Destroy; begin Clear; FreeAndNil(FVolumeData); FreeAndNil(FProgressListeners); inherited; end; function TCache.VolumePresent(Volume: string): TVolumeCache; var p: ^TVolumeCache; begin Result := nil; p := FVolumeData.GetValuePointer(Volume); if Assigned(p) then Result := p^; end; function TCache.VolumesCount: Cardinal; begin Result := FVolumeData.Count; end; function TCache.GetExecData: TArray<TVolumeExecData>; var i: Cardinal; rec: TVolumeExecData; begin for i := 1 to FVolumeData.Count do begin var vol := FVolumeData.GetPair(i - 1).Second; rec.VolumeName := vol.VolName; rec.ExecTime := vol.ExecTime; rec.VolSize := vol.Size; rec.ItemsCount := vol.Count; Insert(rec, Result, Length(Result)); end; end; { function TCache.GetModified: Boolean; var i: Cardinal; begin Result := False; for i := 1 to FVolumeData.Count do begin Result := Result OR FVolumeData.GetPair(i - 1).Second.Modified; if Result then break; end; end; } function TCache.GetStat(Volume: string): TFileSystemStatRecord; begin Result := FVolumeData[Volume].GetStat; end; function TCache.GetVolume(Volume: string): TVolumeCache; begin Result := FVolumeData.GetValue(Volume); end; function TCache.GetVolumeNamesAsString: string; var i: Cardinal; begin for i := 1 to FVolumeData.Count do Result := Result + ' ' + FVolumeData.GetPair(i - 1).Second.VolName; end; function TCache.GetVolumes: TArray<string>; var i: Cardinal; begin for i := 1 to FVolumeData.Count do Insert(FVolumeData.GetPair(i - 1).Second.VolName, Result, Length(Result)); end; class destructor TCache.FreeInst; begin if Assigned(GInstance) then FreeAndNil(GInstance); if Assigned(GInstance2) then FreeAndNil(GInstance2); end; class procedure TCache.FreeInst2; begin if Assigned(GInstance2) then FreeAndNil(GInstance2); end; class function TCache.Instance: TCache; begin if NOT Assigned(GInstance) then GInstance := TCache.CreatePrivate; Result := GInstance; end; class function TCache.NewInstance: TCache; begin if NOT Assigned(GInstance2) then GInstance2 := TCache.CreatePrivate; Result := GInstance2; end; function TCache.Search(Filter: TSearchFilter; Callback: TFNCSearchResult): TSearchResult; var i: Cardinal; begin Result := srOK; for i := 1 to FVolumeData.Count do Result := FVolumeData.GetPair(i - 1).Second.Search(Filter, Callback); end; class function TCache.HasNewInstance: Boolean; begin Result := Assigned(GInstance2); end; class procedure TCache.Swap; begin if Assigned(GInstance) then GInstance.Free; GInstance := GInstance2; GInstance2 := nil; end; procedure TCache.AddProgressListener(listener: IIndexingProgress); begin if NOT Assigned(listener) then Exit; // check if listener has already added to the list if FProgressListeners.IndexOf(listener) = -1 then FProgressListeners.AddValue(listener); end; function TCache.GetOrCreateVolume(Volume: string): TVolumeCache; var pvol: ^TVolumeCache; begin pvol := FVolumeData.GetValuePointer(Volume); if Assigned(pvol) then begin Result := pvol^; end else begin Result := TVolumeCache.Create; FVolumeData[Volume] := Result; end; end; procedure TCache.ReadVolume(Volume: string; ExclusionsList: TArray<string>); var vol: TVolumeCache; begin vol := GetOrCreateVolume(Volume); vol.FProgressListeners := FProgressListeners; vol.ReadVolume(Volume, ExclusionsList); vol.FProgressListeners := nil; end; // tries to fast read NTFS volume. procedure TCache.ReadVolumeFast(Volume: string; ExclusionsList: TArray<string>); var vol: TVolumeCache; begin vol := GetOrCreateVolume(Volume); vol.FProgressListeners := FProgressListeners; // when ReadVolumeFast fails try to read volume using ReadVolume which is more reliable, but slower. try vol.ReadVolumeFast(Volume, ExclusionsList); except on E:Exception do vol.ReadVolume(Volume, ExclusionsList); end; vol.FProgressListeners := nil; end; { procedure TCache.ReadVolumesFast(Volumes: TArray<string>; ExclusionsList: TArray<string>); var i: Cardinal; vol: TVolumeCache; begin for i := Low(Volumes) to High(Volumes) do begin vol := GetOrCreateVolume(Volumes[i]); vol.ReadVolumeFast(Volumes[i], ExclusionsList); end; end; } procedure TCache.RemoveProgressListener(listener: IIndexingProgress); begin var index := FProgressListeners.IndexOf(listener); if index <> -1 then FProgressListeners.DeleteValue(Cardinal(index)); end; initialization {$IF sizeof(MFT_REF) <> 8} {$MESSAGE FATAL 'MFT_REC record size mismatch! Expected 8 bytes.'} {$ENDIF} {$IF sizeof(MFT_INDEX) <> sizeof(MFT_REF) } {$MESSAGE FATAL 'MFT_INDEX and MFT_REF records must have the same size.'} {$ENDIF} {$IF sizeof(NTFS_DUP_INFO) <> 56 } {$MESSAGE FATAL 'NTFS_DUP_INFO record size mismatch. Expected 56 bytes.'} {$ENDIF} {$IF sizeof(ATTR_FILE_NAME) <> 66} {$MESSAGE FATAL 'ATTR_FILE_NAME record size mismatch. Excpected 66 bytes.'} {$ENDIF} {$IF sizeof(CACHE_ITEM) <> 12 + sizeof(MFT_REF) + sizeof(ATTR_FILE_NAME) } {$MESSAGE FATAL 'CACHE_ITEM record size mismatch. Expected 86 bytes.'} {$ENDIF} {$IFDEF CPUX86} {$IF sizeof(TFileLevel) <> 8} {$MESSAGE FATAL 'TFileLEvel record size mismatch. Excpected 8 bytes.'} {$ENDIF} {$ELSEIF defined(CPUX64) } {$IF sizeof(TFileLevel) <> 12} {$MESSAGE FATAL 'TFileLEvel record size mismatch. Excpected 12 bytes.'} {$ENDIF} {$ELSE} {$MESSAGE FATAL 'Neither CPUX86 nor CPUX64 are defined.'} {$ENDIF} { Assert(sizeof(MFT_REF) = $8); Assert(sizeof(MFT_INDEX) = sizeof(MFT_REF)); Assert(sizeof(NTFS_DUP_INFO) = $38); Assert(sizeof(ATTR_FILE_NAME) = $42); Assert(sizeof(CACHE_ITEM) = $C + sizeof(MFT_REF) + sizeof(ATTR_FILE_NAME)); Assert(sizeof(TFileLEvel) = $C); } end.