nfs.cljs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. (ns frontend.handler.web.nfs
  2. "The File System Access API, https://web.dev/file-system-access/."
  3. (:require [cljs-bean.core :as bean]
  4. [promesa.core :as p]
  5. [medley.core :as medley]
  6. [goog.object :as gobj]
  7. [goog.dom :as gdom]
  8. [frontend.util :as util]
  9. [frontend.handler.common :as common-handler]
  10. ["/frontend/utils" :as utils]
  11. [frontend.handler.repo :as repo-handler]
  12. [frontend.handler.file :as file-handler]
  13. [frontend.idb :as idb]
  14. [frontend.state :as state]
  15. [clojure.string :as string]
  16. [clojure.set :as set]
  17. [frontend.ui :as ui]
  18. [frontend.fs :as fs]
  19. [frontend.fs.nfs :as nfs]
  20. [frontend.db :as db]
  21. [frontend.db.model :as db-model]
  22. [frontend.config :as config]
  23. [lambdaisland.glogi :as log]))
  24. (defn remove-ignore-files
  25. [files]
  26. (let [files (remove (fn [f]
  27. (string/starts-with? (:file/path f) ".git/"))
  28. files)]
  29. (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
  30. %) files)]
  31. (if-let [file (:file/file ignore-file)]
  32. (p/let [content (.text file)]
  33. (when content
  34. (let [paths (set (file-handler/ignore-files content (map :file/path files)))]
  35. (when (seq paths)
  36. (filter (fn [f] (contains? paths (:file/path f))) files)))))
  37. (p/resolved files))
  38. (p/resolved files))))
  39. (defn- ->db-files
  40. [dir-name result]
  41. (let [result (flatten (bean/->clj result))]
  42. (map (fn [file]
  43. (let [handle (gobj/get file "handle")
  44. get-attr #(gobj/get file %)
  45. path (-> (get-attr "webkitRelativePath")
  46. (string/replace-first (str dir-name "/") ""))]
  47. {:file/name (get-attr "name")
  48. :file/path path
  49. :file/last-modified-at (get-attr "lastModified")
  50. :file/size (get-attr "size")
  51. :file/type (get-attr "type")
  52. :file/file file
  53. :file/handle handle})) result)))
  54. (defn- filter-markup-and-built-in-files
  55. [files]
  56. (filter (fn [file]
  57. (contains? (set/union config/markup-formats #{:css :edn})
  58. (keyword (util/get-file-ext (:file/path file)))))
  59. files))
  60. (defn- set-batch!
  61. [handles]
  62. (let [handles (map (fn [[path handle]]
  63. {:key path
  64. :value handle}) handles)]
  65. (idb/set-batch! handles)))
  66. (defn- set-files-aux!
  67. [handles]
  68. (if (seq handles)
  69. (let [[h t] (split-at 50 handles)]
  70. (p/let [_ (p/promise (fn [_]
  71. (js/setTimeout (fn []
  72. (p/resolved nil)) 10)))
  73. _ (set-batch! h)]
  74. (when (seq t)
  75. (set-files-aux! t))))))
  76. (defn- set-files!
  77. [handles]
  78. (let [handles (map (fn [[path handle]]
  79. (let [handle-path (str config/local-handle-prefix path)]
  80. [handle-path handle]))
  81. handles)]
  82. (doseq [[path handle] handles]
  83. (nfs/add-nfs-file-handle! path handle))
  84. (set-files-aux! handles)))
  85. (defn ls-dir-files
  86. []
  87. (let [path-handles (atom {})]
  88. ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
  89. (->
  90. (p/let [result (utils/openDirectory #js {:recursive true}
  91. (fn [path handle]
  92. (swap! path-handles assoc path handle)))
  93. _ (state/set-loading-files! true)
  94. root-handle (nth result 0)
  95. dir-name (gobj/get root-handle "name")
  96. repo (str config/local-db-prefix dir-name)
  97. root-handle-path (str config/local-handle-prefix dir-name)
  98. _ (idb/set-item! root-handle-path root-handle)
  99. _ (nfs/add-nfs-file-handle! root-handle-path root-handle)
  100. result (nth result 1)
  101. files (-> (->db-files dir-name result)
  102. remove-ignore-files)
  103. _ (let [file-paths (set (map :file/path files))]
  104. (swap! path-handles (fn [handles]
  105. (->> handles
  106. (filter (fn [[path _handle]]
  107. (or
  108. (contains? file-paths
  109. (string/replace-first path (str dir-name "/") ""))
  110. (let [last-part (last (string/split path "/"))]
  111. (contains? #{config/app-name
  112. config/default-draw-directory
  113. config/default-journals-directory
  114. config/default-pages-directory}
  115. last-part)))))
  116. (into {})))))
  117. _ (set-files! @path-handles)
  118. markup-files (filter-markup-and-built-in-files files)]
  119. (-> (p/all (map (fn [file]
  120. (p/let [content (.text (:file/file file))]
  121. (assoc file :file/content content))) markup-files))
  122. (p/then (fn [result]
  123. _ (state/set-loading-files! false)
  124. (let [files (map #(dissoc % :file/file) result)]
  125. (repo-handler/start-repo-db-if-not-exists! repo {:db-type :local-native-fs})
  126. (repo-handler/load-repo-to-db! repo
  127. {:first-clone? true
  128. :nfs-files files})
  129. (state/add-repo! {:url repo :nfs? true}))))
  130. (p/catch (fn [error]
  131. (log/error :nfs/load-files-error error)))))
  132. (p/catch (fn [error]
  133. (when (not= "AbortError" (gobj/get error "name"))
  134. (log/error :nfs/open-dir-error error)))))))
  135. (defn open-file-picker
  136. "Shows a file picker that lets a user select a single existing file, returning a handle for the selected file. "
  137. ([]
  138. (open-file-picker {}))
  139. ([option]
  140. (.showOpenFilePicker js/window (bean/->js option))))
  141. (defn get-local-repo
  142. []
  143. (when-let [repo (state/get-current-repo)]
  144. (when (config/local-db? repo)
  145. repo)))
  146. (defn ask-permission
  147. [repo]
  148. (fn [close-fn]
  149. [:div
  150. [:p.text-gray-700
  151. "Grant native filesystem permission for directory: "
  152. [:b (config/get-local-dir repo)]]
  153. (ui/button
  154. "Grant"
  155. :on-click (fn []
  156. (nfs/check-directory-permission! repo)
  157. (close-fn)))]))
  158. (defn ask-permission-if-local? []
  159. (when-let [repo (get-local-repo)]
  160. (state/set-modal! (ask-permission repo))))
  161. (defn- compute-diffs
  162. [old-files new-files]
  163. (let [ks [:file/path :file/last-modified-at]
  164. ->set (fn [files ks]
  165. (when (seq files)
  166. (->> files
  167. (map #(select-keys % ks))
  168. set)))
  169. old-files (->set old-files ks)
  170. new-files (->set new-files ks)
  171. file-path-set-f (fn [col] (set (map :file/path col)))
  172. get-file-f (fn [files path] (some #(when (= (:file/path %) path) %) files))
  173. old-file-paths (file-path-set-f old-files)
  174. new-file-paths (file-path-set-f new-files)
  175. added (set/difference new-file-paths old-file-paths)
  176. deleted (set/difference old-file-paths new-file-paths)
  177. modified (set/difference new-file-paths added)]
  178. {:added added
  179. :modified modified
  180. :deleted deleted}))
  181. (defn- reload-dir!
  182. ([repo]
  183. (reload-dir! repo false))
  184. ([repo re-index?]
  185. (when (and repo (config/local-db? repo))
  186. (let [old-files (db/get-files-full repo)
  187. dir-name (config/get-local-dir repo)
  188. handle-path (str config/local-handle-prefix dir-name)
  189. path-handles (atom {})]
  190. (state/set-graph-syncing? true)
  191. (->
  192. (p/let [handle (idb/get-item handle-path)]
  193. (when handle
  194. (p/let [_ (when handle (nfs/verify-permission repo handle true))
  195. files-result (utils/getFiles handle true
  196. (fn [path handle]
  197. (swap! path-handles assoc path handle)))
  198. new-files (-> (->db-files dir-name files-result)
  199. remove-ignore-files)
  200. _ (let [file-paths (set (map :file/path new-files))]
  201. (swap! path-handles (fn [handles]
  202. (->> handles
  203. (filter (fn [[path _handle]]
  204. (contains? file-paths
  205. (string/replace-first path (str dir-name "/") ""))))
  206. (into {})))))
  207. _ (set-files! @path-handles)
  208. get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
  209. {:keys [added modified deleted] :as diffs} (compute-diffs old-files new-files)
  210. ;; Use the same labels as isomorphic-git
  211. rename-f (fn [typ col] (mapv (fn [file] {:type typ :path file}) col))
  212. _ (when (seq deleted)
  213. (let [deleted (doall
  214. (-> (map (fn [path] (if (= "/" (first path))
  215. path
  216. (str "/" path))) deleted)
  217. (distinct)))]
  218. (p/all (map (fn [path]
  219. (let [handle-path (str handle-path path)]
  220. (idb/remove-item! handle-path)
  221. (nfs/remove-nfs-file-handle! handle-path))) deleted))))
  222. added-or-modified (set (concat added modified))
  223. _ (when (seq added-or-modified)
  224. (p/all (map (fn [path]
  225. (when-let [handle (get @path-handles path)]
  226. (idb/set-item! (str handle-path path) handle))) added-or-modified)))]
  227. (-> (p/all (map (fn [path]
  228. (when-let [file (get-file-f path new-files)]
  229. (p/let [content (.text (:file/file file))]
  230. (assoc file :file/content content)))) added-or-modified))
  231. (p/then (fn [result]
  232. (let [files (map #(dissoc % :file/file :file/handle) result)
  233. non-modified? (fn [file]
  234. (let [content (:file/content file)
  235. old-content (:file/content (get-file-f (:file/path file) old-files))]
  236. (= content old-content)))
  237. non-modified-files (->> (filter non-modified? files)
  238. (map :file/path))
  239. [modified-files modified] (if re-index?
  240. [files (set modified)]
  241. [(remove non-modified? files) (set/difference (set modified) (set non-modified-files))])
  242. diffs (concat
  243. (rename-f "remove" deleted)
  244. (rename-f "add" added)
  245. (rename-f "modify" modified))]
  246. (when (or (and (seq diffs) (seq modified-files))
  247. (seq diffs) ; delete
  248. )
  249. (repo-handler/load-repo-to-db! repo
  250. {:diffs diffs
  251. :nfs-files modified-files})))))))))
  252. (p/catch (fn [error]
  253. (log/error :nfs/load-files-error error)))
  254. (p/finally (fn [_]
  255. (state/set-graph-syncing? false))))))))
  256. (defn refresh!
  257. [repo ok-handler]
  258. (when repo
  259. (state/set-nfs-refreshing! true)
  260. (p/let [_ (reload-dir! repo)
  261. _ (ok-handler)]
  262. (state/set-nfs-refreshing! false))))
  263. (defn rebuild-index!
  264. [repo ok-handler]
  265. (when repo
  266. (state/set-nfs-refreshing! true)
  267. ;; TODO: What about other relationships?
  268. (db-model/remove-all-aliases! repo)
  269. (p/let [_ (reload-dir! repo true)
  270. _ (ok-handler)]
  271. (state/set-nfs-refreshing! false))))
  272. (defn supported?
  273. []
  274. (utils/nfsSupported))