0
votes

I have a need to store the Last Write Access time of a file on Windows as a string. Need to avoid any Daylight Savings Time or users in a different time zone issues. I think I have a solution, but it seems there are many issues with dates.

I don't have any need to compare a date to previously stored date. Only need to know if it changed.

Storing the raw TFileTime record (represented as an Int64) seemed best as this is what is actually used by Windows as 2 DWORDS. Delphi seems to want to use TDateTime (FileAge) or an integer (FileSetDate). Both of these seem to translate to local times and only use 32 bits vs. 64 bits.

I do have a need to display a "user friendly" string and did a UTC display string to double check stored values. For these, I did use a TDateTime to translate out of the TFileTime format.

The helper unit looks like this:

unit FileTimeHelperUnt;

interface

uses
  Winapi.Windows, System.SysUtils;

type
  TFileTimeHelper = record helper for TFileTime
    function ToString: String; //Use to export TFileTime as Int64 String.
    function FromString( AString: String ): Boolean; //Use to restore 
TFileTime from Int64 String
    function GetLastWriteTime( AFilePathStr: String ): Boolean;
    function SetLastWriteTime( AFilePathStr: String ): Boolean;
    function UTCString: String;
    function UserFriendlyString: String; //Like Windows Explorer and Local.
  end;

implementation

{ TFileTimeHelper }

function TFileTimeHelper.ToString: String;
var
  TmpInt64: Int64 absolute Self;
begin
  Result := TmpInt64.ToString;
end;

function TFileTimeHelper.FromString(AString: String): Boolean;
begin
  Result := False;
  try
    Int64(Self) := StrToInt64( AString );
    Result := True;
  except on E: Exception do
  end;
end;

function TFileTimeHelper.GetLastWriteTime(AFilePathStr: String): Boolean;
var
  TmpSearchRec: TSearchRec;
begin
  Result := False;

  if FileExists( AFilePathStr )=False then
   Exit;

  if FindFirst( AFilePathStr, faAnyFile, TmpSearchRec )=0 then
  begin
    Self := TmpSearchRec.FindData.ftLastWriteTime;
    Result := True;
  end;
end;

function TFileTimeHelper.SetLastWriteTime(AFilePathStr: String): Boolean;
var
  TmpHandle: THandle;
begin
  Result := False;

  if FileExists( AFilePathStr )=False then
   Exit;

  try
    TmpHandle := FileOpen(AFilePathStr, fmOpenWrite);

    if TmpHandle = THandle(-1) then
     Exit;

    try
      SetFileTime(TmpHandle, nil, nil, @Self);
      Result := (GetLastError=0);
    finally
      FileClose( TmpHandle );
    end;
  except on E: Exception do
  end;
end;

function TFileTimeHelper.UTCString: String;
var
  TmpSystemTime: TSystemTime;
  TmpDateTime: TDateTime;
begin
  FileTimeToSystemTime( Self, TmpSystemTime );
  TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
  Result := DateTimeToStr( TmpDateTime );
end;

function TFileTimeHelper.UserFriendlyString: String;
var
  TmpSystemTime: TSystemTime;
  TmpLocalLastWriteFileTime: TFileTime;
  TmpDateTime: TDateTime;
begin
  FileTimeToLocalFileTime( Self, TmpLocalLastWriteFileTime );
  FileTimeToSystemTime( TmpLocalLastWriteFileTime, TmpSystemTime );
  TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
  Result := FormatDateTime( 'm/d/yyyy h:nn ampm', TmpDateTime );
end;

end.

The calling unit looks like this:

procedure TForm12.btnGetFileDate2Click(Sender: TObject);
var
  TmpFileTime: TFileTime;
begin
  TmpFileTime.GetLastWriteTime( 'File.txt' );
  edtFileDateTime.Text := TmpFileTime.ToString;
  edtLocalFileDateTime.Text := TmpFileTime.UserFriendlyString;
  edtUTCDateTime.Text := TmpFileTime.UTCString;
end;

procedure TForm12.btnSetFileDate2Click(Sender: TObject);
var
  TmpFileTime: TFileTime;
begin
  TmpFileTime.FromString( edtFileDateTime.Text );
  TmpFileTime.SetLastWriteTime( 'File.txt' );
end;

Everything seems to work well. I'm not worried at this point about TFileTime being changed from 64-bits. Hoping I didn't miss any scenarios which could cause problems.

Also, hopefully someone else may find this useful if there aren't many problems.

The question is: is this code going to run into any time zone or daylight savings time issues? I think this code should avoid a "save now and load after daylight savings time changes" problem. Or a "Save in my timezone and then gets loaded by someone else in another time zone" problem. The TFileTime structure should stay the same and my program will recognize it didn't change. Not certain I've got all of the potential problems listed. Basically, is there any case where storing the string and loading later or in a different place will make my program think there is a change?

Thanks.

1
"I have a need to store the Last Write Access time of a file on Windows as a string" - use a standard format, like ISO-8601. "Need to avoid any Daylight Savings Time or users in a different time zone issues" - store the time in UTC format, not in local format. The ftLastWriteTime value provided by FindFirst() is already in UTC.Remy Lebeau
I can't see the question. What is it?David Heffernan
Finding out that ftLastWriteTime is already in UTC was very helpful. I suspected it, but couldn't find any documentation explicitly stating it. On the ISO-8601 standard, I have no idea how to convert to that. I am comfortable with a nonstandard format mainly because it translates in and out of the TFileTime structure and produces the same dwLowDateTime and dwHighDateTime.DelphiGuy
There needs to be a question in the body of the post.David Heffernan
You may get ISO8601 formatted values from SOAP.XSBuiltIns.DateTimeToXMLTime. You may also be interested in System.SysUtils.FileDateToDateTime.Max

1 Answers

0
votes

For whatever reason, my original research didn't turn up this article:MS FileTime Structure

2 things:

  1. Too glib about using a single 64-bit integers which is specifically recommended against because of byte alignment issues. Never had issues, but still changed due to recommendation.
  2. The difference in NTFS and FAT file times were not accounted for. I now only check the file time down to the minute which fits my purposes.

In the interest of completeness, here is the final production code:

unit FileTimeHelperUnt;

interface

uses
  Winapi.Windows, System.SysUtils;

type
  TFileTimeCompare = ( ftNewer, ftOlder, ftEqual );

  TFileTimeHelper = record helper for TFileTime
    function ToString: String;
    function FromString( AString: String ): Boolean;
    function GetLastWriteTime( AFilePathStr: String ): Boolean;
    function SetLastWriteTime( AFilePathStr: String ): Boolean;
    function Compare( AFileTime: TFileTime ): TFileTimeCompare;
    function UTCString: String;
    function UserFriendlyString: String; //Like Windows Explorer and Local.
  end;

implementation

{ TFileTimeHelper }
function TFileTimeHelper.ToString: String;
begin
  Result := IntToStr( Integer(Self.dwLowDateTime) ) + ',' + IntToStr( Integer(Self.dwHighDateTime) );
end;

function TFileTimeHelper.FromString( AString: String ): Boolean;
var
  TmpLowDateTimeStr: String;
  TmpHighDateTimeStr: String;
  TmpPos: Integer;
begin
  Result := False;
  try
    if AString.IsEmpty then
    begin
      Exit;
    end;

    TmpPos := Pos( ',', AString );

    if TmpPos=0 then
    begin
      Exit;
    end;

    TmpLowDateTimeStr := Copy( AString, 1, Pos( ',', AString )-1 );
    TmpHighDateTimeStr := Copy( AString, Pos( ',', AString )+1, MaxInt );

    Self.dwLowDateTime := DWORD( StrToInt( TmpLowDateTimeStr ) );
    Self.dwHighDateTime := DWORD( StrToInt( TmpHighDateTimeStr ) );

    Result := True;
  except on E: Exception do
  end;
end;

function TFileTimeHelper.GetLastWriteTime(AFilePathStr: String): Boolean;
var
  TmpSearchRec: TSearchRec;
begin
  Result := False;

  if FileExists( AFilePathStr )=False then
   Exit;

  if FindFirst( AFilePathStr, faAnyFile, TmpSearchRec )=0 then
  begin
    try
      Self.dwLowDateTime := TmpSearchRec.FindData.ftLastWriteTime.dwLowDateTime;
      Self.dwHighDateTime := TmpSearchRec.FindData.ftLastWriteTime.dwHighDateTime;
      Result := True;
    finally
      FindClose( TmpSearchRec );
    end;
  end;
end;

function TFileTimeHelper.SetLastWriteTime(AFilePathStr: String): Boolean;
var
  TmpHandle: THandle;
begin
  Result := False;

  if FileExists( AFilePathStr )=False then
   Exit;

  try
    TmpHandle := FileOpen(AFilePathStr, fmOpenWrite);

    if TmpHandle = THandle(-1) then
     Exit;

    try
      SetFileTime(TmpHandle, nil, nil, @Self);
      Result := (GetLastError=0);
    finally
      FileClose( TmpHandle );
    end;
  except on E: Exception do
  end;
end;

//Given the imprecision of certain file systems, only compare to the minute.
function TFileTimeHelper.Compare( AFileTime: TFileTime ): TFileTimeCompare;
var
  TmpSelfSystemTime: TSystemTime;
  TmpArgSystemTime: TSystemTime;

  function CompareWord( A, B: Word ): TFileTimeCompare;
  begin
    if A = B then
      Result := ftEqual
    else if A < B then
      Result := ftNewer
    else
      Result := ftOlder;
  end;
begin
  Result := ftEqual;

  FileTimeToSystemTime( Self, TmpSelfSystemTime );
  FileTimeToSystemTime( AFileTime, TmpArgSystemTime );

  //Compare Year
  case CompareWord( TmpSelfSystemTime.wYear, TmpArgSystemTime.wYear ) of
    ftNewer: Exit( ftNewer );
    ftOlder: Exit( ftOlder );
  end;

  //Compare Month
  case CompareWord( TmpSelfSystemTime.wMonth, TmpArgSystemTime.wMonth ) of
    ftNewer: Exit( ftNewer );
    ftOlder: Exit( ftOlder );
  end;

  //Compare Day
  case CompareWord( TmpSelfSystemTime.wMonth, TmpArgSystemTime.wMonth ) of
    ftNewer: Exit( ftNewer );
    ftOlder: Exit( ftOlder );
  end;

  //Compare Hour
  case CompareWord( TmpSelfSystemTime.wHour, TmpArgSystemTime.wHour ) of
    ftNewer: Exit( ftNewer );
    ftOlder: Exit( ftOlder );
  end;

  //Compare Minute
  case CompareWord( TmpSelfSystemTime.wMinute, TmpArgSystemTime.wMinute ) of
    ftNewer: Exit( ftNewer );
    ftOlder: Exit( ftOlder );
  end;
end;

function TFileTimeHelper.UTCString: String;
var
  TmpSystemTime: TSystemTime;
  TmpDateTime: TDateTime;
begin
  FileTimeToSystemTime( Self, TmpSystemTime );
  TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
  Result := DateTimeToStr( TmpDateTime );
end;

function TFileTimeHelper.UserFriendlyString: String;
var
  TmpSystemTime: TSystemTime;
  TmpLocalLastWriteFileTime: TFileTime;
  TmpDateTime: TDateTime;
begin
  try
    FileTimeToLocalFileTime( Self, TmpLocalLastWriteFileTime );
    FileTimeToSystemTime( TmpLocalLastWriteFileTime, TmpSystemTime );
    TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
    Result := FormatDateTime( 'm/d/yyyy h:nn ampm', TmpDateTime );
  except on E: Exception do
    Result := 'Unknown.';
  end;
end;

end.

Thanks for the help. I will check this off as the answer mainly because it's the solution used.