├── deps.edn ├── .travis.yml ├── .gitignore ├── project.clj ├── LICENSE ├── README.md └── src └── watch └── man.clj /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {org.clojure/clojure {:mvn/version "1.10.0"}}} 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: clojure 3 | lein: lein 4 | jdk: 5 | - openjdk8 6 | - oraclejdk11 7 | branches: 8 | except: 9 | - gh-pages 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /.cpcache 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject spootnik/watchman "0.3.8-SNAPSHOT" 2 | :description "Watch directories for changes" 3 | :url "https://github.com/pyr/watchman" 4 | :license {:name "MIT License" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :global-vars {*warn-on-reflection* true} 7 | :plugins [[lein-ancient "0.6.15"]] 8 | :dependencies [[org.clojure/clojure "1.10.1"]]) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Pierre-Yves Ritschard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | watchman: the hero your filesystem needs 2 | ======================================== 3 | 4 | [![Build Status](https://secure.travis-ci.org/pyr/watchman.png)](http://travis-ci.org/pyr/watchman) 5 | 6 | 7 | A Clojure library providing a facade for Java's [WatchService](http://docs.oracle.com/javase/8/docs/api/java/nio/file/WatchService.html). 8 | 9 | 10 | ## Usage 11 | 12 | Pull the depenency with leiningen (add this in your `project.clj`) 13 | 14 | ```clojure 15 | [spootnik/watchman "0.3.7"] 16 | ``` 17 | 18 | 19 | ```clojure 20 | (watch.man/watch! 21 | "/some/dir" 22 | (fn [event] (println (pr-str event)))) 23 | ``` 24 | 25 | `watch!` accepts an optional map argument with the following keys: 26 | 27 | - `event-types`: a collection of any keywords from `:create`, `:modify`, `:delete` 28 | 29 | `watch!` returns the underlying `WatchService`, `watch.man/close` can 30 | be called on the service. 31 | 32 | Each argument to the callback when using `watch!` will be a map with 33 | the following keys: 34 | 35 | - `type`: `:path`, `:exception` or `:closing` 36 | - `path`: The path a `:path` event happens on 37 | - `types`: The types of event a `:path` event was triggered for 38 | - `srv`: The watch service 39 | - `exception`: The exception that was raised for `:exception` events. 40 | 41 | ## Changelog 42 | 43 | ### 0.3.8 44 | 45 | - Readme improvements by @blak3mill3r 46 | 47 | ## License 48 | 49 | Copyright © 2015-2019 Pierre-Yves Ritschard, ISC License 50 | 51 | -------------------------------------------------------------------------------- /src/watch/man.clj: -------------------------------------------------------------------------------- 1 | (ns watch.man 2 | "Small facade for Java's WatchService to be notified of FS changes" 3 | (:import java.nio.file.WatchService 4 | java.nio.file.WatchEvent 5 | java.nio.file.StandardWatchEventKinds 6 | java.nio.file.FileSystems 7 | java.nio.file.FileSystem)) 8 | 9 | ;; convenience functions 10 | 11 | (def evt->kw 12 | "Map WatchEventKinds to keywords" 13 | {StandardWatchEventKinds/ENTRY_CREATE :create 14 | StandardWatchEventKinds/ENTRY_DELETE :delete 15 | StandardWatchEventKinds/ENTRY_MODIFY :modify 16 | StandardWatchEventKinds/OVERFLOW :overflow}) 17 | 18 | (def kw->evt 19 | "Map keywords to WatchEventKinds" 20 | (->> evt->kw 21 | (map (comp vec reverse)) 22 | (reduce merge {}))) 23 | 24 | (defn ->evts 25 | "Prepare a list of event types for Path/register" 26 | [coll] 27 | (into-array 28 | (class StandardWatchEventKinds/ENTRY_CREATE) 29 | (map kw->evt (if (sequential? coll) coll [coll])))) 30 | 31 | (defn ->key 32 | "Create a watch key" 33 | [^FileSystem fs srv types ^String location] 34 | (-> (.getPath fs location (make-array String 0)) 35 | (.register srv types))) 36 | 37 | (defn group-events 38 | "When receiving multiple events on a key, group them by path" 39 | [polled] 40 | (let [watch-events (seq polled)] 41 | (->> (for [^WatchEvent event watch-events 42 | :let [kind (evt->kw (.kind event))] 43 | :when (not= kind :overflow)] 44 | [(.context event) kind]) 45 | (group-by first) 46 | (map (juxt key (comp (partial map second) val))) 47 | (reduce merge {})))) 48 | 49 | ;; "Public" api 50 | 51 | (defn close 52 | "Close a watch service" 53 | [^WatchService service] 54 | (.close service)) 55 | 56 | (defn ->path 57 | "Construct a path from " 58 | ([base elems] 59 | (.getPath 60 | (FileSystems/getDefault) 61 | base 62 | (into-array String (map str (if (sequential? elems) elems [elems]))))) 63 | ([base elems & more] 64 | (->path base conj (list more) elems))) 65 | 66 | (defn watch! 67 | "Watch a location or seq of locations and call notify! with two 68 | args (path and types) when events occur. Accepts an optional map 69 | of options: exception! is a callback and event-types a list of keywords" 70 | ([locations notify! {:keys [event-types] 71 | :or {event-types [:create :modify :delete]}}] 72 | (let [fs (FileSystems/getDefault) 73 | srv (.newWatchService fs) 74 | locs (if (sequential? locations) locations [locations]) 75 | keys (mapv (partial ->key fs srv (->evts event-types)) locs)] 76 | (future 77 | (try 78 | (loop [keys keys] 79 | (when (seq keys) 80 | (let [k (.take srv)] 81 | (doseq [[path types] (group-events (.pollEvents k))] 82 | (notify! {:type :path :path path :types types :srv srv})) 83 | (recur (if (.reset k) keys (remove (partial = k) keys)))))) 84 | (catch Exception e 85 | (notify! {:type :exception :srv srv :exception e})) 86 | (finally 87 | (.close srv) 88 | (notify! {:type :closing :srv srv})))) 89 | srv)) 90 | ([locations notify!] 91 | (watch! locations notify! {}))) 92 | --------------------------------------------------------------------------------