|
|
@@ -1,166 +0,0 @@
|
|
|
-(ns electron.fs-watcher
|
|
|
- "This ns is a wrapper around the chokidar file watcher,
|
|
|
- https://www.npmjs.com/package/chokidar. File watcher events are sent to the
|
|
|
- `file-watcher` ipc channel"
|
|
|
- (:require ["chokidar" :as watcher]
|
|
|
- ["electron" :refer [app]]
|
|
|
- ["fs" :as fs]
|
|
|
- [cljs-bean.core :as bean]
|
|
|
- [electron.logger :as logger]
|
|
|
- [electron.utils :as utils]
|
|
|
- [electron.window :as window]
|
|
|
- [logseq.common.graph :as common-graph]))
|
|
|
-
|
|
|
-;; TODO: explore different solutions for different platforms
|
|
|
-;; 1. https://github.com/Axosoft/nsfw
|
|
|
-
|
|
|
-(defonce polling-interval 10000)
|
|
|
-;; dir -> Watcher
|
|
|
-(defonce *file-watcher (atom {})) ;; val: [watcher watcher-del-f]
|
|
|
-
|
|
|
-(defonce file-watcher-chan "file-watcher")
|
|
|
-(defn- send-file-watcher! [dir type payload]
|
|
|
- (let [send-fn (fn [^js win]
|
|
|
- (when-not (.isDestroyed win)
|
|
|
- (.. win -webContents
|
|
|
- (send file-watcher-chan
|
|
|
- (bean/->js {:type type :payload payload})))
|
|
|
- true))
|
|
|
- wins (if (:global-dir payload)
|
|
|
- (window/get-all-windows)
|
|
|
- (window/get-graph-all-windows dir))]
|
|
|
- (if (or (contains? #{"unlinkDir" "addDir"} type)
|
|
|
- ;; Only change events to a global dir are emitted to all windows.
|
|
|
- ;; Add* events are not emitted to all since each client adds
|
|
|
- ;; files at different times.
|
|
|
- (and (:global-dir payload) (= "change" type)))
|
|
|
- ;; notify every windows
|
|
|
- (doseq [win wins] (send-fn win))
|
|
|
-
|
|
|
- ;; Should only send to one window
|
|
|
- ;; If no window is on this graph, just ignore
|
|
|
- (let [sent? (some send-fn wins)]
|
|
|
- (when-not sent? (logger/warn ::send
|
|
|
- "unhandled file event will cause uncatched file modifications!. target:" dir))))))
|
|
|
-
|
|
|
-(defn- publish-file-event!
|
|
|
- [dir path event options]
|
|
|
- (let [dir-path? (= dir path)
|
|
|
- content (when (and (not= event "unlink")
|
|
|
- (not dir-path?)
|
|
|
- (utils/should-read-content? path))
|
|
|
- (utils/read-file path))
|
|
|
- stat (when (and (not= event "unlink")
|
|
|
- (not dir-path?))
|
|
|
- (utils/fs-stat->clj path))]
|
|
|
- (send-file-watcher! dir event (merge {:dir (utils/fix-win-path! dir)
|
|
|
- :path (utils/fix-win-path! path)
|
|
|
- :content content
|
|
|
- :stat stat}
|
|
|
- (select-keys options [:global-dir])))))
|
|
|
-(defn- create-dir-watcher
|
|
|
- [dir options]
|
|
|
- (let [watcher-opts (clj->js
|
|
|
- {:ignored (fn [path]
|
|
|
- (common-graph/ignored-path? dir path))
|
|
|
- :ignoreInitial true
|
|
|
- :ignorePermissionErrors true
|
|
|
- :interval polling-interval
|
|
|
- :binaryInterval polling-interval
|
|
|
- :persistent true
|
|
|
- :disableGlobbing true
|
|
|
- :usePolling false
|
|
|
- :awaitWriteFinish true})
|
|
|
- dir-watcher (.watch watcher dir watcher-opts)]
|
|
|
- ;; TODO: batch sender
|
|
|
- (.on dir-watcher "unlinkDir"
|
|
|
- (fn [path]
|
|
|
- (logger/warn ::on-unlink-dir {:path path})
|
|
|
- (when (= dir path)
|
|
|
- (publish-file-event! dir dir "unlinkDir" options))))
|
|
|
- (.on dir-watcher "addDir"
|
|
|
- (fn [path]
|
|
|
- (when (= dir path)
|
|
|
- (publish-file-event! dir dir "addDir" options))))
|
|
|
- (.on dir-watcher "add"
|
|
|
- (fn [path]
|
|
|
- (publish-file-event! dir path "add" options)))
|
|
|
- (.on dir-watcher "change"
|
|
|
- (fn [path]
|
|
|
- (publish-file-event! dir path "change" options)))
|
|
|
- (.on dir-watcher "unlink"
|
|
|
- ;; delay 500ms for syncing disks
|
|
|
- (fn [path]
|
|
|
- (logger/debug ::on-unlink {:path path})
|
|
|
- (js/setTimeout #(when (not (fs/existsSync path))
|
|
|
- (publish-file-event! dir path "unlink" options))
|
|
|
- 500)))
|
|
|
- (.on dir-watcher "error"
|
|
|
- (fn [path]
|
|
|
- (logger/warn ::on-error "Watch error happened:" {:path path})))
|
|
|
-
|
|
|
- dir-watcher))
|
|
|
-
|
|
|
-(defn- seed-client-with-initial-global-dir-data
|
|
|
- "Ensures that secondary clients initialize their databases efficiently and in
|
|
|
- the same way as the primary client. This fn achieves this by creating a
|
|
|
- temporary watcher whose sole purpose is to seed the db and then close when
|
|
|
- its done seeding a.k.a. ready event fires."
|
|
|
- [dir options]
|
|
|
- (let [dir-watcher (create-dir-watcher dir options)]
|
|
|
- (.on dir-watcher "ready" (fn []
|
|
|
- (.close dir-watcher)))))
|
|
|
-
|
|
|
-(defn- create-and-save-watcher
|
|
|
- [dir options]
|
|
|
- (let [dir-watcher (create-dir-watcher dir options)
|
|
|
- watcher-del-f #(.close dir-watcher)]
|
|
|
- (swap! *file-watcher assoc dir [dir-watcher watcher-del-f])
|
|
|
- ;; electron app extends `EventEmitter`
|
|
|
- ;; TODO check: duplicated with the logic in "window-all-closed" ?
|
|
|
- (.on app "quit" watcher-del-f)))
|
|
|
-
|
|
|
-(defn- watch-global-dir!
|
|
|
- "Only one watcher exists per global dir so only create the watcher for the
|
|
|
- primary client. Secondary clients only seed their client database."
|
|
|
- [dir options]
|
|
|
- (if (get @*file-watcher dir)
|
|
|
- (seed-client-with-initial-global-dir-data dir options)
|
|
|
- (create-and-save-watcher dir options)))
|
|
|
-
|
|
|
-(defn watch-dir!
|
|
|
- "Watches a directory and emits file events. In addition to file
|
|
|
- watching, clients rely on watchers to initially seed their database with
|
|
|
- the file contents of a dir. This is done with the ignoreInitial option
|
|
|
- set to false, https://github.com/paulmillr/chokidar#path-filtering. The
|
|
|
- watcher emits addDir and add file events which then seed the client database.
|
|
|
- This fn has the following options:
|
|
|
-
|
|
|
-* :global-dir - Boolean that indicates the watched directory is global. This
|
|
|
- type of directory has different behavior then a normal watcher as it
|
|
|
- broadcasts its change events to all clients. This option needs to be passed to
|
|
|
- clients in order for them to identify the correct db"
|
|
|
- [dir options]
|
|
|
- (if (:global-dir options)
|
|
|
- (watch-global-dir! dir options)
|
|
|
- (when-not (get @*file-watcher dir)
|
|
|
- (if (fs/existsSync dir)
|
|
|
- (create-and-save-watcher dir options)
|
|
|
- ;; retry if the `dir` not exists, which is useful when a graph's folder is
|
|
|
- ;; back after refreshing the window
|
|
|
- (js/setTimeout #(watch-dir! dir options) 5000)))))
|
|
|
-
|
|
|
-(defn close-watcher!
|
|
|
- "If no `dir` provided, close all watchers;
|
|
|
- Otherwise, close the specific watcher if exists"
|
|
|
- ([]
|
|
|
- (doseq [[watcher watcher-del-f] (vals @*file-watcher)]
|
|
|
- (.close watcher)
|
|
|
- (.removeListener app "quit" watcher-del-f))
|
|
|
- (reset! *file-watcher {}))
|
|
|
- ([dir]
|
|
|
- (let [[watcher watcher-del-f] (get @*file-watcher dir)]
|
|
|
- (when watcher
|
|
|
- (.close watcher)
|
|
|
- (.removeListener app "quit" watcher-del-f)
|
|
|
- (swap! *file-watcher dissoc dir)))))
|