Мониторинг изменений в директориях и файлах средствами Delphi. Часть #1.

Источник: webdelphi

При разработке приложений Delphi для синхронизации чего-либо, например, файлов на разных компьютерах, так или иначе приходится разрабатывать алгоритм с помощью которого можно однозначно определять какие из файлов необходимо удалить из определенной директории, какие переместить, переименовать и т.д. Подобные алгоритмы и примеры их использования на практике не являются редкостью - в Сети Вы можете найти массу самых различных вариаций Delphi-кода с помощью которого можно отследить изменения в директориях и файлах Windows.

Не так давно и мне довелось столкнуться с подобной задачей - отследить изменения в определенной директории и сформировать список заданий для синхронизации файлов с сервером. Так как до этого момента мне не доводилось разрабатывать подобные алгоритмы, то пришлось пошерстить просторы Интернета и собрать как можно больше информации на заданную тему. Ну, а результаты моих поисков я решил оформить в виде отдельной статьи в блоге. Итак, сегодняшняя тема - мониторинг изменений в директориях и файлам средствами Delphi.

Самым простым и легкодоступным даже для новичков в программировании способом слежения за изменениями в директории является работа по таймеру. Смысл работы заключается в том, что на старте работы программы создается список файлов и поддиректорий в целевой директории. Затем, в момент срабатывания таймера, создается новый список и сравнивается с предыдущим - определяется какие файлы были добавлены, какие удалены/перемещены и т.д. и по уже определенным изменениям проводятся операции синхронизации.  Как в данном случае определить, что, скажем, файл Test.txt был изменен? Например, можно рассчитывать каждый раз CRC файла и сравнивать эту сумму с предыдущим значением. Вот исходник функции с  www.delphisources.ru, для расчёта CRC файла:

function GetCheckSum(FileName: string): DWORD;
var
  F: file of DWORD;
  P: Pointer;
  Fsize: DWORD;
  Buffer: array[0..500] of DWORD;
begin
  FileMode := 0;
  AssignFile(F, FileName);
  Reset(F);
  Seek(F, FileSize(F) div 2);
  Fsize := FileSize(F) - 1 - FilePos(F);
  if Fsize > 500 then
    Fsize := 500;
  BlockRead(F, Buffer, Fsize);
  Close(F);
  P := @Buffer;
  asm
     xor eax, eax
     xor ecx, ecx
     mov edi , p
     @again:
       add eax, [edi + 4*ecx]
       inc ecx
       cmp ecx, fsize
     jl @again
     mov @result, eax
  end;
end;

Пример использования функции. Создадим новое приложение в Delphi со следующими компонентами на форме:



При открытии файла будем определять его размер и рассчитывать CRC с помощью приведенной выше функции:

uses IOUtils;
[...]
procedure TForm3.Button1Click(Sender: TObject);
begin
  if OpenDialog1.Execute then
    begin
      edFile.Text:=OpenDialog1.FileName;
      lbCRC.Caption:=IntToStr(GetCheckSum(OpenDialog1.FileName));
      lbSize.Caption:=IntToStr(TFile.OpenRead(OpenDialog1.FileName).Size);
    end;
end;

Теперь возьмем создадим текстовый файлик и запишем в него строку, скажем "Hello World!", сохраним его, запустим программу и рассчитаем CRC. Вот, что получилось в программе:


Теперь снова откроем файл и заменим заглавные буквы на прописные, т.е. строка примет вид "hello world!". Снова рассчитаем CRC:


Обратите внимание, что размер файла остался прежним, а содержимое файла изменилось. Это лишний пример того, что использование в качестве критерия изменения файла только его размера - это очень ненадежный вариант и, наверное, даже неправильный.

Что можно сказать по поводу предложенного выше варианта мониторинга изменения в директории с помощью таймера?

Достоинством этого метода можно назвать его простота. Не важно, какой таймер будет использоваться в работе - стандартный TTimer или собственноручно созданный высокоточный таймер. Повесить обработчик на срабатывание таймера сможет кто угодно. Но наряду с простотой этого варианта он также имеет и массу недостатков. И самый главный из недостатков - ненадежность.

Никто не даст Вам гарантий того, что заданный интервал срабатывания таймера будет достаточным для выполнения процедуры обработчика. Как говориться, компьютер пользователя - потёмки. Можно, конечно, задавать большой интервал времени и надеяться на то, что обработчик таймера отработает на 100%, но это всего-лишь "костыль", но никак не решение проблем надежности алгоритма.

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

И, поэтому, более рациональным способом мониторинга изменений в файлах и директориях является использование функций Windows. Здесь можно выделить два варианта работы:

  1. Мониторинг изменений в директории без вывода информации об изменениях, т.е. простая констатация факта - было изменение, а что именно было изменено не определяется. Для этого способа используется тройка функций: FindFirstChangeNotification FindNextChangeNotificationFindCloseChangeNotification.
  2. Мониторинг изменений в директории с выводом информации по измененным элементам. Для этого способа используется пара функций:  CreateFile  и  ReadDirectoryChangesW .
Оба этих способа в равной степени удобны, но какой из этих способов использовать в конкретной ситуации решать только Вам. Рассмотрим примеры использования функций Windows для мониторинга изменений в директориях.

Использование функций FindFirstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification

Прежде, чем приступим к изучению функций, создадим модуль-заготовку для дальнейшей работы. Следить за изменениями мы будем в потоке (TThread):

unit Monitor;
 
interface
 
uses Classes, Windows, SysUtils;
 
type
  TChangeMonitor = class(TThread)
    private
    public
    protected
      procedure Execute; override;
  end;
 
implementation
 
{ TChangeMonitor }
 
procedure TChangeMonitor.Execute;
begin
 {здесь будем проводить мониторинг}
end;
 
end.

Теперь рассмотрим назначение функций Windows.

FindFirstChangeNotification - создает дескриптор уведомления об изменениях и устанавливает начальные условия отправки уведомления. Функция возвращает дескриптор (THandle) либо INVALID_HANDLE_VALUE в случае ошибки:

FindFirstChangeNotification(lpPathName: PChar; bWatchSubtree: boolean; dwNotifyFilter:DWORD): THandle;
 

lpPathName: PChar  - полный путь к директории за которой проводится слежение. Значение этого параметра не может содержать относительный путь или пустую строку.
bWatchSubtree: boolean  -  True  - указывает на то, что также в результат мониторинга будут попадать изменения в поддиректориях.
dwNotifyFilter:DWORD  - набор флагов, определяющих настройки фильтра. Флаги могут быть следующими:

  • FILE_NOTIFY_CHANGE_FILE_NAME (0x00000001)  - любое изменение имени файла в каталоге или подкаталоге.  Изменения включают в себя переименование, создание или удаление файла.
  • FILE_NOTIFY_CHANGE_DIR_NAME (0x00000002)  - любое изменение имени директории в каталоге или подкаталоге.  Изменения включают в себя переименование, создание или удаление директории.
  • FILE_NOTIFY_CHANGE_ATTRIBUTES (0x00000004)  - любое изменение атрибутов в просматриваемой директории и поддиректориях.
  • FILE_NOTIFY_CHANGE_SIZE (0x00000008)  - изменение размера файла в директории или поддиректории. Изменение размера обнаруживается только когда файл записывается на диск.
  • FILE_NOTIFY_CHANGE_LAST_WRITE (0x00000010)  - изменение времени последней записи в файл.
  • FILE_NOTIFY_CHANGE_SECURITY (0x00000100)  - изменение параметров безопасности в каталоге или подкаталоге.

FindNextChangeNotification - указывает, чтобы операционная система вернула сигнал уведомления об изменении THandle  в следующий раз, когда обнаруживаются изменения, согласно фильтру, установленному функцией FindFirstChangeNotification .

FindNextChangeNotification(hChangeHandle: THandle):boolean;
 

hChangeHandle: THandle  - дескриптор, полученный с помощью функции  FindFirstChangeNotification .

FindCloseChangeNotification - останавливает мониторинг изменений в директории.

FindCloseChangeNotification(hChangeHandle: THandle):boolean;
 

hChangeHandle: THandle  - дескриптор, полученный с помощью функции  FindFirstChangeNotification .

Назначение функций теперь более или менее стали нам понятны - осталось закрепить полученные знания на опыте. Итак, прежде всего наш поток должен получать два значения: путь к директории за которой мы будем следить и флаг, указывающий следует ли мониторить подкаталоги. Пишем конструктор потока:

type
  TChangeMonitor = class(TThread)
    private
      FDirectory: string;
      FScanSubDirs: boolean;
    public
      constructor Create(ASuspended: boolean; ADirectory:string; AScanSubDirs: boolean);
    protected
      procedure Execute; override;
  end;
 
implementation
 
{ TChangeMonitor }
 
constructor TChangeMonitor.Create(ASuspended: boolean; ADirectory: string;
  AScanSubDirs: boolean);
begin
  inherited Create(ASuspended);
  FDirectory:=ADirectory;
  FScanSubDirs:=AScanSubDirs;
  FreeOnTerminate:=true;
end;

Теперь создадим следующий Execute:

procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
begin
  {получаем хэндл события}
  ChangeHandle:=FindFirstChangeNotification(PChar(FDirectory),
                                            FScanSubDirs,
                                            FILE_NOTIFY_CHANGE_FILE_NAME+ 
                                            FILE_NOTIFY_CHANGE_DIR_NAME+
                                            FILE_NOTIFY_CHANGE_SIZE
                                            );
 {Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
 Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);
 try
    {выполняем цикл пока}
    while not Terminated do
    begin
      case WaitForSingleObject(ChangeHandle,1000) of
        WAIT_FAILED: Terminate; {Ошибка, завершаем поток}
        WAIT_OBJECT_0: {Сообщаем об изменениях};
      end;
      FindNextChangeNotification(ChangeHandle);
    end;
  finally
    FindCloseChangeNotification(ChangeHandle);
  end;
end;

В приведенном выше обработчике Execute мы проводим мониторинг изменения имени файла/директории или размера файла. При этом мы ожидаем любого из заданных в фильтре событий и выводим сообщение. Кстати, создадим событие для вывода сообщения об изменениях, например, такое:

type
  TChangeMonitor = class(TThread)
    private
      [...]
      FOnChange   : TNotifyEvent;
      procedure DoChange;
    public
      [...]
      property OnChange   : TNotifyEvent read FOnChange write FOnChange;
    protected
      procedure Execute; override;
  end;
 
procedure TChangeMonitor.DoChange;
begin
  if Assigned(FOnChange) then
    OnChange(Self)
end;
 
procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
begin
[...]
     case WaitForSingleObject(ChangeHandle,INFINITE) of
        WAIT_FAILED: Terminate; //Ошибка, завершаем поток
        WAIT_OBJECT_0: Synchronize(DoChange);
      end;
[...]
end;

Теперь всё готово для проверки работоспособности нашего потока. Создадим новый проект Delphi и на главную форму положим следующие компоненты:


В uses подключим модуль с нашим потоком и объявим следующую переменную:

uses
  [...], Monitor;
 
type
  TForm3 = class(TForm)
    [...]
  private
    FChangeMonitor:TChangeMonitor;
  public
    { Public declarations }
  end;

Теперь напишем необходимые обработчики для событий:

procedure TForm3.Button1Click(Sender: TObject);
begin
  mmLog.Lines.Clear;
  {создаем поток}
  FChangeMonitor:=TChangeMonitor.Create(True,edPath.Text,CheckBox1.Checked);
  {определяем обработчик события}
  FChangeMonitor.OnChange:=OnChange;
  {запускаем поток на выполнение}
  FChangeMonitor.Start;
  Button1.Enabled:=False;
  Button2.Enabled:=True;
end;
 
procedure TForm3.Button2Click(Sender: TObject);
begin
  {останавливаем поток}
  FChangeMonitor.Terminate;
  Button1.Enabled:=True;
  Button2.Enabled:=False;
end;
 
procedure TForm3.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if Assigned(FChangeMonitor) then
    FChangeMonitor.Terminate;
end;
 
procedure TForm3.OnChange(Sender: TObject);
const
  cLogStr = '%s - Изменения в директории';
begin
  {выводим сообщение в лог}
  mmLog.Lines.Add(Format(cLogStr,[DateTimeToStr(Now)]))
end;

Запускаем программу, выбираем директорию за которой необходимо следить и нажимаем кнопку "Следить". Теперь попробуем скопировать/удалить какой-нибудь файл и увидим в Memo соответствующее сообщение. Можете накачать электронные книги бесплатно и слить в директорию сразу кучу текстовых файлов - поток корректно сообщит Вам об изменениях каталога ровно столько раз сколько файлов Вы сбросите в директорию.
Приведенный выше пример является, наверное, самым простым, когда мы отслеживаем всего лишь одно событие по которому просто констатируем факт - произошли изменения в директории. А какие изменения - об этом мы ничего не сообщаем. Мы даже не можем в этом случае сказать, что именно произошло. Можно слегка подкорректировать приведенный выше обработчик и, используя приведенные выше функции Windows определить различные события на каждый вариант изменений, используя при этом вместо функции WaitForSingleObject функцию WaitForMultipleObjects. Но об этом, а также об использовании методов  CreateFile  и  ReadDirectoryChangesW  мы поговорим в следующий раз.



Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=26374