nfs.cljs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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. [clojure.set :as set]
  5. [clojure.string :as string]
  6. [frontend.config :as config]
  7. [frontend.db :as db]
  8. [frontend.fs :as fs]
  9. [frontend.fs.nfs :as nfs]
  10. [frontend.handler.common :as common-handler]
  11. [frontend.handler.global-config :as global-config-handler]
  12. [frontend.handler.repo :as repo-handler]
  13. [frontend.handler.route :as route-handler]
  14. [frontend.idb :as idb]
  15. [frontend.mobile.util :as mobile-util]
  16. [frontend.search :as search]
  17. [frontend.state :as state]
  18. [frontend.util :as util]
  19. [frontend.util.fs :as util-fs]
  20. [goog.object :as gobj]
  21. [lambdaisland.glogi :as log]
  22. [logseq.graph-parser.util :as gp-util]
  23. [promesa.core :as p]))
  24. (defn remove-ignore-files
  25. [files dir-name nfs?]
  26. (let [files (remove (fn [f]
  27. (let [path (:file/path f)]
  28. (or (string/starts-with? path ".git/")
  29. (string/includes? path ".git/")
  30. (and (util-fs/ignored-path? (if nfs? "" dir-name) path)
  31. (not= (:file/name f) ".gitignore")))))
  32. files)]
  33. (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
  34. %) files)]
  35. (if-let [file (:file/file ignore-file)]
  36. (p/let [content (.text file)]
  37. (when content
  38. (let [paths (set (common-handler/ignore-files content (map :file/path files)))]
  39. (when (seq paths)
  40. (filter (fn [f] (contains? paths (:file/path f))) files)))))
  41. (p/resolved files))
  42. (p/resolved files))))
  43. (defn- ->db-files
  44. ;; TODO(andelf): rm nfs? parameter
  45. [result nfs?]
  46. (->>
  47. (cond
  48. ;; TODO(andelf): use the same structure for both fields
  49. (mobile-util/native-platform?)
  50. (map (fn [{:keys [path content size mtime]}]
  51. {:file/path (gp-util/path-normalize path)
  52. :file/last-modified-at mtime
  53. :file/size size
  54. :file/content content})
  55. result)
  56. (util/electron?)
  57. (map (fn [{:keys [path stat content]}]
  58. (let [{:keys [mtime size]} stat]
  59. {:file/path (gp-util/path-normalize path)
  60. :file/last-modified-at mtime
  61. :file/size size
  62. :file/content content}))
  63. result)
  64. nfs?
  65. (map (fn [{:keys [path content size mtime type] :as file-obj}]
  66. (merge file-obj
  67. {:file/path (gp-util/path-normalize path)
  68. :file/last-modified-at mtime
  69. :file/size size
  70. :file/type type
  71. :file/content content}))
  72. result)
  73. :else ;; NFS backend
  74. result)
  75. (sort-by :file/path)))
  76. (defn- filter-markup-and-built-in-files
  77. [files]
  78. (filter (fn [file]
  79. (contains? (set/union config/markup-formats #{:css :edn})
  80. (keyword (util/get-file-ext (:file/path file)))))
  81. files))
  82. (defn- precheck-graph-dir
  83. "Check graph dir, notify user if:
  84. - Graph dir contains a nested graph, which should be avoided
  85. - Over 10000 files found in graph dir, which might cause performance issues"
  86. [_dir files]
  87. ;; disable this check for now
  88. (when (some #(string/ends-with? (:path %) "/logseq/config.edn") files)
  89. (state/pub-event!
  90. [:notification/show {:content "It seems that you are trying to open a Logseq graph folder with nested graph. Please unlink this graph and choose a correct folder."
  91. :status :warning
  92. :clear? false}]))
  93. (when (>= (count files) 10000)
  94. (state/pub-event!
  95. [:notification/show {:content "It seems that you are trying to open a Logseq graph folder that contains an excessive number of files, This might lead to performance issues."
  96. :status :warning
  97. :clear? true}])))
  98. ;; TODO: extract code for `ls-dir-files` and `reload-dir!`
  99. (defn ls-dir-files-with-handler!
  100. "Read files from directory and setup repo (for the first time setup a repo)"
  101. ([ok-handler] (ls-dir-files-with-handler! ok-handler nil))
  102. ([ok-handler {:keys [on-open-dir dir-result-fn picked-root-fn dir]}]
  103. (let [electron? (util/electron?)
  104. mobile-native? (mobile-util/native-platform?)
  105. nfs? (and (not electron?)
  106. (not mobile-native?))
  107. *repo (atom nil)]
  108. ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
  109. (->
  110. (p/let [result (if (fn? dir-result-fn)
  111. (dir-result-fn)
  112. (fs/open-dir dir))
  113. _ (when (fn? on-open-dir)
  114. (on-open-dir result))
  115. root-dir (:path result)
  116. ;; calling when root picked
  117. _ (when (fn? picked-root-fn) (picked-root-fn root-dir))
  118. repo (str config/local-db-prefix root-dir)]
  119. (state/set-loading-files! repo true)
  120. (when-not (or (state/home?) (state/setups-picker?))
  121. (route-handler/redirect-to-home! false))
  122. (reset! *repo repo)
  123. (when-not (string/blank? root-dir)
  124. (p/let [files (:files result)
  125. _ (precheck-graph-dir root-dir (:files result))
  126. files (-> (->db-files files nfs?)
  127. ;; filter again, in case fs backend does not handle this
  128. (remove-ignore-files root-dir nfs?))
  129. markup-files (filter-markup-and-built-in-files files)]
  130. (-> files
  131. (p/then (fn [result]
  132. ;; handle graphs txid
  133. (p/let [files (mapv #(dissoc % :file/file) result)
  134. graphs-txid-meta (util-fs/read-graphs-txid-info root-dir)
  135. graph-uuid (and (vector? graphs-txid-meta) (second graphs-txid-meta))]
  136. (if-let [exists-graph (state/get-sync-graph-by-id graph-uuid)]
  137. (state/pub-event!
  138. [:notification/show
  139. {:content (str "This graph already exists in \"" (:root exists-graph) "\"")
  140. :status :warning}])
  141. (p/do! (repo-handler/start-repo-db-if-not-exists! repo)
  142. (when (config/global-config-enabled?)
  143. (global-config-handler/restore-global-config!))
  144. (repo-handler/load-new-repo-to-db! repo
  145. {:new-graph? true
  146. :empty-graph? (nil? (seq markup-files))
  147. :file-objs files})
  148. (state/add-repo! {:url repo :nfs? true})
  149. (state/set-loading-files! repo false)
  150. (when ok-handler (ok-handler {:url repo}))
  151. (db/persist-if-idle! repo))))))
  152. (p/catch (fn [error]
  153. (log/error :nfs/load-files-error repo)
  154. (log/error :exception error)))))))
  155. (p/catch (fn [error]
  156. (log/error :exception error)
  157. (when mobile-native?
  158. (state/pub-event!
  159. [:notification/show {:content (str error) :status :error}]))
  160. (when (contains? #{"AbortError" "Error"} (gobj/get error "name"))
  161. (when @*repo (state/set-loading-files! @*repo false))
  162. (throw error))))
  163. (p/finally
  164. (fn []
  165. (state/set-loading-files! @*repo false)))))))
  166. (defn ls-dir-files-with-path!
  167. ([path] (ls-dir-files-with-path! path nil))
  168. ([path opts]
  169. (when-let [dir-result-fn
  170. (and path (fn []
  171. (p/let [files-result (fs/open-dir path)]
  172. files-result)))]
  173. (ls-dir-files-with-handler!
  174. (:ok-handler opts)
  175. (merge {:dir-result-fn dir-result-fn} opts)))))
  176. (defn- compute-diffs
  177. [old-files new-files]
  178. (let [ks [:file/path :file/last-modified-at :file/content]
  179. ->set (fn [files ks]
  180. (when (seq files)
  181. (->> files
  182. (map #(select-keys % ks))
  183. set)))
  184. old-files (->set old-files ks)
  185. new-files (->set new-files ks)
  186. file-path-set-f (fn [col] (set (map :file/path col)))
  187. get-file-f (fn [files path] (some #(when (= (:file/path %) path) %) files))
  188. old-file-paths (file-path-set-f old-files)
  189. new-file-paths (file-path-set-f new-files)
  190. added (set/difference new-file-paths old-file-paths)
  191. deleted (set/difference old-file-paths new-file-paths)
  192. modified (->> (set/intersection new-file-paths old-file-paths)
  193. (filter (fn [path]
  194. (not= (:file/content (get-file-f old-files path))
  195. (:file/content (get-file-f new-files path)))))
  196. (set))]
  197. (prn ::compute-diffs :added (count added) :modified (count modified) :deleted (count deleted))
  198. {:added added
  199. :modified modified
  200. :deleted deleted}))
  201. (defn- handle-diffs!
  202. "Compute directory diffs and (re)load repo"
  203. [repo nfs? old-files new-files re-index? ok-handler]
  204. (let [get-last-modified-at (fn [path] (some (fn [file]
  205. (when (= path (:file/path file))
  206. (:file/last-modified-at file)))
  207. new-files))
  208. get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
  209. {:keys [added modified deleted]} (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 :last-modified-at (get-last-modified-at file)}) col))
  212. added-or-modified (set (concat added modified))]
  213. (-> (p/all (map (fn [path]
  214. (when-let [file (get-file-f path new-files)]
  215. (p/let [content (if nfs?
  216. (.text (:file/file file))
  217. (:file/content file))]
  218. (assoc file :file/content content)))) added-or-modified))
  219. (p/then (fn [result]
  220. (let [files (map #(dissoc % :file/file :file/handle) result)
  221. [modified-files modified] (if re-index?
  222. [files (set modified)]
  223. (let [modified-files (filter (fn [file] (contains? added-or-modified (:file/path file))) files)]
  224. [modified-files (set modified)]))
  225. diffs (concat
  226. (rename-f "remove" deleted)
  227. (rename-f "add" added)
  228. (rename-f "modify" modified))]
  229. (when (or (and (seq diffs) (seq modified-files))
  230. (seq diffs))
  231. (-> (repo-handler/load-repo-to-db! repo
  232. {:diffs diffs
  233. :nfs-files modified-files
  234. :refresh? (not re-index?)
  235. :new-graph? re-index?})
  236. (p/then (fn [_state]
  237. (ok-handler)))
  238. (p/catch (fn [error]
  239. (js/console.error "load-repo-to-db" error)))))
  240. (when (and (util/electron?) (not re-index?))
  241. (db/transact! repo new-files))))))))
  242. (defn- reload-dir!
  243. "Handle refresh and re-index"
  244. [repo {:keys [re-index? ok-handler]
  245. :or {re-index? false}}]
  246. (when (and repo (config/local-db? repo))
  247. (let [old-files (db/get-files-full repo)
  248. repo-dir (config/get-local-dir repo)
  249. handle-path (str "handle/" repo-dir)
  250. electron? (util/electron?)
  251. mobile-native? (mobile-util/native-platform?)
  252. nfs? (and (not electron?)
  253. (not mobile-native?))]
  254. (when re-index?
  255. (state/set-graph-syncing? true))
  256. (->
  257. (p/let [handle (when-not electron? (idb/get-item handle-path))]
  258. (when (or handle electron? mobile-native?)
  259. (p/let [_ (when nfs? (nfs/verify-permission repo true))
  260. local-files-result (fs/get-files repo-dir)
  261. _ (when (config/global-config-enabled?)
  262. ;; reload global config into state
  263. (global-config-handler/restore-global-config!))
  264. new-files (-> (->db-files (:files local-files-result) nfs?)
  265. (remove-ignore-files repo-dir nfs?))]
  266. (handle-diffs! repo nfs? old-files new-files re-index? ok-handler))))
  267. (p/catch (fn [error]
  268. (log/error :nfs/load-files-error repo)
  269. (log/error :exception error)))
  270. (p/finally (fn [_]
  271. (state/set-graph-syncing? false)))))))
  272. (defn rebuild-index!
  273. [repo ok-handler]
  274. (let [ok-handler (fn []
  275. (ok-handler)
  276. (state/set-nfs-refreshing! false))]
  277. (when repo
  278. (state/set-nfs-refreshing! true)
  279. (search/reset-indice! repo)
  280. (db/remove-conn! repo)
  281. (db/clear-query-state!)
  282. (db/start-db-conn! repo)
  283. (reload-dir! repo {:re-index? true
  284. :ok-handler ok-handler}))))
  285. ;; TODO: move to frontend.handler.repo
  286. (defn refresh!
  287. [repo ok-handler]
  288. (let [ok-handler (fn []
  289. (ok-handler)
  290. (state/set-nfs-refreshing! false))]
  291. (when (and repo
  292. (not (state/unlinked-dir? (config/get-repo-dir repo))))
  293. (state/set-nfs-refreshing! true)
  294. (reload-dir! repo {:ok-handler ok-handler}))))
  295. (defn supported?
  296. []
  297. (or (utils/nfsSupported) (util/electron?)))