Отслеживание изменений в папках и файлах

Наверное, опрашивать интересующие нас файлы или папки с максимально возможной скоростью - не самое умное решение. .

Выход - ожидать уведомления от файловой системы о том, что в ней что-то происходит. Сначала при помощи FindFirstChangeNotification, сформировать объект отслеживания, получить его хэндл, и дальше просто ожидать событий при помощи WaitForSingleObject.

Конечно, чтобы не мешать этими циклами основному приложению, отслеживание нужно запускать в отдельном потоке. 

А чтобы отслеживать сразу несколько объектов, нужна возможность запускать параллельно произвольное количество отслеживаний.

Получать уведомления хотелось бы в какой-нибудь привычный обработчик событий.

Ну и, конечно, всё это должно быть потокобезопасно, чтобы на ходу добавлять или исключать расположения отслеживания не боясь вызвать крах программы.

Слово за слово, получается какой-то такой класс:

unit uFileSystemMonitor;

interface

uses
     System.Classes
    ,Windows
    ,SyncObjs
    ,Generics.Collections
    ,System.SysUtils
    ;

type

     TFileMonitorEvent = procedure(aPath: string) of object;

     /// <summary>
     ///   Отдельный поток для отслеживания одного объекта файловой системы
     /// </summary>
     TTrackedObject = class(TThread)
                      private
                        FPath         : string;
                        FInterval     : integer;
                        FSubtree      : Boolean;
                        FNotifyFilter : Cardinal;

                        FChange : TFileMonitorEvent;
                      protected
                        procedure Execute; override;

                        function  GetInterval: integer;
                        procedure SetInterval(aNewInterval: integer);
                        function  GetPath: string;

                        procedure DoChange;
                        procedure SetOnChange(NewEventHandler: TFileMonitorEvent);
                      public
                        /// <summary>
                        ///   Конструктор
                        /// </summary>
                        /// <param name="aPath">
                        ///   Путь до объекта. Может быть файлом или папкой.
                        /// </param>
                        /// <param name="aSubTree">
                        ///   Нужно ли отслеживать вложенные объекты.
                        /// </param>
                        /// <param name="aNotifyFilter">
                        ///   Маска событий, которые нужно отслеживать.
                        /// </param>
                        /// <param name="aInterval">
                        ///   Временной интервал в милисекундах, в течение
                        ///   которого класс обращается за внешними командами.
                        ///   Маленькие значения приводят к повышенной нагрузке
                        ///   на процессор, большие - к медленной реакции на
                        ///   изменение свойст и команду завершения.
                        /// </param>
                        constructor Create(const aPath: string;
                                           aSubTree: Boolean;
                                           aNotifyFilter: Cardinal;
                                           aInterval: integer);

                        /// <summary>
                        ///   Текущее значение интервала опроса внешних команд
                        ///   в милисекундах.
                        /// </summary>
                        property Interval : integer read GetInterval write SetInterval;
                        /// <summary>
                        ///   Путь к отслеживаемому объекту.
                        /// </summary>
                        /// <remarks>
                        ///   Read only. Переключиться на отслеживание другой
                        ///   папки нельзя. Нужно создать новый экземпляр.
                        /// </remarks>
                        property Path: string read GetPath;

                        /// <summary>
                        ///   Обработчик события изменения в отслеживаемом
                        ///   объекте.
                        /// </summary>
                        property OnChange: TFileMonitorEvent read FChange write SetOnChange;
                      end;


     /// <summary>
     ///   Класс для мониторинга объектов файловой системы. Папок и файлов.
     /// </summary>
     TFileSystemMonitor = class
                          private
                            FItems  : TList<TTrackedObject>;
                            FChange : TFileMonitorEvent;
                          protected
                            function GetTrackedPath(Index: integer): string;
                            function GetCount: integer;
                            procedure DoChange(const aPath: string);
                            procedure SetOnChange(NewEventHandler: TFileMonitorEvent);
                          public
                            constructor Create;
                            destructor  Destroy; override;

                            /// <summary>
                            ///   Добавить новый объект отслеживания.
                            /// </summary>
                            /// <param name="aPath">
                            ///   Путь до объекта, файла или папки.
                            /// </param>
                            /// <param name="aSubTree">
                            ///   Нужно ли отслеживать изменения во вложенных
                            ///   объектах.
                            /// </param>
                            /// <param name="aNotifyFilter">
                            ///   Маска отслеживаемых событий. По умолчанию:
                            ///   FILE_NOTIFY_CHANGE_FILE_NAME <br />+
                            ///   FILE_NOTIFY_CHANGE_ATTRIBUTES
                            /// </param>
                            /// <param name="aInterval">
                            ///   Интервал опроса внешних команд в
                            ///   милисекундах. По умолчанию: 1000
                            /// </param>
                            /// <remarks>
                            ///   Если объект не существует, он не будет
                            ///   добавлен в список отслеживаемых. Ни каких
                            ///   ошибок при этом не возникнет.
                            /// </remarks>
                            procedure AddTrackedObject(const aPath: string;
                                                       aSubTree: boolean;
                                                       aNotifyFilter: Cardinal =  FILE_NOTIFY_CHANGE_FILE_NAME
                                                                                + FILE_NOTIFY_CHANGE_ATTRIBUTES;
                                                       aInterval: integer = 1000);
                            /// <summary>
                            ///   Прекратить отслеживание объекта по его имени.
                            /// </summary>
                            /// <param name="Path">
                            ///   Путь к объекту.
                            /// </param>
                            /// <remarks>
                            ///   Функция не чувствительна к регистру букв.
                            /// </remarks>
                            procedure DeleteTrackedObject(Path: string); overload;
                            /// <summary>
                            ///   Прекратить отслеживание объекта по его
                            ///   номеру.
                            /// </summary>
                            /// <param name="Index">
                            ///   Индекс объекта.
                            /// </param>
                            procedure DeleteTrackedObject(Index: integer); overload;

                            /// <summary>
                            ///   Массив отслеживаемых расположений в файловой
                            ///   системе.
                            /// </summary>
                            property TrackedPath[Index: integer]: string read GetTrackedPath;
                            /// <summary>
                            ///   Количество отслеживаемых расположений.
                            /// </summary>
                            property Count: integer read GetCount;

                            /// <summary>
                            ///   Обработчик событий изменения в отслеживаемых
                            ///   расположениях.
                            /// </summary>
                            property OnChange: TFileMonitorEvent read FChange write SetOnChange;
                          end;

implementation

{ TFileSystemMonitor }

procedure TFileSystemMonitor.AddTrackedObject(const aPath: string;
                                              aSubTree: boolean;
                                              aNotifyFilter: Cardinal =  FILE_NOTIFY_CHANGE_FILE_NAME
                                                                       + FILE_NOTIFY_CHANGE_ATTRIBUTES;
                                              aInterval: integer = 1000);
begin
  if (FileExists(aPath)) or (DirectoryExists(aPath)) then
  begin
    FItems.Add(TTrackedObject.Create(aPath, aSubTree, aNotifyFilter, aInterval));
    FItems.Last.OnChange := OnChange;
    FItems.Last.Start;
  end;
end;

constructor TFileSystemMonitor.Create;
begin
  FItems := TList<TTrackedObject>.Create;
end;

procedure TFileSystemMonitor.DeleteTrackedObject(Path: string);
var
  i: Integer;
begin
  for i := 0 to Pred(FItems.Count) do
  begin
    if LowerCase(FItems.Items[i].Path) = LowerCase(Path) then
    begin
      FItems.Items[i].Terminate;
      while (not(FItems.Items[i].Terminated)) and (not(FItems.Items[i].Finished)) do ; {-}
      FItems.Delete(i);
      Break;
    end;
  end;
end;

procedure TFileSystemMonitor.DeleteTrackedObject(Index: integer);
begin
  FItems.Items[Index].Terminate;
  while not(FItems.Items[Index].Terminated) do ; {-}
  FItems.Delete(Index);
end;

destructor TFileSystemMonitor.Destroy;
var
  i: Integer;
begin
  for i := 0 to Pred(FItems.Count) do
  begin
    FItems.Items[i].Terminate;
    while not(FItems.Items[i].Terminated) do ;  {-}
    FItems.Items[i].Free;
  end;
  FItems.Free;

  inherited;
end;

procedure TFileSystemMonitor.DoChange(const aPath: string);
begin
  TMonitor.Enter(Self);
  if Assigned(FChange) then
    FChange(aPath);
  TMonitor.Exit(Self);
end;

function TFileSystemMonitor.GetCount: integer;
begin
  result := FItems.Count;
end;


function TFileSystemMonitor.GetTrackedPath(Index: integer): string;
begin
  result := FItems[Index].Path;
end;


procedure TFileSystemMonitor.SetOnChange(NewEventHandler: TFileMonitorEvent);
var
  i: Integer;
begin
  TMonitor.Enter(Self);
  for i := 0 to Pred(FItems.Count) do
  begin
    FItems.Items[i].OnChange := NewEventHandler;
  end;
  TMonitor.Exit(Self);
end;


{ TTracketObject }

constructor TTrackedObject.Create(const aPath: string;
                                  aSubTree: Boolean;
                                  aNotifyFilter: Cardinal;
                                  aInterval: integer);
begin
  inherited Create(true);
  FInterval       := aInterval;
  FPath           := aPath;
  FNotifyFilter   := aNotifyFilter;
  FreeOnTerminate := false;
  FSubtree        := aSubTree;
end;

procedure TTrackedObject.DoChange;
begin
  TMonitor.Enter(Self);
  if Assigned(FChange) then
    FChange(Path);
  TMonitor.Exit(Self);
end;

procedure TTrackedObject.Execute;
var
  EventHandle: THandle;
begin
  //Создаем объект отслеживания
  EventHandle :=
    FindFirstChangeNotification(PChar(FPath),
                                FSubTree,
                                FNotifyFilter);

  if EventHandle <> INVALID_HANDLE_VALUE then
  begin
    try
      while not Terminated do // Крутимся, пока не придёт команда остановки.
      begin
        case WaitForSingleObject(EventHandle, Interval) of
          WAIT_FAILED  : Terminate;
          WAIT_OBJECT_0: Synchronize(DoChange); // Сообщаем об изменении.
        end;
        FindNextChangeNotification(EventHandle);
      end;
    finally
      FindCloseChangeNotification(EventHandle);
    end;
  end;
end;

function TTrackedObject.GetInterval: integer;
begin
  TMonitor.Enter(Self);
  result := FInterval;
  TMonitor.Exit(Self);
end;

function TTrackedObject.GetPath: string;
begin
  TMonitor.Enter(Self);
  result := FPath;
  TMonitor.Exit(Self);
end;

procedure TTrackedObject.SetInterval(aNewInterval: integer);
begin
  TMonitor.Enter(Self);
  FInterval := aNewInterval;
  TMonitor.Exit(Self);
end;

procedure TTrackedObject.SetOnChange(NewEventHandler: TFileMonitorEvent);
begin
  TMonitor.Enter(Self);
  FChange := NewEventHandler;
  TMonitor.Exit(Self);
end;


end.

Пример использования:

type
  TTestAreaMain = class(TForm)
    ...
    private
      FFileSystemMonitor : TFileSystemMonitor;

      procedure DirectoryChange(aPath: string);
    ...
  end;
...
procedure TTestAreaMain.Button1Click(Sender: TObject);
begin
  // Создаём монитор и начинаем отслеживание каталога.
  FFileSystemMonitor := TFileSystemMonitor.Create;
  FFileSystemMonitor.AddTrackedObject('C:\tmp', true, FILE_NOTIFY_CHANGE_LAST_ACCESS);
  FFileSystemMonitor.OnChange := DirectoryChange;
end;

procedure TTestAreaMain.DirectoryChange(aPath: string);
begin
  // Обработчик событий.
  // aPath - расположение, в котором произошло изменение.
end;
Метки: