nfs.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. (ns frontend.handler.web.nfs
  2. "The File System Access API, https://web.dev/file-system-access/."
  3. (:require ["/frontend/utils" :as utils]
  4. [cljs-bean.core :as bean]
  5. [clojure.core.async :as async]
  6. [clojure.set :as set]
  7. [clojure.string :as string]
  8. [frontend.config :as config]
  9. [frontend.db :as db]
  10. [frontend.encrypt :as encrypt]
  11. [frontend.fs :as fs]
  12. [frontend.fs.nfs :as nfs]
  13. [frontend.handler.common :as common-handler]
  14. [frontend.handler.global-config :as global-config-handler]
  15. [frontend.handler.repo :as repo-handler]
  16. [frontend.handler.route :as route-handler]
  17. [frontend.idb :as idb]
  18. [frontend.mobile.util :as mobile-util]
  19. [frontend.search :as search]
  20. [frontend.state :as state]
  21. [frontend.util :as util]
  22. [frontend.util.fs :as util-fs]
  23. [goog.object :as gobj]
  24. [lambdaisland.glogi :as log]
  25. [logseq.graph-parser.config :as gp-config]
  26. [logseq.graph-parser.util :as gp-util]
  27. [promesa.core :as p]))
  28. (defn remove-ignore-files
  29. [files dir-name nfs?]
  30. (let [files (remove (fn [f]
  31. (let [path (:file/path f)]
  32. (or (string/starts-with? path ".git/")
  33. (string/includes? path ".git/")
  34. (and (util-fs/ignored-path? (if nfs? "" dir-name) path)
  35. (not= (:file/name f) ".gitignore")))))
  36. files)]
  37. (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
  38. %) files)]
  39. (if-let [file (:file/file ignore-file)]
  40. (p/let [content (.text file)]
  41. (when content
  42. (let [paths (set (common-handler/ignore-files content (map :file/path files)))]
  43. (when (seq paths)
  44. (filter (fn [f] (contains? paths (:file/path f))) files)))))
  45. (p/resolved files))
  46. (p/resolved files))))
  47. (defn- ->db-files
  48. [mobile-native? electron? dir-name result]
  49. (->>
  50. (cond
  51. mobile-native?
  52. (map (fn [{:keys [uri content size mtime]}]
  53. {:file/path (gp-util/path-normalize uri)
  54. :file/last-modified-at mtime
  55. :file/size size
  56. :file/content content})
  57. result)
  58. electron?
  59. (map (fn [{:keys [path stat content]}]
  60. (let [{:keys [mtime size]} stat]
  61. {:file/path (gp-util/path-normalize path)
  62. :file/last-modified-at mtime
  63. :file/size size
  64. :file/content content}))
  65. result)
  66. :else
  67. (let [result (flatten (bean/->clj result))]
  68. (map (fn [file]
  69. (let [handle (gobj/get file "handle")
  70. get-attr #(gobj/get file %)
  71. path (-> (get-attr "webkitRelativePath")
  72. (string/replace-first (str dir-name "/") ""))]
  73. {:file/name (get-attr "name")
  74. :file/path (gp-util/path-normalize path)
  75. :file/last-modified-at (get-attr "lastModified")
  76. :file/size (get-attr "size")
  77. :file/type (get-attr "type")
  78. :file/file file
  79. :file/handle handle})) result)))
  80. (sort-by :file/path)))
  81. (defn- filter-markup-and-built-in-files
  82. [files]
  83. (filter (fn [file]
  84. (contains? (set/union config/markup-formats #{:css :edn})
  85. (keyword (util/get-file-ext (:file/path file)))))
  86. files))
  87. (defn- set-batch!
  88. [handles]
  89. (let [handles (map (fn [[path handle]]
  90. {:key path
  91. :value handle}) handles)]
  92. (idb/set-batch! handles)))
  93. (defn- set-files-aux!
  94. [handles]
  95. (when (seq handles)
  96. (let [[h t] (split-at 50 handles)]
  97. (p/let [_ (p/promise (fn [_]
  98. (js/setTimeout (fn []
  99. (p/resolved nil)) 10)))
  100. _ (set-batch! h)]
  101. (when (seq t)
  102. (set-files-aux! t))))))
  103. (defn- set-files!
  104. [handles]
  105. (let [handles (map (fn [[path handle]]
  106. (let [handle-path (str config/local-handle-prefix path)]
  107. [handle-path handle]))
  108. handles)]
  109. (doseq [[path handle] handles]
  110. (nfs/add-nfs-file-handle! path handle))
  111. (set-files-aux! handles)))
  112. ;; TODO: extract code for `ls-dir-files` and `reload-dir!`
  113. (defn ^:large-vars/cleanup-todo ls-dir-files-with-handler!
  114. ([ok-handler] (ls-dir-files-with-handler! ok-handler nil))
  115. ([ok-handler {:keys [empty-dir?-or-pred dir-result-fn]}]
  116. (let [path-handles (atom {})
  117. electron? (util/electron?)
  118. mobile-native? (mobile-util/native-platform?)
  119. nfs? (and (not electron?)
  120. (not mobile-native?))
  121. *repo (atom nil)]
  122. ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
  123. (->
  124. (p/let [result (if (fn? dir-result-fn)
  125. (dir-result-fn {:path-handles path-handles :nfs? nfs?})
  126. (fs/open-dir (fn [path handle]
  127. (when nfs?
  128. (swap! path-handles assoc path handle)))))
  129. _ (when-not (nil? empty-dir?-or-pred)
  130. (cond
  131. (boolean? empty-dir?-or-pred)
  132. (and (not-empty (second result))
  133. (throw (js/Error. "EmptyDirOnly")))
  134. (fn? empty-dir?-or-pred)
  135. (empty-dir?-or-pred result)))
  136. root-handle (first result)
  137. dir-name (if nfs?
  138. (gobj/get root-handle "name")
  139. root-handle)
  140. repo (str config/local-db-prefix dir-name)
  141. _ (state/set-loading-files! repo true)
  142. _ (when-not (or (state/home?) (state/setups-picker?))
  143. (route-handler/redirect-to-home! false))]
  144. (reset! *repo repo)
  145. (when-not (string/blank? dir-name)
  146. (p/let [root-handle-path (str config/local-handle-prefix dir-name)
  147. _ (when nfs?
  148. (idb/set-item! root-handle-path root-handle)
  149. (nfs/add-nfs-file-handle! root-handle-path root-handle))
  150. result (nth result 1)
  151. files (-> (->db-files mobile-native? electron? dir-name result)
  152. (remove-ignore-files dir-name nfs?))
  153. _ (when nfs?
  154. (let [file-paths (set (map :file/path files))]
  155. (swap! path-handles (fn [handles]
  156. (->> handles
  157. (filter (fn [[path _handle]]
  158. (or
  159. (contains? file-paths
  160. (string/replace-first path (str dir-name "/") ""))
  161. (let [last-part (last (string/split path "/"))]
  162. (contains? #{config/app-name
  163. gp-config/default-draw-directory
  164. (config/get-journals-directory)
  165. (config/get-whiteboards-directory)
  166. (config/get-pages-directory)}
  167. last-part)))))
  168. (into {})))))
  169. (set-files! @path-handles))
  170. markup-files (filter-markup-and-built-in-files files)]
  171. (-> (p/all (map (fn [file]
  172. (p/let [content (if nfs?
  173. (.text (:file/file file))
  174. (:file/content file))
  175. content (encrypt/decrypt content)]
  176. (assoc file :file/content content))) markup-files))
  177. (p/then (fn [result]
  178. (p/let [files (map #(dissoc % :file/file) result)
  179. graphs-txid-meta (util-fs/read-graphs-txid-info dir-name)
  180. graph-uuid (and (vector? graphs-txid-meta) (second graphs-txid-meta))]
  181. (if-let [exists-graph (state/get-sync-graph-by-id graph-uuid)]
  182. (state/pub-event!
  183. [:notification/show
  184. {:content (str "This graph already exists in \"" (:root exists-graph) "\"")
  185. :status :warning}])
  186. (do
  187. (repo-handler/start-repo-db-if-not-exists! repo)
  188. (async/go
  189. (let [_finished? (async/<! (repo-handler/load-repo-to-db! repo
  190. {:new-graph? true
  191. :empty-graph? (nil? (seq markup-files))
  192. :nfs-files files}))]
  193. (state/add-repo! {:url repo :nfs? true})
  194. (state/set-loading-files! repo false)
  195. (when ok-handler (ok-handler {:url repo}))
  196. (fs/watch-dir! dir-name)
  197. (db/persist-if-idle! repo))))))))
  198. (p/catch (fn [error]
  199. (log/error :nfs/load-files-error repo)
  200. (log/error :exception error)))))))
  201. (p/catch (fn [error]
  202. (log/error :exception error)
  203. (when mobile-native?
  204. (state/pub-event!
  205. [:notification/show {:content (str error) :status :error}]))
  206. (when (contains? #{"AbortError" "Error"} (gobj/get error "name"))
  207. (when @*repo (state/set-loading-files! @*repo false))
  208. (throw error))))
  209. (p/finally
  210. (fn []
  211. (state/set-loading-files! @*repo false)))))))
  212. (defn ls-dir-files-with-path!
  213. ([path] (ls-dir-files-with-path! path nil))
  214. ([path opts]
  215. (when-let [dir-result-fn
  216. (and path (fn [{:keys [path-handles nfs?]}]
  217. (p/let [files-result (fs/get-files
  218. path
  219. (fn [path handle]
  220. (when nfs?
  221. (swap! path-handles assoc path handle))))]
  222. [path files-result])))]
  223. (ls-dir-files-with-handler!
  224. (:ok-handler opts)
  225. (merge {:dir-result-fn dir-result-fn} opts)))))
  226. (defn- compute-diffs
  227. [old-files new-files]
  228. (let [ks [:file/path :file/last-modified-at :file/content]
  229. ->set (fn [files ks]
  230. (when (seq files)
  231. (->> files
  232. (map #(select-keys % ks))
  233. set)))
  234. old-files (->set old-files ks)
  235. new-files (->set new-files ks)
  236. file-path-set-f (fn [col] (set (map :file/path col)))
  237. get-file-f (fn [files path] (some #(when (= (:file/path %) path) %) files))
  238. old-file-paths (file-path-set-f old-files)
  239. new-file-paths (file-path-set-f new-files)
  240. added (set/difference new-file-paths old-file-paths)
  241. deleted (set/difference old-file-paths new-file-paths)
  242. modified (->> (set/intersection new-file-paths old-file-paths)
  243. (filter (fn [path]
  244. (not= (:file/content (get-file-f old-files path))
  245. (:file/content (get-file-f new-files path)))))
  246. (set))]
  247. {:added added
  248. :modified modified
  249. :deleted deleted}))
  250. (defn- handle-diffs!
  251. [repo nfs? old-files new-files handle-path path-handles re-index?]
  252. (let [get-last-modified-at (fn [path] (some (fn [file]
  253. (when (= path (:file/path file))
  254. (:file/last-modified-at file)))
  255. new-files))
  256. get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
  257. {:keys [added modified deleted]} (compute-diffs old-files new-files)
  258. ;; Use the same labels as isomorphic-git
  259. rename-f (fn [typ col] (mapv (fn [file] {:type typ :path file :last-modified-at (get-last-modified-at file)}) col))
  260. _ (when (and nfs? (seq deleted))
  261. (let [deleted (doall
  262. (-> (map (fn [path] (if (= "/" (first path))
  263. path
  264. (str "/" path))) deleted)
  265. (distinct)))]
  266. (p/all (map (fn [path]
  267. (let [handle-path (str handle-path path)]
  268. (idb/remove-item! handle-path)
  269. (nfs/remove-nfs-file-handle! handle-path))) deleted))))
  270. added-or-modified (set (concat added modified))
  271. _ (when (and nfs? (seq added-or-modified))
  272. (p/all (map (fn [path]
  273. (when-let [handle (get @path-handles path)]
  274. (idb/set-item! (str handle-path path) handle))) added-or-modified)))]
  275. (-> (p/all (map (fn [path]
  276. (when-let [file (get-file-f path new-files)]
  277. (p/let [content (if nfs?
  278. (.text (:file/file file))
  279. (:file/content file))
  280. content (encrypt/decrypt content)]
  281. (assoc file :file/content content)))) added-or-modified))
  282. (p/then (fn [result]
  283. (let [files (map #(dissoc % :file/file :file/handle) result)
  284. [modified-files modified] (if re-index?
  285. [files (set modified)]
  286. (let [modified-files (filter (fn [file] (contains? added-or-modified (:file/path file))) files)]
  287. [modified-files (set modified)]))
  288. diffs (concat
  289. (rename-f "remove" deleted)
  290. (rename-f "add" added)
  291. (rename-f "modify" modified))]
  292. (when (or (and (seq diffs) (seq modified-files))
  293. (seq diffs))
  294. (comment "re-index a local graph is handled here")
  295. (repo-handler/load-repo-to-db! repo
  296. {:diffs diffs
  297. :nfs-files modified-files
  298. :refresh? (not re-index?)
  299. :new-graph? re-index?}))
  300. (when (and (util/electron?) (not re-index?))
  301. (db/transact! repo new-files))))))))
  302. (defn- reload-dir!
  303. ([repo]
  304. (reload-dir! repo false))
  305. ([repo re-index?]
  306. (when (and repo (config/local-db? repo))
  307. (let [old-files (db/get-files-full repo)
  308. dir-name (config/get-local-dir repo)
  309. handle-path (str config/local-handle-prefix dir-name)
  310. path-handles (atom {})
  311. electron? (util/electron?)
  312. mobile-native? (mobile-util/native-platform?)
  313. nfs? (and (not electron?)
  314. (not mobile-native?))]
  315. (when re-index?
  316. (state/set-graph-syncing? true))
  317. (->
  318. (p/let [handle (when-not electron? (idb/get-item handle-path))]
  319. (when (or handle electron? mobile-native?) ; electron doesn't store the file handle
  320. (p/let [_ (when handle (nfs/verify-permission repo handle true))
  321. local-files-result
  322. (fs/get-files (if nfs? handle
  323. (config/get-local-dir repo))
  324. (fn [path handle]
  325. (when nfs?
  326. (swap! path-handles assoc path handle))))
  327. new-local-files (-> (->db-files mobile-native? electron? dir-name local-files-result)
  328. (remove-ignore-files dir-name nfs?))
  329. new-global-files (if (config/global-config-enabled?)
  330. (p/let [global-files-result (fs/get-files
  331. (global-config-handler/global-config-dir)
  332. (constantly nil))
  333. global-files (-> (->db-files mobile-native? electron? (global-config-handler/global-config-dir) global-files-result)
  334. (remove-ignore-files (global-config-handler/global-config-dir) nfs?))]
  335. global-files)
  336. (p/resolved []))
  337. new-files (concat new-local-files new-global-files)
  338. _ (when nfs?
  339. (let [file-paths (set (map :file/path new-files))]
  340. (swap! path-handles (fn [handles]
  341. (->> handles
  342. (filter (fn [[path _handle]]
  343. (contains? file-paths
  344. (string/replace-first path (str dir-name "/") ""))))
  345. (into {})))))
  346. (set-files! @path-handles))]
  347. (handle-diffs! repo nfs? old-files new-files handle-path path-handles re-index?))))
  348. (p/catch (fn [error]
  349. (log/error :nfs/load-files-error repo)
  350. (log/error :exception error)))
  351. (p/finally (fn [_]
  352. (state/set-graph-syncing? false))))))))
  353. (defn rebuild-index!
  354. [repo ok-handler]
  355. (when repo
  356. (state/set-nfs-refreshing! true)
  357. (search/reset-indice! repo)
  358. (db/remove-conn! repo)
  359. (db/clear-query-state!)
  360. (db/start-db-conn! repo)
  361. (p/let [_ (reload-dir! repo true)
  362. _ (ok-handler)]
  363. (state/set-nfs-refreshing! false))))
  364. ;; TODO: move to frontend.handler.repo
  365. (defn refresh!
  366. [repo ok-handler]
  367. (when (and repo
  368. (not (state/unlinked-dir? (config/get-repo-dir repo))))
  369. (state/set-nfs-refreshing! true)
  370. (p/let [_ (reload-dir! repo)
  371. _ (ok-handler)]
  372. (state/set-nfs-refreshing! false))))
  373. (defn supported?
  374. []
  375. (or (utils/nfsSupported) (util/electron?)))