unit DirectoryMonitor;

// Based on DirMon by phaeteon and LoLa

interface

uses
  Windows, Messages, SysUtils, Classes, CompThread;

type
  EDirectoryMonitorError = class(Exception);

  TFileChangedEvent = procedure(Sender: TObject; const FileName: string) of Object;
  TFileRenamedEvent = procedure(Sender: TObject; const FromFileName: string; const ToFileName: string) of Object;

  TDirectoryMonitor = class(TComponent)
  private
    FDirectoryHandle: THandle;
    FNotificationBuffer: array[0..4096] of Byte;
    FWatchThread: TCompThread;
    FWatchFilters: DWord;
    FOverlapped: TOverlapped;
    FPOverlapped: POverlapped;
    FBytesWritten: DWORD;
    FCompletionPort: THandle;
    FPath: string;
    FActive: Boolean;
    FOnCreated: TFileChangedEvent;
    FOnDeleted: TFileChangedEvent;
    FOnModified: TFileChangedEvent;
    FOnRenamed: TFileRenamedEvent;
    FWatchSubTree: Boolean;

    procedure SetActive(AActive: Boolean);
    procedure SetPath(aPath: string);

  protected
    procedure Start;
    procedure Stop;
    procedure DoCreated(Sender: TObject; FileName: string);
    procedure DoDeleted(Sender: TObject; FileName: string);
    procedure DoModified(Sender: TObject; FileName: string);
    procedure DoRenamed(Sender: TObject; FromFileName: string; ToFileName: string);

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

  published
    property Active: Boolean read FActive write SetActive;
    property Path: string read FPath write SetPath;
    property OnCreated: TFileChangedEvent read FOnCreated write FOnCreated;
    property OnDeleted: TFileChangedEvent read FOnDeleted write FOnDeleted;
    property OnModified: TFileChangedEvent read FOnModified write FOnModified;
    property OnRenamed: TFileRenamedEvent read FOnRenamed write FOnRenamed;
    property WatchSubtree: Boolean read FWatchSubTree write FWatchSubtree;
    property WatchFilters: DWord read FWatchFilters write FWatchFilters;
  end;

implementation

type
  // See Windows API help
  PFileNotifyInformation = ^TFileNotifyInformation;
  TFileNotifyInformation = record
    NextEntryOffset: DWord;
    Action: DWord;
    FileNameLength: DWord;
    FileName: array[0..0] of WideChar;
  end;

const
  FILE_LIST_DIRECTORY = $0001;

type
  TDirectoryMonitorThread = class(TCompThread)
  private
    FParent: TDirectoryMonitor;
    FRenamedFrom: string;
    procedure HandleEvent;

  protected
    procedure Execute; override;

  public
    constructor Create(AParent: TDirectoryMonitor);
  end;

constructor TDirectoryMonitorThread.Create(AParent: TDirectoryMonitor);
begin
  inherited Create(True);
  FreeOnTerminate := False;
  FParent := AParent;
end;

procedure TDirectoryMonitorThread.HandleEvent;
var
  FileOpNotification: PFileNotifyInformation;
  Offset: DWord;
  FileName: string;
begin
  FileOpNotification := PFileNotifyInformation(@FParent.FNotificationBuffer[0]);
  repeat
    Offset := FileOpNotification^.NextEntryOffset;
    FileName := WideCharLenToString(@(FileOpNotification^.FileName), FileOpNotification^.FileNameLength div SizeOf(Char));
    case FileOpNotification^.Action of
      1: FParent.DoCreated(FParent, FileName);
      2: FParent.DoDeleted(FParent, FileName);
      3: FParent.DoModified(FParent, FileName);
      4: FRenamedFrom := FileName;
      5: FParent.DoRenamed(FParent, FRenamedFrom, FileName);
    end;
    FileOpNotification := PFileNotifyInformation(IntPtr(FileOpNotification) + IntPtr(Offset));
  until (Offset = 0);
end;

procedure TDirectoryMonitorThread.Execute;
var
  NumBytes: DWord;
  CompletionKey: ULONG_PTR;
begin
  while (not Terminated) do
  begin
    GetQueuedCompletionStatus(FParent.FCompletionPort, NumBytes, CompletionKey, FParent.FPOverlapped, INFINITE);
    if CompletionKey <> 0 then
    begin
      Synchronize(HandleEvent);
      with FParent do
      begin
        FBytesWritten := 0;
        ZeroMemory(@FNotificationBuffer, SizeOf(FNotificationBuffer));
        ReadDirectoryChanges(FDirectoryHandle, @FNotificationBuffer, SizeOf(FNotificationBuffer), FParent.WatchSubtree, WatchFilters, @FBytesWritten, @FOverlapped, nil);
      end;
    end
      else
    begin
      Terminate;
    end;
  end;
end;

{ TDirectoryMonitor }

constructor TDirectoryMonitor.Create(AOwner: TComponent);
begin
  inherited;
  FCompletionPort := 0;
  FDirectoryHandle := 0;
  FPOverlapped := @FOverlapped;
  ZeroMemory(@FOverlapped, SizeOf(FOverlapped));
  FWatchFilters := FILE_NOTIFY_CHANGE_FILE_NAME or FILE_NOTIFY_CHANGE_DIR_NAME or FILE_NOTIFY_CHANGE_LAST_WRITE or FILE_NOTIFY_CHANGE_CREATION;
end;

destructor TDirectoryMonitor.Destroy;
begin
  if FActive then Stop;
  inherited;
end;

procedure TDirectoryMonitor.SetActive(AActive: Boolean);
begin
  if csDesigning in ComponentState then Exit;

  if AActive <> FActive then
  begin
    if AActive then Start
      else Stop;
  end;
end;

procedure TDirectoryMonitor.Start;
begin
  FDirectoryHandle :=
    CreateFile(
      PChar(FPath),
      FILE_LIST_DIRECTORY,
      FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
      nil,
      OPEN_EXISTING,
      FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OVERLAPPED,
      0);

  if FDirectoryHandle = INVALID_HANDLE_VALUE then
  begin
    FDirectoryHandle := 0;
    raise EDirectoryMonitorError.Create(SysErrorMessage(GetLastError));
    exit;
  end;

  FCompletionPort := CreateIoCompletionPort(FDirectoryHandle, 0, LongInt(Pointer(Self)), 0);
  ZeroMemory(@FNotificationBuffer, SizeOf(FNotificationBuffer));
  FBytesWritten := 0;
  if not ReadDirectoryChanges(FDirectoryHandle, @FNotificationBuffer, SizeOf(FNotificationBuffer), FWatchSubTree, WatchFilters, @FBytesWritten, @FOverlapped, nil) then
  begin
    CloseHandle(FDirectoryHandle);
    FDirectoryHandle := 0;
    CloseHandle(FCompletionPort);
    FCompletionPort := 0;
    raise EDirectoryMonitorError.Create(SysErrorMessage(GetLastError));
    exit;
  end;
  FWatchThread := TDirectoryMonitorThread.Create(Self);
  TDirectoryMonitorThread(FWatchThread).Resume;
  FActive := True;
end;

procedure TDirectoryMonitor.Stop;
begin
  PostQueuedCompletionStatus(FCompletionPort, 0, 0, nil);
  FWatchThread.WaitFor;
  FWatchThread.Free;
  CloseHandle(FDirectoryHandle);
  FDirectoryHandle := 0;
  CloseHandle(FCompletionPort);
  FCompletionPort := 0;
  FActive := False;
end;

procedure TDirectoryMonitor.DoCreated(Sender: TObject; FileName: string);
begin
  if Assigned(FOnCreated) then FOnCreated(Sender, FPath + FileName);
end;

procedure TDirectoryMonitor.DoDeleted(Sender: TObject; FileName: string);
begin
  if Assigned(FOnDeleted) then FOnDeleted(Sender, FPath + FileName);
end;

procedure TDirectoryMonitor.DoModified(Sender: TObject; FileName: string);
begin
  if Assigned(FOnModified) then FOnModified(Sender, FPath + FileName);
end;

procedure TDirectoryMonitor.DoRenamed(Sender: TObject; FromFileName: string; ToFileName: string);
begin
  if Assigned(FOnRenamed) then FOnRenamed(Sender, FPath + FromFileName, FPath + ToFileName);
end;

procedure TDirectoryMonitor.SetPath(APath: string);
begin
  APath := IncludeTrailingPathDelimiter(APath);
  if APath <> FPath then
  begin
    FPath := APath;
    if FActive then
    begin
      Stop;
      Start;
    end;
  end;
end;

end.
