mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-27 21:14:01 +02:00
553 lines
16 KiB
ObjectPascal
553 lines
16 KiB
ObjectPascal
{
|
|
**********************************************************************
|
|
This file is part of LazUtils.
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
**********************************************************************
|
|
}
|
|
unit LazFileCache;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, AVL_Tree,
|
|
LazDbgLog, LazFileUtils;
|
|
|
|
type
|
|
TFileStateCacheItemFlag = (
|
|
fsciExists, // file or directory exists
|
|
fsciDirectory, // file exists and is directory
|
|
fsciReadable, // file is readable
|
|
fsciWritable, // file is writable
|
|
fsciDirectoryReadable, // file is directory and can be searched
|
|
fsciDirectoryWritable, // file is directory and new files can be created
|
|
fsciText, // file is text file (not binary)
|
|
fsciExecutable,// file is executable
|
|
fsciAge, // file age is valid
|
|
fsciPhysical // physical filename is valid
|
|
);
|
|
TFileStateCacheItemFlags = set of TFileStateCacheItemFlag;
|
|
|
|
{ TFileStateCacheItem }
|
|
|
|
TFileStateCacheItem = class
|
|
private
|
|
FAge: longint;
|
|
FFilename: string;
|
|
FFlags: TFileStateCacheItemFlags;
|
|
FPhysicalFilename: string;
|
|
FTestedFlags: TFileStateCacheItemFlags;
|
|
FTimeStamp: int64;
|
|
public
|
|
constructor Create(const TheFilename: string; NewTimeStamp: int64);
|
|
function CalcMemSize: PtrUint;
|
|
public
|
|
property Filename: string read FFilename;
|
|
property PhysicalFilename: string read FPhysicalFilename;
|
|
property Flags: TFileStateCacheItemFlags read FFlags;
|
|
property TestedFlags: TFileStateCacheItemFlags read FTestedFlags;
|
|
property TimeStamp: int64 read FTimeStamp;
|
|
property Age: longint read FAge;
|
|
end;
|
|
|
|
TOnChangeFileStateTimeStamp = procedure(Sender: TObject;
|
|
const AFilename: string) of object;
|
|
|
|
{ TFileStateCache }
|
|
|
|
TFileStateCache = class
|
|
private
|
|
FFiles: TAVLTree; // tree of TFileStateCacheItem
|
|
FTimeStamp: int64;
|
|
FLockCount: integer;
|
|
FChangeTimeStampHandler: array of TOnChangeFileStateTimeStamp;
|
|
procedure SetFlag(AFile: TFileStateCacheItem;
|
|
AFlag: TFileStateCacheItemFlag; NewValue: boolean);
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
procedure Lock;
|
|
procedure Unlock;
|
|
function Locked: boolean;
|
|
procedure IncreaseTimeStamp(const AFilename: string);
|
|
function FileExistsCached(const AFilename: string): boolean;
|
|
function DirPathExistsCached(const AFilename: string): boolean;
|
|
function DirectoryIsWritableCached(const DirectoryName: string): boolean;
|
|
function FileIsExecutableCached(const AFilename: string): boolean;
|
|
function FileIsReadableCached(const AFilename: string): boolean;
|
|
function FileIsWritableCached(const AFilename: string): boolean;
|
|
function FileIsTextCached(const AFilename: string): boolean;
|
|
function FileAgeCached(const AFileName: string): Longint;
|
|
function GetPhysicalFilenameCached(const AFileName: string; {%H-}EmptyOnError: boolean): string;
|
|
function FindFile(const Filename: string;
|
|
CreateIfNotExists: boolean): TFileStateCacheItem;
|
|
function Check(const Filename: string; AFlag: TFileStateCacheItemFlag;
|
|
out AFile: TFileStateCacheItem; var FlagIsSet: boolean): boolean;
|
|
procedure AddChangeTimeStampHandler(const Handler: TOnChangeFileStateTimeStamp);
|
|
procedure RemoveChangeTimeStampHandler(const Handler: TOnChangeFileStateTimeStamp);
|
|
function CalcMemSize: PtrUint;
|
|
public
|
|
property TimeStamp: int64 read FTimeStamp;
|
|
end;
|
|
|
|
var
|
|
FileStateCache: TFileStateCache = nil;
|
|
|
|
function FileExistsCached(const AFilename: string): boolean;
|
|
function DirPathExistsCached(const AFilename: string): boolean;
|
|
function DirectoryIsWritableCached(const ADirectoryName: string): boolean;
|
|
function FileIsExecutableCached(const AFilename: string): boolean;
|
|
function FileIsReadableCached(const AFilename: string): boolean;
|
|
function FileIsWritableCached(const AFilename: string): boolean;
|
|
function FileIsTextCached(const AFilename: string): boolean;
|
|
function FileAgeCached(const AFileName: string): Longint;
|
|
function GetPhysicalFilenameCached(const AFilename: string; EmptyOnError: boolean): string;
|
|
|
|
procedure InvalidateFileStateCache(const Filename: string = ''); inline;
|
|
function CompareFileStateItems(Data1, Data2: Pointer): integer;
|
|
function CompareFilenameWithFileStateCacheItem(Key, Data: Pointer): integer;
|
|
|
|
const
|
|
LUInvalidChangeStamp = Low(integer);
|
|
LUInvalidChangeStamp64 = Low(int64); // using a value outside integer to spot wrong types early
|
|
procedure LUIncreaseChangeStamp(var ChangeStamp: integer); inline;
|
|
procedure LUIncreaseChangeStamp64(var ChangeStamp: int64); inline;
|
|
|
|
type
|
|
TOnFileExistsCached = function(Filename: string): boolean of object;
|
|
TOnFileAgeCached = function(Filename: string): longint of object;
|
|
var
|
|
OnFileExistsCached: TOnFileExistsCached = nil;
|
|
OnFileAgeCached: TOnFileAgeCached = nil;
|
|
|
|
implementation
|
|
|
|
|
|
function FileExistsCached(const AFilename: string): boolean;
|
|
begin
|
|
if OnFileExistsCached<>nil then
|
|
Result:=OnFileExistsCached(AFilename)
|
|
else if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileExistsCached(AFilename)
|
|
else
|
|
Result:=FileExistsUTF8(AFilename);
|
|
end;
|
|
|
|
function DirPathExistsCached(const AFilename: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.DirPathExistsCached(AFilename)
|
|
else
|
|
Result:=DirPathExists(AFilename);
|
|
end;
|
|
|
|
function DirectoryIsWritableCached(const ADirectoryName: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.DirectoryIsWritableCached(ADirectoryName)
|
|
else
|
|
Result:=DirectoryIsWritable(ADirectoryName);
|
|
end;
|
|
|
|
function FileIsExecutableCached(const AFilename: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileIsExecutableCached(AFilename)
|
|
else
|
|
Result:=FileIsExecutable(AFilename);
|
|
end;
|
|
|
|
function FileIsReadableCached(const AFilename: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileIsReadableCached(AFilename)
|
|
else
|
|
Result:=FileIsReadable(AFilename);
|
|
end;
|
|
|
|
function FileIsWritableCached(const AFilename: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileIsWritableCached(AFilename)
|
|
else
|
|
Result:=FileIsWritable(AFilename);
|
|
end;
|
|
|
|
function FileIsTextCached(const AFilename: string): boolean;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileIsTextCached(AFilename)
|
|
else
|
|
Result:=FileIsText(AFilename);
|
|
end;
|
|
|
|
function FileAgeCached(const AFileName: string): Longint;
|
|
begin
|
|
if OnFileAgeCached<>nil then
|
|
Result:=OnFileAgeCached(AFilename)
|
|
else if FileStateCache<>nil then
|
|
Result:=FileStateCache.FileAgeCached(AFilename)
|
|
else
|
|
Result:=FileAgeUTF8(AFileName);
|
|
end;
|
|
|
|
function GetPhysicalFilenameCached(const AFilename: string;
|
|
EmptyOnError: boolean): string;
|
|
var
|
|
OnError: TPhysicalFilenameOnError;
|
|
begin
|
|
if FileStateCache<>nil then
|
|
Result:=FileStateCache.GetPhysicalFilenameCached(AFilename,EmptyOnError)
|
|
else begin
|
|
if EmptyOnError then
|
|
OnError:=pfeEmpty
|
|
else
|
|
OnError:=pfeOriginal;
|
|
Result:=GetPhysicalFilename(AFilename,OnError);
|
|
end;
|
|
end;
|
|
|
|
procedure InvalidateFileStateCache(const Filename: string);
|
|
begin
|
|
FileStateCache.IncreaseTimeStamp(Filename);
|
|
end;
|
|
|
|
function CompareFileStateItems(Data1, Data2: Pointer): integer;
|
|
begin
|
|
Result:=CompareFilenames(TFileStateCacheItem(Data1).FFilename,
|
|
TFileStateCacheItem(Data2).FFilename);
|
|
end;
|
|
|
|
function CompareFilenameWithFileStateCacheItem(Key, Data: Pointer): integer;
|
|
begin
|
|
Result:=CompareFilenames(AnsiString(Key),TFileStateCacheItem(Data).FFilename);
|
|
//debugln('CompareFilenameWithFileStateCacheItem Key=',AnsiString(Key),' Data=',TFileStateCacheItem(Data).FFilename,' Result=',dbgs(Result));
|
|
end;
|
|
|
|
procedure LUIncreaseChangeStamp(var ChangeStamp: integer);
|
|
begin
|
|
if ChangeStamp<High(ChangeStamp) then
|
|
inc(ChangeStamp)
|
|
else
|
|
ChangeStamp:=LUInvalidChangeStamp+1;
|
|
end;
|
|
|
|
procedure LUIncreaseChangeStamp64(var ChangeStamp: int64);
|
|
begin
|
|
if ChangeStamp<High(ChangeStamp) then
|
|
inc(ChangeStamp)
|
|
else
|
|
ChangeStamp:=LUInvalidChangeStamp64+1;
|
|
end;
|
|
|
|
{ TFileStateCacheItem }
|
|
|
|
constructor TFileStateCacheItem.Create(const TheFilename: string;
|
|
NewTimeStamp: int64);
|
|
begin
|
|
FFilename:=TheFilename;
|
|
FTimeStamp:=NewTimeStamp;
|
|
end;
|
|
|
|
function TFileStateCacheItem.CalcMemSize: PtrUint;
|
|
begin
|
|
Result:=PtrUInt(InstanceSize)
|
|
+MemSizeString(FFilename)
|
|
+MemSizeString(FPhysicalFilename);
|
|
end;
|
|
|
|
{ TFileStateCache }
|
|
|
|
procedure TFileStateCache.SetFlag(AFile: TFileStateCacheItem;
|
|
AFlag: TFileStateCacheItemFlag; NewValue: boolean);
|
|
begin
|
|
if AFile.FTimeStamp<>FTimeStamp then begin
|
|
AFile.FTestedFlags:=[];
|
|
AFile.FTimeStamp:=FTimeStamp;
|
|
end;
|
|
Include(AFile.FTestedFlags,AFlag);
|
|
if NewValue then
|
|
Include(AFile.FFlags,AFlag)
|
|
else
|
|
Exclude(AFile.FFlags,AFlag);
|
|
//WriteStr(s, AFlag);
|
|
//debugln('TFileStateCache.SetFlag AFile.Filename=',AFile.Filename,' ',s,'=',dbgs(AFlag in AFile.FFlags),' Valid=',dbgs(AFlag in AFile.FTestedFlags));
|
|
end;
|
|
|
|
constructor TFileStateCache.Create;
|
|
begin
|
|
FFiles:=TAVLTree.Create(@CompareFileStateItems);
|
|
LUIncreaseChangeStamp64(FTimeStamp); // one higher than default for new files
|
|
end;
|
|
|
|
destructor TFileStateCache.Destroy;
|
|
begin
|
|
FFiles.FreeAndClear;
|
|
FFiles.Free;
|
|
SetLength(FChangeTimeStampHandler,0);
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TFileStateCache.Lock;
|
|
begin
|
|
inc(FLockCount);
|
|
end;
|
|
|
|
procedure TFileStateCache.Unlock;
|
|
|
|
procedure RaiseTooManyUnlocks;
|
|
begin
|
|
raise Exception.Create('TFileStateCache.Unlock');
|
|
end;
|
|
|
|
begin
|
|
if FLockCount<=0 then RaiseTooManyUnlocks;
|
|
dec(FLockCount);
|
|
end;
|
|
|
|
function TFileStateCache.Locked: boolean;
|
|
begin
|
|
Result:=FLockCount>0;
|
|
end;
|
|
|
|
procedure TFileStateCache.IncreaseTimeStamp(const AFilename: string);
|
|
var
|
|
i: Integer;
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
if Self=nil then exit;
|
|
if AFilename='' then begin
|
|
// invalidate all
|
|
LUIncreaseChangeStamp64(FTimeStamp);
|
|
end else begin
|
|
// invalidate single file
|
|
AFile:=FindFile(AFilename,false);
|
|
if AFile<>nil then
|
|
AFile.FTestedFlags:=[];
|
|
end;
|
|
for i:=0 to length(FChangeTimeStampHandler)-1 do
|
|
FChangeTimeStampHandler[i](Self,AFilename);
|
|
//debugln('TFileStateCache.IncreaseTimeStamp FTimeStamp=',dbgs(FTimeStamp));
|
|
end;
|
|
|
|
function TFileStateCache.FileExistsCached(const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciExists,AFile,Result) then exit;
|
|
Result:=FileExistsUTF8(AFile.Filename);
|
|
SetFlag(AFile,fsciExists,Result);
|
|
{if not Check(Filename,fsciExists,AFile,Result) then begin
|
|
WriteDebugReport;
|
|
raise Exception.Create('');
|
|
end;}
|
|
end;
|
|
|
|
function TFileStateCache.DirPathExistsCached(const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciDirectory,AFile,Result) then exit;
|
|
Result:=DirPathExists(AFile.Filename);
|
|
SetFlag(AFile,fsciDirectory,Result);
|
|
end;
|
|
|
|
function TFileStateCache.DirectoryIsWritableCached(const DirectoryName: string
|
|
): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(DirectoryName,fsciDirectoryWritable,AFile,Result) then exit;
|
|
Result:=DirectoryIsWritable(AFile.Filename);
|
|
SetFlag(AFile,fsciDirectoryWritable,Result);
|
|
end;
|
|
|
|
function TFileStateCache.FileIsExecutableCached(
|
|
const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciExecutable,AFile,Result) then exit;
|
|
Result:=FileIsExecutable(AFile.Filename);
|
|
SetFlag(AFile,fsciExecutable,Result);
|
|
end;
|
|
|
|
function TFileStateCache.FileIsReadableCached(const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciReadable,AFile,Result) then exit;
|
|
Result:=FileIsReadable(AFile.Filename);
|
|
SetFlag(AFile,fsciReadable,Result);
|
|
end;
|
|
|
|
function TFileStateCache.FileIsWritableCached(const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciWritable,AFile,Result) then exit;
|
|
Result:=FileIsWritable(AFile.Filename);
|
|
SetFlag(AFile,fsciWritable,Result);
|
|
end;
|
|
|
|
function TFileStateCache.FileIsTextCached(const AFilename: string): boolean;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
begin
|
|
Result := False;
|
|
if Check(AFilename,fsciText,AFile,Result) then exit;
|
|
Result:=FileIsText(AFile.Filename);
|
|
SetFlag(AFile,fsciText,Result);
|
|
end;
|
|
|
|
function TFileStateCache.FileAgeCached(const AFileName: string): Longint;
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
Dummy: Boolean;
|
|
begin
|
|
Dummy := False;
|
|
if Check(AFilename,fsciAge,AFile,Dummy) then begin
|
|
Result:=AFile.Age;
|
|
exit;
|
|
end;
|
|
Result:=FileAge(AFile.Filename);
|
|
AFile.FAge:=Result;
|
|
Include(AFile.FTestedFlags,fsciAge);
|
|
end;
|
|
|
|
function TFileStateCache.GetPhysicalFilenameCached(const AFileName: string;
|
|
EmptyOnError: boolean): string;
|
|
{$IFDEF Unix}
|
|
var
|
|
AFile: TFileStateCacheItem;
|
|
Dummy: Boolean;
|
|
{$ENDIF}
|
|
begin
|
|
{$IFDEF Unix}
|
|
Dummy := False;
|
|
if Check(AFilename,fsciPhysical,AFile,Dummy) then begin
|
|
Result:=AFile.PhysicalFilename;
|
|
exit;
|
|
end;
|
|
Result:=ExtractFilePath(AFile.Filename);
|
|
if Result<>'' then begin
|
|
// use cache recursively for directory
|
|
if (Result='.') or (Result='..') or (Result='/') then begin
|
|
// no query required
|
|
end else begin
|
|
Result:=GetPhysicalFilenameCached(Result,true);
|
|
end;
|
|
if Result<>'' then begin
|
|
Result:=AppendPathDelim(Result)+ExtractFilename(AFile.Filename);
|
|
Result:=ReadAllLinks(Result,false);
|
|
end;
|
|
end else begin
|
|
// no path
|
|
Result:=ReadAllLinks(AFile.Filename,false);
|
|
end;
|
|
AFile.FPhysicalFilename:=Result;
|
|
Include(AFile.FTestedFlags,fsciPhysical);
|
|
if (Result='') and (not EmptyOnError) then
|
|
Result:=AFileName;
|
|
{$ELSE}
|
|
Result:=AFileName;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function TFileStateCache.FindFile(const Filename: string;
|
|
CreateIfNotExists: boolean): TFileStateCacheItem;
|
|
var
|
|
NormedFilename: String;
|
|
ANode: TAVLTreeNode;
|
|
begin
|
|
// make filename unique
|
|
NormedFilename:=ChompPathDelim(ResolveDots(Filename));
|
|
ANode:=FFiles.FindKey(Pointer(NormedFilename),
|
|
@CompareFilenameWithFileStateCacheItem);
|
|
if ANode<>nil then
|
|
Result:=TFileStateCacheItem(ANode.Data)
|
|
else if CreateIfNotExists then begin
|
|
Result:=TFileStateCacheItem.Create(NormedFilename,FTimeStamp);
|
|
FFiles.Add(Result);
|
|
if FFiles.FindKey(Pointer(NormedFilename),
|
|
@CompareFilenameWithFileStateCacheItem)=nil
|
|
then begin
|
|
//DebugLn(format('FileStateCache.FindFile: "%s"',[FileName]));
|
|
raise Exception.Create('');
|
|
end;
|
|
end else
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TFileStateCache.Check(const Filename: string;
|
|
AFlag: TFileStateCacheItemFlag; out AFile: TFileStateCacheItem;
|
|
var FlagIsSet: boolean): boolean;
|
|
begin
|
|
AFile:=FindFile(Filename,true);
|
|
if FTimeStamp=AFile.FTimeStamp then begin
|
|
Result:=AFlag in AFile.FTestedFlags;
|
|
FlagIsSet:=AFlag in AFile.FFlags;
|
|
end else begin
|
|
AFile.FTestedFlags:=[];
|
|
AFile.FTimeStamp:=FTimeStamp;
|
|
Result:=false;
|
|
FlagIsSet:=false;
|
|
end;
|
|
//WriteStr(s, AFlag);
|
|
//debugln('TFileStateCache.Check Filename=',Filename,' AFile.Filename=',AFile.Filename,' ',s,'=',dbgs(FlagIsSet),' Valid=',dbgs(Result));
|
|
end;
|
|
|
|
procedure TFileStateCache.AddChangeTimeStampHandler(
|
|
const Handler: TOnChangeFileStateTimeStamp);
|
|
begin
|
|
SetLength(FChangeTimeStampHandler,length(FChangeTimeStampHandler)+1);
|
|
FChangeTimeStampHandler[length(FChangeTimeStampHandler)-1]:=Handler;
|
|
end;
|
|
|
|
procedure TFileStateCache.RemoveChangeTimeStampHandler(
|
|
const Handler: TOnChangeFileStateTimeStamp);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
for i:=length(FChangeTimeStampHandler)-1 downto 0 do begin
|
|
if Handler=FChangeTimeStampHandler[i] then begin
|
|
if i<length(FChangeTimeStampHandler)-1 then
|
|
System.Move(FChangeTimeStampHandler[i+1],FChangeTimeStampHandler[i],
|
|
SizeOf(TNotifyEvent)*(length(FChangeTimeStampHandler)-i-1));
|
|
SetLength(FChangeTimeStampHandler,length(FChangeTimeStampHandler)-1);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TFileStateCache.CalcMemSize: PtrUint;
|
|
var
|
|
Node: TAVLTreeNode;
|
|
begin
|
|
Result:=PtrUInt(InstanceSize)
|
|
+PtrUInt(length(FChangeTimeStampHandler))*SizeOf(TNotifyEvent);
|
|
if FFiles<>nil then begin
|
|
inc(Result,PtrUInt(FFiles.InstanceSize)
|
|
+PtrUInt(FFiles.Count)*PtrUInt(TAVLTreeNode.InstanceSize));
|
|
Node:=FFiles.FindLowest;
|
|
while Node<>nil do begin
|
|
inc(Result,TFileStateCacheItem(Node.Data).CalcMemSize);
|
|
Node:=FFiles.FindSuccessor(Node);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
initialization
|
|
OnInvalidateFileStateCache:=@InvalidateFileStateCache;
|
|
|
|
end.
|
|
|