lloydmeta / schwatcher   0.3.5

MIT License GitHub

File-watching library for Scala. Built on Java 7's WatchService, RxScala and Akka actors.

Scala versions: 2.12 2.11

Schwatcher Build Status Maven Central

Note: Requires Java8+ because the WatchService API is an essential part of this library along with Akka (Akka 2.4.x dropped support for Java7)

TL;DR A library that wraps the WatchService API. of Java7. You can choose to send callbacks to be registered on paths (both directories and files) or turn path events into a composable asynchronous data stream (RxScala Observable). For details, check out the Example usage section below.

As of Java7, the WatchService API was introduced to allow developers to monitor the file system without resorting to an external library/dependency like JNotify. Unfortunately, it requires you to use a loop that blocks in order to retrieve events from the API and does not have recursion support built-in.

Schwatcher will allow you to instantiate an Akka actor, register callbacks (functions that take a Path and return Unit) to be fired for certain Paths and event types by sending messages to it, then simply wait for the callbacks to be fired. Alternatively, use the Observable interface to register for notifications on paths.

The goal of Schwatcher is to facilitate the use of the Java7 API in Scala in a simple way that is in line with the functional programming paradigm.

As of now, Scala 2.11.x and 2.12.x are supported (Akka 2.4.x dropped support for 2.10.x)

Installation

Add the following to your build.sbt

libraryDependencies += "com.beachape.filemanagement" %% "schwatcher" % "0.3.2"

If the above does not work because it cannot be resolved, its likely because it hasn't been synced to Maven central yet. In that case, download a SNAPSHOT release of the same version by adding this to build.sbt

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

libraryDependencies += "com.beachape.filemanagement" %% "schwatcher" % "0.3.2-SNAPSHOT"

Those using Akka 2.1.x should stay on 0.0.2 as 0.0.3 onwards uses Akka 2.2.x, which requires some work to upgrade to.

Those using Akka 2.2.x should stay on 0.0.9 as 0.0.10 onwards uses Akka 2.3.x, which may require some work to upgrade to.

0.1.0 brings with it breaking changes on how to register callbacks (see example). Please be aware when upgrading from 0.0.x releases.

Those using Akka 2.3.x should stay on 0.1.x as 0.2.x onwards uses Akka 2.4.x, which requires Java8 and Scala 2.11.x

Example Usage

Akka

The basic workflow is:

  1. Instantiate a MonitorActor
  2. Register callbacks by sending RegisterCallback messages to the MonitorActor
  3. Carry on
  • Optionally, you can un-register all callbacks for a path and event type by sending UnRegisterCallback messages.
  • Both RegisterCallback and UnRegisterCallback messages have a recursive argument (be sure to see point 5 in the Caveats section) that triggers recursive registration and un-registration of callbacks for path.
import akka.actor.ActorSystem
import com.beachape.filemanagement.MonitorActor
import com.beachape.filemanagement.RegistryTypes._
import com.beachape.filemanagement.Messages._

import java.io.{FileWriter, BufferedWriter}

import java.nio.file.Paths
import java.nio.file.StandardWatchEventKinds._

implicit val system = ActorSystem("actorSystem")
val fileMonitorActor = system.actorOf(MonitorActor(concurrency = 2))

val modifyCallbackFile: Callback = { path => println(s"Something was modified in a file: $path")}
val modifyCallbackDirectory: Callback = { path => println(s"Something was modified in a directory: $path")}

val desktop = Paths get "/Users/lloyd/Desktop"
val desktopFile = Paths get "/Users/lloyd/Desktop/test"

/*
  This will receive callbacks for just the one file
*/
fileMonitorActor ! RegisterCallback(
  event = ENTRY_MODIFY,
  path = desktopFile,
  callback =  modifyCallbackFile
  )

/*
  If desktopFile is modified, this will also receive a callback
  it will receive callbacks for everything under the desktop directory
*/
fileMonitorActor ! RegisterCallback(
  event = ENTRY_MODIFY,
  path = desktop,
  callback = modifyCallbackDirectory)


//modify a monitored file
val writer = new BufferedWriter(new FileWriter(desktopFile.toFile))
writer.write("Theres text in here wee!!")
writer.close

// #=> Something was modified in a file: /Users/lloyd/Desktop/test.txt
//     Something was modified in a directory: /Users/lloyd/Desktop/test.txt

RxScala

There is another way to monitor files supported by this library: via Rx Observables.

This is neat because it allows you to treat notifications for files as, well, an Observable, which conceptually is like a Stream, or a Go channel. You can filter on the stream, map on it, filter on it ... basically compose it as you would a normal observable.

Note that since RxMonitor is powered by an Akka actor under the covers, the changes to what you (do not) want to monitor are done in a thread-safe manner so feel free to pass this object around.

import com.beachape.filemanagement.RxMonitor
import java.io.{FileWriter, BufferedWriter}

import java.nio.file.Paths
import java.nio.file.StandardWatchEventKinds._

val monitor = RxMonitor()
val observable = monitor.observable

val subscription = observable.subscribe(
  onNext = { p => println(s"Something was modified in a file mufufu: $p")},
  onError = { t => println(t)},
  onCompleted = { () => println("Monitor has been shut down") }
)

val desktopFile = Paths get "/Users/lloyd/Desktop/test"

monitor.registerPath(ENTRY_MODIFY, desktopFile)

Thread.sleep(100)

//modify a monitored file
val writer = new BufferedWriter(new FileWriter(desktopFile.toFile))
writer.write("Theres text in here wee!!")
writer.close

// #=> Something was modified in a file mufufu: /Users/lloyd/Desktop/test

// stop monitoring
monitor.stop()

// #=> Monitor has been shut down

Caveats

Above all things understand that library relies on the Java7 WatchService API and has the same constraints, limitations, and behaviours according to the implementation for the version of JVM you're using.

Additional library-specific caveats and notes are:

  1. Callbacks are registered for specific paths, and for directory paths, can be registered as recursive so that a single callback is fired when an event occurs inside the directory tree.
  2. Callbacks are not checked for uniqueness when registered to a specific path.
  3. A specific path can have multiple callbacks registered to a file event types, all will be fired.
  4. Callbacks are not guaranteed to be fired in any particular order unless if concurrency is set to 1 on the MonitorActor.
  5. Any event on a file path will bubble up to its immediate parent folder path. This means that if both a file and it's parent directory are registered for callbacks, both sets of callbacks will be fired.

As a result of note 6, you may want to think twice about registering recursive callbacks for ENTRY_DELETE because if a directory is deleted within a directory, 2 callbacks will be fired, once for the path registered for the deleted directory and once for the path registered for the directory above it (its parent directory).

Contributors

IDE Sponsor

Jetbrains sponsors this project through their Open Source licence, which has been an immensely helpful tool for all of my Scala development, at home and at work.

IntelliJ IDEA