mirror of
https://github.com/logseq/logseq.git
synced 2026-04-25 22:54:48 +00:00
241 lines
10 KiB
Clojure
241 lines
10 KiB
Clojure
(ns frontend.handler.web.nfs
|
|
"The File System Access API, https://web.dev/file-system-access/."
|
|
(:require [cljs-bean.core :as bean]
|
|
[promesa.core :as p]
|
|
[goog.object :as gobj]
|
|
[goog.dom :as gdom]
|
|
[frontend.util :as util]
|
|
["/frontend/utils" :as utils]
|
|
[frontend.handler.repo :as repo-handler]
|
|
[frontend.handler.file :as file-handler]
|
|
[frontend.idb :as idb]
|
|
[frontend.state :as state]
|
|
[clojure.string :as string]
|
|
[clojure.set :as set]
|
|
[frontend.ui :as ui]
|
|
[frontend.fs :as fs]
|
|
[frontend.db :as db]
|
|
[frontend.config :as config]
|
|
[lambdaisland.glogi :as log]))
|
|
|
|
(defn remove-ignore-files
|
|
[files]
|
|
(let [files (remove (fn [f]
|
|
(string/starts-with? (:file/path f) ".git/"))
|
|
files)]
|
|
(if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
|
|
%) files)]
|
|
(if-let [file (:file/file ignore-file)]
|
|
(p/let [content (.text file)]
|
|
(when content
|
|
(let [paths (set (file-handler/ignore-files content (map :file/path files)))]
|
|
(when (seq paths)
|
|
(filter (fn [f] (contains? paths (:file/path f))) files)))))
|
|
(p/resolved files))
|
|
(p/resolved files))))
|
|
|
|
(defn- ->db-files
|
|
[dir-name result]
|
|
(let [result (flatten (bean/->clj result))]
|
|
(map (fn [file]
|
|
(let [handle (gobj/get file "handle")
|
|
get-attr #(gobj/get file %)
|
|
path (-> (get-attr "webkitRelativePath")
|
|
(string/replace-first (str dir-name "/") ""))]
|
|
{:file/name (get-attr "name")
|
|
:file/path path
|
|
:file/last-modified-at (get-attr "lastModified")
|
|
:file/size (get-attr "size")
|
|
:file/type (get-attr "type")
|
|
:file/file file
|
|
:file/handle handle})) result)))
|
|
|
|
(defn- filter-markup-and-built-in-files
|
|
[files]
|
|
(filter (fn [file]
|
|
(contains? (set/union config/markup-formats #{:css :edn})
|
|
(keyword (util/get-file-ext (:file/path file)))))
|
|
files))
|
|
|
|
(defn- set-files!
|
|
[handles]
|
|
(doseq [[path handle] handles]
|
|
(let [handle-path (str config/local-handle-prefix path)]
|
|
(idb/set-item! handle-path handle)
|
|
(fs/add-nfs-file-handle! handle-path handle))))
|
|
|
|
(defn ls-dir-files
|
|
[]
|
|
(let [path-handles (atom {})]
|
|
(->
|
|
(p/let [result (utils/openDirectory #js {:recursive true}
|
|
(fn [path handle]
|
|
(swap! path-handles assoc path handle)))
|
|
root-handle (nth result 0)
|
|
dir-name (gobj/get root-handle "name")
|
|
repo (str config/local-db-prefix dir-name)
|
|
root-handle-path (str config/local-handle-prefix dir-name)
|
|
_ (idb/set-item! root-handle-path root-handle)
|
|
_ (fs/add-nfs-file-handle! root-handle-path root-handle)
|
|
_ (set-files! @path-handles)
|
|
result (nth result 1)
|
|
files (-> (->db-files dir-name result)
|
|
remove-ignore-files)
|
|
markup-files (filter-markup-and-built-in-files files)]
|
|
(-> (p/all (map (fn [file]
|
|
(p/let [content (.text (:file/file file))]
|
|
(assoc file :file/content content))) markup-files))
|
|
(p/then (fn [result]
|
|
(let [files (map #(dissoc % :file/file) result)]
|
|
(repo-handler/start-repo-db-if-not-exists! repo {:db-type :local-native-fs})
|
|
(repo-handler/load-repo-to-db! repo
|
|
{:first-clone? true
|
|
:nfs-files files})
|
|
|
|
(state/add-repo! {:url repo :nfs? true}))))
|
|
(p/catch (fn [error]
|
|
(log/error :nfs/load-files-error error)))))
|
|
(p/catch (fn [error]
|
|
(log/error :nfs/open-dir-error error))))))
|
|
|
|
(defn open-file-picker
|
|
"Shows a file picker that lets a user select a single existing file, returning a handle for the selected file. "
|
|
([]
|
|
(open-file-picker {}))
|
|
([option]
|
|
(js/window.showOpenFilePicker (bean/->js option))))
|
|
|
|
(defn get-local-repo
|
|
[]
|
|
(when-let [repo (state/get-current-repo)]
|
|
(when (config/local-db? repo)
|
|
repo)))
|
|
|
|
(defn check-directory-permission!
|
|
[repo]
|
|
(p/let [handle (idb/get-item (str "handle/" repo))]
|
|
(when handle
|
|
(utils/verifyPermission handle true))))
|
|
|
|
(defn ask-permission
|
|
[repo]
|
|
(fn [close-fn]
|
|
[:div
|
|
[:p.text-gray-700
|
|
"Grant native filesystem permission for directory: "
|
|
[:b (config/get-local-dir repo)]]
|
|
(ui/button
|
|
"Grant"
|
|
:on-click (fn []
|
|
(check-directory-permission! repo)
|
|
(close-fn)))]))
|
|
|
|
(defn trigger-check! []
|
|
(when-let [repo (get-local-repo)]
|
|
(state/set-modal! (ask-permission repo))))
|
|
|
|
(defn- compute-diffs
|
|
[old-files new-files]
|
|
(let [ks [:file/path :file/last-modified-at]
|
|
->set (fn [files ks]
|
|
(when (seq files)
|
|
(->> files
|
|
(map #(select-keys % ks))
|
|
set)))
|
|
old-files (->set old-files ks)
|
|
new-files (->set new-files ks)
|
|
file-path-set-f (fn [col] (set (map :file/path col)))
|
|
get-file-f (fn [files path] (some #(when (= (:file/path %) path) %) files))
|
|
old-file-paths (file-path-set-f old-files)
|
|
new-file-paths (file-path-set-f new-files)
|
|
added (set/difference new-file-paths old-file-paths)
|
|
deleted (set/difference old-file-paths new-file-paths)
|
|
modified (let [modified (set/difference new-file-paths added)]
|
|
(when (seq modified)
|
|
(filter (fn [path]
|
|
(let [old-file (get-file-f old-files path)
|
|
new-file (get-file-f new-files path)]
|
|
;; TODO: the `last-modified-at` attribute in the db is always after
|
|
;; the file in the local file sytem because we transact to the db first and write to the
|
|
;; file system later.
|
|
;; It doesn't mean this is a bug, but it could impact the performance.
|
|
(> (:file/last-modified-at new-file)
|
|
(:file/last-modified-at old-file))))
|
|
modified)))]
|
|
{:added added
|
|
:modified modified
|
|
:deleted deleted}))
|
|
|
|
(defn- reload-dir!
|
|
[repo]
|
|
(when (and repo (config/local-db? repo))
|
|
(let [old-files (db/get-files-full repo)
|
|
dir-name (config/get-local-dir repo)
|
|
handle-path (str config/local-handle-prefix dir-name)
|
|
path-handles (atom {})]
|
|
(state/set-graph-syncing? true)
|
|
(p/let [handle (idb/get-item handle-path)
|
|
files-result (utils/getFiles handle true
|
|
(fn [path handle]
|
|
(swap! path-handles assoc path handle)))
|
|
_ (set-files! @path-handles)
|
|
new-files (-> (->db-files dir-name files-result)
|
|
remove-ignore-files)
|
|
get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
|
|
{:keys [added modified deleted] :as diffs} (compute-diffs old-files new-files)
|
|
;; Use the same labels as isomorphic-git
|
|
rename-f (fn [typ col] (mapv (fn [file] {:type typ :path file}) col))
|
|
_ (when (seq deleted)
|
|
(p/all (map #(idb/remove-item! (str handle-path %)) deleted)))
|
|
added-or-modified (set (concat added modified))
|
|
_ (when (seq added-or-modified)
|
|
(p/all (map (fn [path]
|
|
(when-let [handle (get @path-handles path)]
|
|
(idb/set-item! (str handle-path path) handle))) added-or-modified)))]
|
|
(-> (p/all (map (fn [path]
|
|
(when-let [file (get-file-f path new-files)]
|
|
(p/let [content (.text (:file/file file))]
|
|
(assoc file :file/content content)))) added-or-modified))
|
|
(p/then (fn [result]
|
|
(let [files (map #(dissoc % :file/file :file/handle) result)
|
|
non-modified? (fn [file]
|
|
(let [content (:file/content file)
|
|
old-content (:file/content (get-file-f (:file/path file) old-files))]
|
|
(= content old-content)))
|
|
non-modified-files (->> (filter non-modified? files)
|
|
(map :file/path))
|
|
modified-files (remove non-modified? files)
|
|
modified (set/difference (set modified) (set non-modified-files))
|
|
diffs (concat
|
|
(rename-f "remove" deleted)
|
|
(rename-f "add" added)
|
|
(rename-f "modify" modified))]
|
|
(when (or (and (seq diffs) (seq modified-files))
|
|
(seq diffs) ; delete
|
|
)
|
|
(repo-handler/load-repo-to-db! repo
|
|
{:diffs diffs
|
|
:nfs-files modified-files})))))
|
|
(p/catch (fn [error]
|
|
(log/error :nfs/load-files-error error)))
|
|
(p/finally (fn [_]
|
|
(state/set-graph-syncing? false))))))))
|
|
|
|
(defn- refresh!
|
|
[repo]
|
|
(when repo
|
|
(reload-dir! repo)
|
|
;; check permission is too slow
|
|
;; (->
|
|
;; (p/let [verified? (check-directory-permission! repo)]
|
|
;; (prn {:verified? verified?})
|
|
;; (when verified?
|
|
;; (reload-dir! repo)))
|
|
;; (p/catch (fn [error]
|
|
;; (log/error :nfs/refresh-error error))))
|
|
))
|
|
|
|
(defn supported?
|
|
[]
|
|
(utils/nfsSupported))
|