快速确定文件夹内容是否已被修改

4

我需要确定哪些文件夹包含最近(在一定时间间隔内)修改过的文件。我注意到,只要包含的文件被修改,文件夹日期戳就会更新,但这种行为不会向上传播,即包含修改文件的文件夹的父文件夹的日期戳不会更新。

我可以利用这个行为,但我怀疑它取决于平台/文件系统/网络或本地驱动器等因素。我仍然想尽可能利用它,因此我需要一个布尔函数,如果运行我的应用程序的平台/磁盘支持此行为,则返回true。

我很乐意递归遍历整个树形结构。我想避免的是对每个文件夹中的每个文件执行FindFirst / FindNext操作,以查看是否有任何文件在(例如)最近一天内进行了修改 - 如果我可以避免对没有在过去一天内修改其日期戳的文件夹进行这样的操作,将节省大量时间。

4个回答

3

2

我为我的一个项目编写了这个目的的代码。它使用FindFirstChangeNotification 和 FindNextChangeNotification API 函数。 以下是代码 (我删除了一些特定于项目的部分):

/// <author> Ali Keshavarz </author>
/// <date> 2010/07/23 </date>

unit uFolderWatcherThread;

interface

uses
  SysUtils, Windows, Classes, Generics.Collections;

type
  TOnThreadFolderChange = procedure(Sender: TObject; PrevModificationTime, CurrModificationTime: TDateTime) of object;
  TOnThreadError = procedure(Sender: TObject; const Msg: string; IsFatal: Boolean) of object;

  TFolderWatcherThread = class(TThread)
  private
    class var TerminationEvent : THandle;
  private
    FPath : string;
    FPrevModificationTime : TDateTime;
    FLatestModification : TDateTime;
    FOnFolderChange : TOnThreadFolderChange;
    FOnError : TOnThreadError;
    procedure DoOnFolderChange;
    procedure DoOnError(const ErrorMsg: string; IsFatal: Boolean);
    procedure HandleException(E: Exception);
  protected
    procedure Execute; override;

  public
    constructor Create(const FolderPath: string;
                       OnFolderChangeHandler: TOnThreadFolderChange;
                       OnErrorHandler: TOnThreadError);
    destructor Destroy; override;
    class procedure PulseTerminationEvent;
    property Path: string read FPath;
    property OnFolderChange: TOnThreadFolderChange read FOnFolderChange write FOnFolderChange;
    property OnError: TOnThreadError read FOnError write FOnError;
  end;

  /// <summary>
  /// Provides a list container for TFolderWatcherThread instances.
  /// TFolderWatcherThreadList can own the objects, and terminate removed items
  ///  automatically. It also uses TFolderWatcherThread.TerminationEvent to unblock
  ///  waiting items if the thread is terminated but blocked by waiting on the
  ///  folder changes.
  /// </summary>
  TFolderWatcherThreadList = class(TObjectList<TFolderWatcherThread>)
  protected
    procedure Notify(const Value: TFolderWatcherThread; Action: TCollectionNotification); override;
  end;

implementation

{ TFolderWatcherThread }

constructor TFolderWatcherThread.Create(const FolderPath: string;
  OnFolderChangeHandler: TOnThreadFolderChange; OnErrorHandler: TOnThreadError);
begin
  inherited Create(True);
  FPath := FolderPath;
  FOnFolderChange := OnFolderChangeHandler;
  Start;
end;

destructor TFolderWatcherThread.Destroy;
begin
  inherited;
end;

procedure TFolderWatcherThread.DoOnFolderChange;
begin
  Queue(procedure
        begin
          if Assigned(FOnFolderChange) then
            FOnFolderChange(Self, FPrevModificationTime, FLatestModification);
        end);
end;

procedure TFolderWatcherThread.DoOnError(const ErrorMsg: string; IsFatal: Boolean);
begin
  Synchronize(procedure
              begin
                if Assigned(Self.FOnError) then
                  FOnError(Self,ErrorMsg,IsFatal);
              end);
end;

procedure TFolderWatcherThread.Execute;
var
  NotifierFielter : Cardinal;
  WaitResult : Cardinal;
  WaitHandles : array[0..1] of THandle;
begin
 try
    NotifierFielter := FILE_NOTIFY_CHANGE_DIR_NAME +
                       FILE_NOTIFY_CHANGE_LAST_WRITE +
                       FILE_NOTIFY_CHANGE_FILE_NAME +
                       FILE_NOTIFY_CHANGE_ATTRIBUTES +
                       FILE_NOTIFY_CHANGE_SIZE;
    WaitHandles[0] := FindFirstChangeNotification(PChar(FPath),True,NotifierFielter);
    if WaitHandles[0] = INVALID_HANDLE_VALUE then
      RaiseLastOSError;
    try
      WaitHandles[1] := TerminationEvent;
      while not Terminated do
      begin
        //If owner list has created an event, then wait for both handles;
        //otherwise, just wait for change notification handle.
        if WaitHandles[1] > 0 then
         //Wait for change notification in the folder, and event signaled by
         //TWatcherThreads (owner list).
          WaitResult := WaitForMultipleObjects(2,@WaitHandles,False,INFINITE)
        else
          //Wait just for change notification in the folder
          WaitResult := WaitForSingleObject(WaitHandles[0],INFINITE);

        case WaitResult of
          //If a change in the monitored folder occured
          WAIT_OBJECT_0 :
          begin
            // notifiy caller.
            FLatestModification := Now;
            DoOnFolderChange;
            FPrevModificationTime := FLatestModification;
          end;

          //If event handle is signaled, let the loop to iterate, and check
          //Terminated status.
          WAIT_OBJECT_0 + 1: Continue;
        end;
        //Continue folder change notification job
        if not FindNextChangeNotification(WaitHandles[0]) then
          RaiseLastOSError;
      end;
    finally
      FindCloseChangeNotification(WaitHandles[0]);
    end;  
  except
    on E: Exception do
      HandleException(E);
  end;
end;

procedure TFolderWatcherThread.HandleException(E: Exception);
begin
  if E is EExternal then
  begin
    DoOnError(E.Message,True);
    Terminate;
  end
  else
    DoOnError(E.Message,False);
end;

class procedure TFolderWatcherThread.PulseTerminationEvent;
begin
  /// All instances of TFolderChangeTracker which are waiting will be unblocked,
  ///  and blocked again immediately to check their Terminated property.
  ///  If an instance is terminated, then it will end its execution, and the rest
  ///  continue their work.
  PulseEvent(TerminationEvent);
end;


{ TFolderWatcherThreadList }

procedure TFolderWatcherThreadList.Notify(const Value: TFolderWatcherThread;
  Action: TCollectionNotification);
begin
  if OwnsObjects and (Action = cnRemoved) then
  begin
    /// If the thread is running, terminate it, before freeing it.
    Value.Terminate;
    /// Pulse global termination event to all TFolderWatcherThread instances.
    TFolderWatcherThread.PulseTerminationEvent;
    Value.WaitFor;
  end;

  inherited;
end;

end.

这里提供了两个类;一个线程类用于监视文件夹的变化,如果检测到变化,它将通过OnFolderChange事件返回当前变化时间和上一次变化时间。还有一个列表类用于存储监视线程的列表。当从列表中删除线程时,该列表会自动终止每个线程的执行。希望这能对您有所帮助。

谢谢。在一般情况下,这可能不是我要找的(如果我想监视每个文件夹,我需要一个线程吗? - 在我的情况下,我将扫描整个磁盘以查找需要备份的文件)。但其中有一些通用代码,我肯定会在某个时候使用它们。 - rossmcm

2
到目前为止,已经发布的解决方案涉及获取通知并且对于此目的它们非常有效。如果你想研究过去,并查看上次更改时间,而不是实时监视它,那么就变得更加棘手了。我认为除了通过递归搜索文件夹树并检查日期戳之外,没有其他方法可以做到这一点。
编辑:针对OP的评论,似乎没有办法配置FindFirst/FindNext只命中目录而不是文件。但是通过使用这个筛选器,你可以跳过检查文件日期:(SearchRec.Attr和SysUtils.faDirectory <> 0)。这应该会加快速度。根本不要检查文件日期。你可能仍然需要扫描所有内容,因为Windows API没有提供任何我所知道的仅查询文件夹而不是文件的方式。

我很乐意通过树递归。我想避免的是在每个文件夹中为每个文件执行FindFirst/FindNext以查看是否在(比如)最近一天内进行了任何修改 - 如果我可以避免对未在最近一天内修改其日期时间戳的文件夹执行此操作,那么它将节省大量时间。 - rossmcm
@user8961,不用担心在文件上检查时间/大小/属性,CPU不会因为所有递归而晕眩。说真的,一旦任何给定文件夹的结构信息已经在内存中,时间惩罚是微不足道的(I/O惩罚已经付出了 - 这就是瓶颈所在)。此外,在目录中可能会发生日期和时间无法说明的事情。例如:一个人可能会将新文件复制到目录中(复制的文件保留创建和修改时间),而该操作可能不会更改目录上的修改时间。 - Cosmin Prund

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接