├── .gitignore ├── README.md ├── project.clj ├── src └── clojure_watch │ ├── core.clj │ └── example.clj └── test └── clojure_watch └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure-watch 2 | 3 | The most user-friendly file-system watch library written in Clojure, built on top of the [Java 7 WatchEvent API](http://docs.oracle.com/javase/tutorial/essential/io/notification.html). 4 | 5 | ## Installation 6 | 7 | Add this to your dependencies: 8 | 9 | ```clojure 10 | [clojure-watch "LATEST"] 11 | ``` 12 | 13 | Check out [Clojars](https://clojars.org/clojure-watch) for more specific installation instructions. 14 | 15 | ## Usage 16 | 17 | An example: 18 | 19 | ```clojure 20 | (ns clojure-watch.example 21 | (:require [clojure-watch.core :refer [start-watch]])) 22 | 23 | (start-watch [{:path "/some/valid/path" 24 | :event-types [:create :modify :delete] 25 | :bootstrap (fn [path] (println "Starting to watch " path)) 26 | :callback (fn [event filename] (println event filename)) 27 | :options {:recursive true}}]) 28 | ``` 29 | 30 | You call `start-watch` with a collection of specifications. A specification is a map with five entries: 31 | 32 | 1. `:path`. The path to a directory that you want to watch. 33 | 2. `:event-types`. A collection of the types of events that you want to watch. Possible values include `:create`, `:modify`, and `:delete`. 34 | 3. `:bootstrap` (optional). A function to be run only once when `start-watch` is invoked. The function should accept one argument: the path given in the spec. 35 | 4. `:callback`. A callback function to be invoked when an event occurs. The function should accept two arguments, the first one being the type of the event that happened (`:create`, `:modify`, or `:delete`), and the second one being the full path to the file to which the event happened. 36 | 5. `:options`. A map of options. Currently the only available option is `:recursive`. If it's set to true, all sub-directories will be watched. 37 | 38 | ### Stop the watch 39 | 40 | `start-watch` returns a function that can be called to stop the watch. For example: 41 | 42 | (let [stop-watch (start-watch [{ 43 | :path "/some/valid/path" 44 | :event-types [:create :modify :delete] 45 | :callback (fn [event filename] (println event filename))}])] ; start the watch 46 | (Thread/sleep 20000) ; manipulate files on the path 47 | (stop-watch)) ; stop the watch 48 | 49 | ## License 50 | 51 | [WTFPL](http://www.wtfpl.net/). 52 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-watch "0.1.13" 2 | :description "A library for watching file system events." 3 | :url "https://github.com/derekchiang/Clojure-Watch" 4 | :license {:name "WTFPL" 5 | :url "http://www.wtfpl.net/"} 6 | :dependencies [[org.clojure/clojure "1.5.1"]]) 7 | 8 | -------------------------------------------------------------------------------- /src/clojure_watch/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-watch.core 2 | (:import (java.nio.file WatchService Paths FileSystems))) 3 | 4 | (def ENTRY_CREATE java.nio.file.StandardWatchEventKinds/ENTRY_CREATE) 5 | (def ENTRY_DELETE java.nio.file.StandardWatchEventKinds/ENTRY_DELETE) 6 | (def ENTRY_MODIFY java.nio.file.StandardWatchEventKinds/ENTRY_MODIFY) 7 | 8 | (defn register [{:keys [path event-types callback options] :as spec} 9 | watcher keys] 10 | (letfn [(register-helper 11 | [{:keys [path event-types callback options]} watcher keys] 12 | (let [; make-array is needed because Paths/get is a variadic method 13 | ; Java compiler handles variadic method automatically, but when 14 | ; using Clojure it's necessary to manually supply an array at 15 | ; the end. 16 | dir (Paths/get path (make-array String 0)) 17 | types (reduce (fn [acc type] 18 | (case type 19 | :create (conj acc ENTRY_CREATE) 20 | :delete (conj acc ENTRY_DELETE) 21 | :modify (conj acc ENTRY_MODIFY))) 22 | [] 23 | event-types) 24 | 25 | modifier (try 26 | (let [c (Class/forName "com.sun.nio.file.SensitivityWatchEventModifier") 27 | f (.getField c "HIGH")] 28 | (.get f c)) 29 | (catch Exception e)) 30 | 31 | modifiers (when modifier 32 | (doto (make-array java.nio.file.WatchEvent$Modifier 1) 33 | (aset 0 modifier))) 34 | 35 | key (if modifiers 36 | (.register dir watcher (into-array types) modifiers) 37 | (.register dir watcher (into-array types)))] 38 | 39 | (assoc keys key [dir callback])))] 40 | (register-helper spec watcher keys))) 41 | 42 | (defn start-watch [specs] 43 | (letfn [(handle-recursive 44 | [specs] 45 | (reduce 46 | (fn [acc 47 | {:keys [path event-types callback options bootstrap] :as spec}] 48 | (if bootstrap 49 | (bootstrap path)) 50 | (if (:recursive options) 51 | (let [f (clojure.java.io/file path) 52 | fs (file-seq f) 53 | acc (ref acc)] 54 | (do 55 | (doall 56 | (pmap 57 | (fn [file] 58 | (if (.isDirectory file) 59 | (dosync 60 | (commute 61 | acc 62 | #(conj % (assoc spec :path (str file))))))) fs)) 63 | (deref acc))) 64 | (conj acc spec))) 65 | [] 66 | specs))] 67 | (let [specs (handle-recursive specs) 68 | watcher (.. FileSystems getDefault newWatchService) 69 | keys (reduce (fn [keys spec] 70 | (register spec watcher keys)) {} specs)] 71 | (letfn [(kind-to-key [kind] 72 | (case kind 73 | "ENTRY_CREATE" :create 74 | "ENTRY_MODIFY" :modify 75 | "ENTRY_DELETE" :delete)) 76 | (watch [watcher keys] 77 | (let [key (.take watcher) 78 | [dir callback] (keys key)] 79 | (do 80 | (doseq [event (.pollEvents key)] 81 | (let [kind (kind-to-key (.. event kind name)) 82 | name (->> event 83 | .context 84 | (.resolve dir) 85 | str)] 86 | ; Run callback in another thread 87 | (future (do 88 | (callback kind name) 89 | (.reset key))))) 90 | (recur watcher keys)))) 91 | (close-watcher [] 92 | (.close watcher))] 93 | (future (watch watcher keys)) 94 | close-watcher)))) 95 | -------------------------------------------------------------------------------- /src/clojure_watch/example.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-watch.example 2 | (:require [clojure-watch.core :refer [start-watch]])) 3 | 4 | (defn monitor-dir 5 | [path] 6 | (println "monitoring directory: " path) 7 | (start-watch [{:path path 8 | :event-types [:create :modify :delete] 9 | :bootstrap (fn [path] (println "Starting to watch " path)) 10 | :callback (fn [event filename] (println event filename)) 11 | :options {:recursive true}}])) 12 | 13 | ; Set this to a valid directory path 14 | (let [stop-watch (monitor-dir "/some/valid/path")] 15 | (Thread/sleep 20000) ; Manipulate files on the path 16 | (stop-watch)) ; turn off the watcher 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/clojure_watch/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-watch.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-watch.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | --------------------------------------------------------------------------------