diff --git a/common/src/integrationTest/java/bisq/common/io_watcher/DirectoryWatcherTests.java b/common/src/integrationTest/java/bisq/common/io_watcher/DirectoryWatcherTests.java
new file mode 100644
index 0000000000..508456e6d6
--- /dev/null
+++ b/common/src/integrationTest/java/bisq/common/io_watcher/DirectoryWatcherTests.java
@@ -0,0 +1,65 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.common.io_watcher;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DirectoryWatcherTests {
+ @Test
+ void detectFileCreation(@TempDir Path tempDir) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ try (var watcher = new DirectoryWatcher(tempDir, Set.of(StandardWatchEventKinds.ENTRY_CREATE))) {
+ var completableFuture = new CompletableFuture();
+ watcher.initialize(completableFuture::complete);
+
+ Path newFilePath = tempDir.resolve("newFile");
+ Files.writeString(newFilePath, "Hello!");
+
+ Path path = completableFuture.get(30, TimeUnit.SECONDS);
+ assertThat(path).isEqualTo(newFilePath);
+ }
+ }
+
+ @Test
+ void detectFileWrite(@TempDir Path tempDir) throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ Path newFilePath = tempDir.resolve("newFile");
+ Files.writeString(newFilePath, "Hello!");
+
+ try (var watcher = new DirectoryWatcher(tempDir, Set.of(StandardWatchEventKinds.ENTRY_MODIFY))) {
+ var completableFuture = new CompletableFuture();
+ watcher.initialize(completableFuture::complete);
+
+ Files.writeString(newFilePath, "World!");
+
+ Path path = completableFuture.get(30, TimeUnit.SECONDS);
+ assertThat(path).isEqualTo(newFilePath);
+ }
+ }
+}
diff --git a/common/src/main/java/bisq/common/io_watcher/CouldNotInitializeDirectoryWatcherException.java b/common/src/main/java/bisq/common/io_watcher/CouldNotInitializeDirectoryWatcherException.java
new file mode 100644
index 0000000000..e9f7d084c2
--- /dev/null
+++ b/common/src/main/java/bisq/common/io_watcher/CouldNotInitializeDirectoryWatcherException.java
@@ -0,0 +1,28 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.common.io_watcher;
+
+public class CouldNotInitializeDirectoryWatcherException extends RuntimeException {
+ public CouldNotInitializeDirectoryWatcherException(String message) {
+ super(message);
+ }
+
+ public CouldNotInitializeDirectoryWatcherException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/common/src/main/java/bisq/common/io_watcher/DirectoryWatcher.java b/common/src/main/java/bisq/common/io_watcher/DirectoryWatcher.java
new file mode 100644
index 0000000000..1177770dbe
--- /dev/null
+++ b/common/src/main/java/bisq/common/io_watcher/DirectoryWatcher.java
@@ -0,0 +1,64 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.common.io_watcher;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchService;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class DirectoryWatcher implements AutoCloseable {
+ private final Path directoryPath;
+ private final Set> watchEventKinds;
+
+ private WatchService watchService;
+
+ public DirectoryWatcher(Path directoryPath, Set> watchEventKinds) {
+ this.directoryPath = directoryPath;
+ this.watchEventKinds = watchEventKinds;
+ }
+
+ public void initialize(Consumer eventConsumer) {
+ try {
+ watchService = FileSystems.getDefault().newWatchService();
+
+ WatchEvent.Kind>[] eventKinds = new WatchEvent.Kind[watchEventKinds.size()];
+ watchEventKinds.toArray(eventKinds);
+ directoryPath.register(watchService, eventKinds);
+
+ subscribeToChangesAsync(eventConsumer);
+
+ } catch (IOException e) {
+ throw new CouldNotInitializeDirectoryWatcherException(e);
+ }
+ }
+
+ private void subscribeToChangesAsync(Consumer consumer) {
+ var directoryEventPublisher = new DirectoryEventPublisher(watchService, directoryPath, watchEventKinds);
+ var directoryEventSubscriber = new DirectoryEventSubscriber(consumer);
+ directoryEventPublisher.subscribe(directoryEventSubscriber);
+ }
+
+ @Override
+ public void close() throws IOException {
+ watchService.close();
+ }
+}