Früher habe ich mich immer gefragt, wie z. B. Dropbox & Co. Änderungen an Dateien feststellen. Irgendwann bin ich dann über „FileWatcher“ gestolpert. FileWatcher? Genau. Ein FileWatcher überwacht ein Verzeichnis und gibt einem Rückmeldung, ob sich in diesem Verzeichnis etwas geändert hat.
Wahrscheinlich gibt es dazu Bibliotheken, die das ganze Kapseln für Crossplatform. In diesem Blogbeitrag möchte ich euch den Weg für Windows zeigen. Wie der Titel bereits sagt – Ein einfacher FileWatcher mit der Win32Api.
FileWatcher registrieren
Mit FindFirstChangeNotificationW wird der FileWatcher für ein Verzeichnis aktiviert:
HANDLE hChangeNotification = FindFirstChangeNotificationW( this->edtPfad->Text.c_str(), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE );
Folgende Parameter werden übergeben:
- Pfad, der überwacht werden soll
- Angabe, ob der Verzeichnisbaum überwacht werden soll (also ob auch Unterverzeichnisse auf Änderungen überwacht werden sollen)
- Die Änderung, auf die geachtet werden soll. Hier sind z. B. auch Änderung des Dateinamens, etc. möglich
Als Rückgabewert erhält man einen Handle, mit dem man weiterarbeiten kann. Die komplette Beschreibung zu der Funktion findet ihr hier.
Als nächstes wird noch kurz geprüft, ob alles geklappt hat, ansonsten wird die Funktion verlassen:
// Pruefen, ob der FileWatcher richtig registriert werden konnte if (hChangeNotification == INVALID_HANDLE_VALUE || hChangeNotification == NULL) { this->meInfo->Lines->Add(L"Fehler beim registrieren des FileWatchers"); return; }
Zum Schluss wird mit WaitForSingleObject gewartet, bis das Objekt ausgelöst wird.
// Warten, bis im Ordner ein Aenderung auftritt WaitForSingleObject(hChangeNotification, INFINITE);
Die ganze Funktion sieht in meinem Fall so aus:
void __fastcall TFormFileWatcher::btnOhneThreadClick(TObject *Sender) { std::list<Datei> lstDateienAlt, lstDateienNeu; this->meInfo->Lines->Add(L"FileWatcher gestartet ..."); // Den aktuellen Stand der Dateien lesen this->DateienLesen(this->edtPfad->Text, lstDateienAlt); // FileWatcher registrieren HANDLE hChangeNotification = FindFirstChangeNotificationW(this->edtPfad->Text.c_str(), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE); // Pruefen, ob der FileWatcher richtig registriert werden konnte if (hChangeNotification == INVALID_HANDLE_VALUE || hChangeNotification == NULL) { this->meInfo->Lines->Add(L"Fehler beim registrieren des FileWatchers"); return; } // Warten, bis im Ordner ein Aenderung auftritt WaitForSingleObject(hChangeNotification, INFINITE); // Meldung ausgeben this->meInfo->Lines->Add(L"Änderung registriert!"); // Den aktuellen Stand der Dateien lesen this->DateienLesen(this->edtPfad->Text, lstDateienNeu); // Alte und neue Dateien vergleichen und evtl. gemachte Aenderungen ausgeben this->DateienVergleichen(lstDateienAlt, lstDateienNeu); } //---------------------------------------------------------------------------
Das Problem: Der Blockende Aufruf
Das Problem an der Sache: WaitForSingleObject wartet, bis das entsprechende Event aufgerufen wird. Solange das nicht der Fall ist, wird der Thread in dem es aufgerufen wird geblockt. Die Lösung ist wie zu erwarten ein Thread. Ein weiterer Pluspunkt des Threads: Mit FindNextChangeNotification kann gleich wieder auf eine Dateiänderung gewartet werden. Hier ein Beispiel mit einem Aufruf über einen Thread:
void __fastcall TFormFileWatcher::btnMitThreadClick(TObject *Sender) { auto pThread = TThread::CreateAnonymousThread([this]() -> void { std::list<Datei> lstDateienAlt, lstDateienNeu; this->meInfo->Lines->Add(L"FileWatcher gestartet ..."); // FileWatcher registrieren HANDLE hChangeNotification = FindFirstChangeNotificationW(this->edtPfad->Text.c_str(), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE); // Pruefen, ob der FileWatcher richtig registriert werden konnte if (hChangeNotification == INVALID_HANDLE_VALUE || hChangeNotification == NULL) { this->meInfo->Lines->Add(L"Fehler beim registrieren des FileWatchers"); return; } while(true) { lstDateienAlt.clear(); lstDateienNeu.clear(); // Den aktuellen Stand der Dateien lesen this->DateienLesen(this->edtPfad->Text, lstDateienAlt); // Warten, bis im Ordner ein Aenderung auftritt WaitForSingleObject(hChangeNotification, INFINITE); // Den aktuellen Stand der Dateien lesen this->DateienLesen(this->edtPfad->Text, lstDateienNeu); // Alte und neue Dateien vergleichen und evtl. gemachte Aenderungen ausgeben this->DateienVergleichen(lstDateienAlt, lstDateienNeu); FindNextChangeNotification(hChangeNotification); } }); pThread->Start(); } //---------------------------------------------------------------------------
Fazit
Mit FindFirstChangeNotificationW und FindNextChangeNotification kann ziemlich einfach auf Dateiänderungen in einem Verzeichnis gelauscht werden. In meinem Testprogramm prüfe ich Testhalber, welche Dateien sich geändert haben, welche neu hinzugekommen sind und welche gelöscht wurden. Um gleich die geänderte Datei zu bekommen gibt es allerdings noch bessere/effizentere Wege, die allerdings auch *etwas* komplizierter sind. Diese werde ich in einem eigenen Beitrag vorstellen.
Das Beispielprojekt könnt ihr euch bei Github herunterladen.