Delphi Store и получение TFileTime в виде строки без перехода на летнее время, проблемы с часовым поясом - PullRequest
0 голосов
/ 30 августа 2018

Мне нужно сохранить время последней записи файла в Windows в виде строки. Необходимо избегать перехода на летнее время или пользователей в других часовых поясах. Я думаю У меня есть решение, но кажется, что есть много проблем с датами.

У меня нет необходимости сравнивать дату с ранее сохраненной датой. Нужно только знать, изменилось ли оно.

Сохранение необработанной записи TFileTime (представленной как Int64) показалось наилучшим, поскольку именно это фактически используется Windows как 2 DWORDS. Delphi, кажется, хочет использовать TDateTime (FileAge) или целое число (FileSetDate). Кажется, что оба они переводят в местное время и используют только 32 бита против 64 бит.

Мне нужно отобразить «дружественную пользователю» строку, и я сделал строку отображения UTC, чтобы дважды проверить сохраненные значения. Для этого я использовал TDateTime для перевода из формата TFileTime.

Вспомогательный блок выглядит так:

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.

Вызывающее устройство выглядит так:

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;

Кажется, все работает хорошо. На данный момент меня не беспокоит изменение TFileTime с 64-битного. Надеюсь, я не пропустил ни одного сценария, который может вызвать проблемы.

Кроме того, мы надеемся, что кто-то еще может найти это полезным, если проблем не много.

Вопрос в том, будет ли этот код работать в любом часовом поясе или при переходе на летнее время? Я думаю, что этот код должен избегать проблемы "сохранить сейчас и загрузить после изменения летнего времени". Или проблема «Сохранить в моем часовом поясе, а затем загрузить другим пользователем в другом часовом поясе». Структура TFileTime должна остаться прежней, и моя программа обнаружит, что она не изменилась. Не уверен, что у меня есть все потенциальные проблемы в списке. В принципе, есть ли случай, когда сохранение строки и загрузка позже или в другом месте заставит мою программу подумать об изменении?

Спасибо.

1 Ответ

0 голосов
/ 29 октября 2018

По какой-то причине мое оригинальное исследование не показало эту статью: Структура MS FileTime

2 вещи:

  1. Слишком много об использовании 64-битных целых чисел, что особенно рекомендуется из-за проблем с выравниванием байтов. Никогда не было проблем, но все же изменилось из-за рекомендации.
  2. Разница во времени файлов NTFS и FAT не учитывалась. Теперь я проверяю только время файла до той минуты, которая соответствует моим целям.

В целях полноты приведем окончательный производственный код:

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.

Спасибо за помощь. Я отмечу это как ответ, главным образом потому, что это решение, используемое.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...