Directory Watcher

repository·main·Indexed 43 days ago

https://github.com/gmethvin/directory-watcher

A utility for JDK 8+ providing recursive directory watching across Linux, macOS, and Windows. It improves upon the standard JDK WatchService with native macOS support via Carbon File System Events API, event deduplication through file hashing, and a Scala integration via better-files.

Tokens
1.7K
Snippets
4
Records
5
Agent score
5%

What's inside Directory Watcher

  1. How DirectoryWatcher differs from standard WatchService

    main

    DirectoryWatcher provides several improvements over the standard JDK WatchService:

    1. Recursive Watching: It provides true recursive watching. On Windows, it uses ExtendedWatchEventModifier.FILE_TREE. On Linux, it manually registers new WatchKeys for subdirectories as they are added.
    2. Native macOS Support: It uses a JNA-based implementation for macOS to avoid the inefficient polling-based JDK implementation.
    3. Event Deduplication: It uses file hashing to filter out duplicate or redundant events (e.g., multiple modify events emitted by Windows for a single change).
    4. Path Resolution: On macOS, the implementation returns the full absolute path in notifications, so you do not need to resolve it against a WatchKey.
  2. Integrate with better-files (Scala)

    main

    For Scala users, the recommended API is the better-files integration. Add the dependency io.methvin %% directory-watcher-better-files to your project.

    Use the RecursiveFileMonitor class to implement directory watching. You can override specific lifecycle methods like onCreate, onModify, and onDelete, or use the more general onEvent method to handle java.nio.file.WatchEvent types.

    import better.files._
    import io.methvin.better.files._
    
    val myDir = File("/directory/to/watch/")
    val watcher = new RecursiveFileMonitor(myDir) {
      override def onCreate(file: File, count: Int) = println(s"$file got created")
      override def onModify(file: File, count: Int) = println(s"$file got modified $count times")
      override def onDelete(file: File, count: Int) = println(s"$file got deleted")
    }
    
    import scala.concurrent.ExecutionContext.Implicits.global
    watcher.start()
  3. Install Directory Watcher

    main

    Add the directory-watcher dependency to your project using Maven or sbt. Replace directoryWatcherVersion with the latest version.

    ### For sbt:
    ```scala
    libraryDependencies += "io.methvin" % "directory-watcher" % directoryWatcherVersion

    For maven:

    <dependency>
        <groupId>io.methvin</groupId>
        <artifactId>directory-watcher</artifactId>
        <version>${directoryWatcherVersion}</version>
    </dependency>
  4. Configure file hashing in DirectoryWatcher

    main

    By default, DirectoryWatcher uses file hashing to prevent duplicate events (common on Windows). This involves calculating a hash for every file encountered.

    To disable hashing (which improves performance but may increase duplicate events):

    • Use .fileHashing(false) in the builder.

    To use a custom hashing strategy, use .fileHasher(FileHasher):

    • FileHasher.LAST_MODIFIED_TIME: Uses the file's last modified timestamp. This is recommended for JDK 10+ on macOS with APFS for better performance.
    // Disable hashing
    DirectoryWatcher watcher = DirectoryWatcher.builder()
        .path(path)
        .listener(listener)
        .fileHashing(false)
        .build();
    
    // Use last modified time as a hasher
    DirectoryWatcher watcher = DirectoryWatcher.builder()
        .path(path)
        .listener(listener)
        .fileHasher(FileHasher.LAST_MODIFIED_TIME)
        .build();
  5. Use the Java API to watch directories

    main

    Use DirectoryWatcher.builder() to configure and create a watcher. You can watch a single path using .path(Path) or multiple directories using .paths(Iterable<Path>).

    To start watching, use:

    • watch(): Blocks the current thread.
    • watchAsync(): Watches in a separate thread and returns a CompletableFuture<Void>.

    On macOS, the library automatically provides a native implementation using the Carbon File System Events API via JNA, replacing the default polling-based JDK implementation.

    package com.example;
    
    import io.methvin.watcher.DirectoryWatcher;
    import java.io.IOException;
    import java.nio.file.Path;
    import java.util.concurrent.CompletableFuture;
    
    public class DirectoryWatchingUtility {
    
        private final Path directoryToWatch;
        private final DirectoryWatcher watcher;
    
        public DirectoryWatchingUtility(Path directoryToWatch) throws IOException {
            this.directoryToWatch = directoryToWatch;
            this.watcher = DirectoryWatcher.builder()
                    .path(directoryToWatch)
                    .listener(event -> {
                        switch (event.eventType()) {
                            case CREATE: /* file created */; break;
                            case MODIFY: /* file modified */; break;
                            case DELETE: /* file deleted */; break;
                        }
                    })
                    .build();
        }
    
        public void stopWatching() throws IOException {
            watcher.close();
        }
    
        public CompletableFuture<Void> watch() {
            return watcher.watchAsync();
        }
    }