|
@@ -1,366 +0,0 @@
|
|
|
-(ns frontend.fs.nfs
|
|
|
|
|
- "Browser File System API based fs implementation.
|
|
|
|
|
-
|
|
|
|
|
- Rationale:
|
|
|
|
|
- - nfs-file-handles-cache stores all file & directory handle
|
|
|
|
|
- - idb stores top-level directory handle
|
|
|
|
|
- - readdir/get-files is called by re-index and initial watcher to init all handles"
|
|
|
|
|
- (:require [frontend.fs.protocol :as protocol]
|
|
|
|
|
- [frontend.util :as util]
|
|
|
|
|
- [clojure.string :as string]
|
|
|
|
|
- [frontend.idb :as idb]
|
|
|
|
|
- [promesa.core :as p]
|
|
|
|
|
- [lambdaisland.glogi :as log]
|
|
|
|
|
- [goog.object :as gobj]
|
|
|
|
|
- [frontend.db :as db]
|
|
|
|
|
- [frontend.config :as config]
|
|
|
|
|
- [frontend.state :as state]
|
|
|
|
|
- [frontend.handler.notification :as notification]
|
|
|
|
|
- ["/frontend/utils" :as utils]
|
|
|
|
|
- [logseq.common.util :as common-util]
|
|
|
|
|
- [logseq.common.path :as path]))
|
|
|
|
|
-
|
|
|
|
|
-;; Cache the file handles in the memory so that
|
|
|
|
|
-;; the browser will not keep asking permissions.
|
|
|
|
|
-(defonce nfs-file-handles-cache (atom {}))
|
|
|
|
|
-
|
|
|
|
|
-(defn- get-nfs-file-handle
|
|
|
|
|
- [handle-path]
|
|
|
|
|
- (get @nfs-file-handles-cache handle-path))
|
|
|
|
|
-
|
|
|
|
|
-(defn add-nfs-file-handle!
|
|
|
|
|
- [handle-path handle]
|
|
|
|
|
- (prn ::DEBUG "add-nfs-file-handle!" handle-path)
|
|
|
|
|
- (swap! nfs-file-handles-cache assoc handle-path handle))
|
|
|
|
|
-
|
|
|
|
|
-(defn remove-nfs-file-handle!
|
|
|
|
|
- [handle-path]
|
|
|
|
|
- (swap! nfs-file-handles-cache dissoc handle-path))
|
|
|
|
|
-
|
|
|
|
|
-(defn- nfs-saved-handler
|
|
|
|
|
- [repo path file]
|
|
|
|
|
- (when-let [last-modified (gobj/get file "lastModified")]
|
|
|
|
|
- ;; TODO: extract
|
|
|
|
|
- (let [path (if (= \/ (first path))
|
|
|
|
|
- (subs path 1)
|
|
|
|
|
- path)]
|
|
|
|
|
- ;; Bad code
|
|
|
|
|
- (db/set-file-last-modified-at! repo path last-modified))))
|
|
|
|
|
-
|
|
|
|
|
-(defn- verify-handle-permission
|
|
|
|
|
- [handle read-write?]
|
|
|
|
|
- (utils/verifyPermission handle read-write?))
|
|
|
|
|
-
|
|
|
|
|
-(defn verify-permission
|
|
|
|
|
- [repo read-write?]
|
|
|
|
|
- (let [repo (or repo (state/get-current-repo))
|
|
|
|
|
- repo-dir (config/get-repo-dir repo)
|
|
|
|
|
- handle-path (str "handle/" repo-dir)
|
|
|
|
|
- handle (get-nfs-file-handle handle-path)]
|
|
|
|
|
- (p/then
|
|
|
|
|
- (utils/verifyPermission handle read-write?)
|
|
|
|
|
- (fn []
|
|
|
|
|
- (state/set-state! [:nfs/user-granted? repo] true)
|
|
|
|
|
- true))))
|
|
|
|
|
-
|
|
|
|
|
-(defn check-directory-permission!
|
|
|
|
|
- [repo]
|
|
|
|
|
- (when (config/local-file-based-graph? repo)
|
|
|
|
|
- (p/let [repo-dir (config/get-repo-dir repo)
|
|
|
|
|
- handle-path (str "handle/" repo-dir)
|
|
|
|
|
- handle (idb/get-item handle-path)]
|
|
|
|
|
- (when handle
|
|
|
|
|
- (add-nfs-file-handle! handle-path handle)
|
|
|
|
|
- (verify-permission repo true)))))
|
|
|
|
|
-
|
|
|
|
|
-(defn- contents-matched?
|
|
|
|
|
- [disk-content db-content]
|
|
|
|
|
- (when (and (string? disk-content) (string? db-content))
|
|
|
|
|
- (= (string/trim disk-content) (string/trim db-content))))
|
|
|
|
|
-
|
|
|
|
|
-(defn- await-permission-granted
|
|
|
|
|
- "Guard against File System Access API permission, avoiding early access before granted"
|
|
|
|
|
- [repo]
|
|
|
|
|
- (if (state/nfs-user-granted? repo)
|
|
|
|
|
- (p/resolved true)
|
|
|
|
|
- (js/Promise. (fn [resolve reject]
|
|
|
|
|
- (let [timer (atom nil)
|
|
|
|
|
- timer' (js/setInterval (fn []
|
|
|
|
|
- (when (state/nfs-user-granted? repo)
|
|
|
|
|
- (js/clearInterval @timer)
|
|
|
|
|
- (resolve true)))
|
|
|
|
|
- 1000)
|
|
|
|
|
- _ (reset! timer timer')]
|
|
|
|
|
- (js/setTimeout (fn []
|
|
|
|
|
- (js/clearInterval timer)
|
|
|
|
|
- (reject false))
|
|
|
|
|
- 100000))))))
|
|
|
|
|
-
|
|
|
|
|
-(defn await-get-nfs-file-handle
|
|
|
|
|
- "for accessing File handle outside, ensuring user granted."
|
|
|
|
|
- [repo handle-path]
|
|
|
|
|
- (p/let [_ (await-permission-granted repo)]
|
|
|
|
|
- (get-nfs-file-handle handle-path)))
|
|
|
|
|
-
|
|
|
|
|
-(defn- readdir-and-reload-all-handles
|
|
|
|
|
- "Return list of filenames"
|
|
|
|
|
- [root-dir root-handle]
|
|
|
|
|
- (p/let [files (utils/getFiles root-handle
|
|
|
|
|
- true
|
|
|
|
|
- (fn [path entry]
|
|
|
|
|
- (let [handle-path (str "handle/" path)]
|
|
|
|
|
- ;; Same for all handles here, even for directories and ignored directories(for backing up)
|
|
|
|
|
- ;; FileSystemDirectoryHandle or FileSystemFileHandle
|
|
|
|
|
- (when-not (string/includes? path "/.")
|
|
|
|
|
- (add-nfs-file-handle! handle-path entry)))))]
|
|
|
|
|
- (->> files
|
|
|
|
|
- (remove (fn [file]
|
|
|
|
|
- (let [rpath (string/replace-first (.-webkitRelativePath file) (str root-dir "/") "")
|
|
|
|
|
- ext (util/get-file-ext rpath)]
|
|
|
|
|
- (or (string/blank? rpath)
|
|
|
|
|
- (string/starts-with? rpath ".")
|
|
|
|
|
- (string/starts-with? rpath "logseq/bak")
|
|
|
|
|
- ; (string/starts-with? rpath "logseq/version-files")
|
|
|
|
|
- (not (contains? #{"md" "org" "excalidraw" "edn" "css"} ext))))))
|
|
|
|
|
- (map (fn [file]
|
|
|
|
|
- (-> (.-webkitRelativePath file)
|
|
|
|
|
- common-util/path-normalize))))))
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-(defn- get-files-and-reload-all-handles
|
|
|
|
|
- "Return list of file objects"
|
|
|
|
|
- [root-dir root-handle]
|
|
|
|
|
- (p/let [files (utils/getFiles root-handle
|
|
|
|
|
- true
|
|
|
|
|
- (fn [path entry]
|
|
|
|
|
- (let [handle-path (str "handle/" path)]
|
|
|
|
|
- ;; Same for all handles here, even for directories and ignored directories(for backing up)
|
|
|
|
|
- ;; FileSystemDirectoryHandle or FileSystemFileHandle
|
|
|
|
|
- (when-not (string/includes? path "/.")
|
|
|
|
|
- (add-nfs-file-handle! handle-path entry)))))]
|
|
|
|
|
- (p/all (->> files
|
|
|
|
|
- (remove (fn [file]
|
|
|
|
|
- (let [rpath (string/replace-first (.-webkitRelativePath file) (str root-dir "/") "")
|
|
|
|
|
- ext (util/get-file-ext rpath)]
|
|
|
|
|
- (or (string/blank? rpath)
|
|
|
|
|
- (string/starts-with? rpath ".")
|
|
|
|
|
- (string/starts-with? rpath "logseq/bak")
|
|
|
|
|
- (string/starts-with? rpath "logseq/version-files")
|
|
|
|
|
- (not (contains? #{"md" "org" "excalidraw" "edn" "css"} ext))))))
|
|
|
|
|
- ;; Read out using .text, Promise<string>
|
|
|
|
|
- (map (fn [file]
|
|
|
|
|
- (p/let [content (.text file)]
|
|
|
|
|
- {:name (.-name file)
|
|
|
|
|
- :path (-> (.-webkitRelativePath file)
|
|
|
|
|
- common-util/path-normalize)
|
|
|
|
|
- :mtime (.-lastModified file)
|
|
|
|
|
- :size (.-size file)
|
|
|
|
|
- :type (.-kind (.-handle file))
|
|
|
|
|
- :content content
|
|
|
|
|
- :file/file file})))))))
|
|
|
|
|
-
|
|
|
|
|
-(defrecord ^:large-vars/cleanup-todo Nfs []
|
|
|
|
|
- protocol/Fs
|
|
|
|
|
- (mkdir! [_this dir]
|
|
|
|
|
- (let [dir (path/path-normalize dir)
|
|
|
|
|
- parent-dir (path/parent dir)
|
|
|
|
|
-
|
|
|
|
|
- parent-handle-path (str "handle/" parent-dir)]
|
|
|
|
|
- (-> (p/let [parent-handle (or (get-nfs-file-handle parent-handle-path)
|
|
|
|
|
- (idb/get-item parent-handle-path))
|
|
|
|
|
- _ (when parent-handle (verify-handle-permission parent-handle true))]
|
|
|
|
|
- (when parent-handle
|
|
|
|
|
- (p/let [new-dir-name (path/filename dir)
|
|
|
|
|
- new-handle (.getDirectoryHandle ^js parent-handle new-dir-name
|
|
|
|
|
- #js {:create true})
|
|
|
|
|
- handle-path (str "handle/" dir)
|
|
|
|
|
- _ (idb/set-item! handle-path new-handle)]
|
|
|
|
|
- (add-nfs-file-handle! handle-path new-handle)
|
|
|
|
|
- (println "dir created: " dir))))
|
|
|
|
|
- (p/catch (fn [error]
|
|
|
|
|
- (js/console.debug "mkdir error: " error ", dir: " dir)
|
|
|
|
|
- (throw error))))))
|
|
|
|
|
-
|
|
|
|
|
- (mkdir-recur! [this dir]
|
|
|
|
|
- (protocol/mkdir! this dir))
|
|
|
|
|
-
|
|
|
|
|
- (readdir [_this dir]
|
|
|
|
|
- ;; This method is only used for repo-dir and version-files dir
|
|
|
|
|
- ;; There's no Logseq Sync support for nfs. So assume dir is always a repo dir.
|
|
|
|
|
- (p/let [repo-url (str "logseq_local_" dir)
|
|
|
|
|
- _ (await-permission-granted repo-url)
|
|
|
|
|
- handle-path (str "handle/" dir)
|
|
|
|
|
- handle (or (get-nfs-file-handle handle-path)
|
|
|
|
|
- (idb/get-item handle-path))
|
|
|
|
|
- _ (when handle
|
|
|
|
|
- (verify-handle-permission handle true))
|
|
|
|
|
- fpaths (if (string/includes? dir "/")
|
|
|
|
|
- (js/console.error "ERROR: unimpl")
|
|
|
|
|
- (readdir-and-reload-all-handles dir handle))]
|
|
|
|
|
- fpaths))
|
|
|
|
|
-
|
|
|
|
|
- (unlink! [this repo fpath _opts]
|
|
|
|
|
- (let [repo-dir (config/get-repo-dir repo)
|
|
|
|
|
- filename (path/filename fpath)
|
|
|
|
|
- handle-path (str "handle/" fpath)
|
|
|
|
|
- recycle-dir (path/path-join repo-dir config/app-name config/recycle-dir)]
|
|
|
|
|
- (->
|
|
|
|
|
- (p/let [_ (protocol/mkdir! this recycle-dir)
|
|
|
|
|
- handle (get-nfs-file-handle handle-path)
|
|
|
|
|
- file (.getFile handle)
|
|
|
|
|
- content (.text file)
|
|
|
|
|
-
|
|
|
|
|
- bak-handle (get-nfs-file-handle (str "handle/" recycle-dir))
|
|
|
|
|
- bak-filename (-> (path/relative-path repo-dir fpath)
|
|
|
|
|
- (string/replace "/" "_")
|
|
|
|
|
- (string/replace "\\" "_"))
|
|
|
|
|
- file-handle (.getFileHandle ^js bak-handle bak-filename #js {:create true})
|
|
|
|
|
- _ (utils/writeFile file-handle content)
|
|
|
|
|
-
|
|
|
|
|
- parent-dir (path/parent fpath)
|
|
|
|
|
- parent-handle (get-nfs-file-handle (str "handle/" parent-dir))
|
|
|
|
|
- _ (when parent-handle
|
|
|
|
|
- (.removeEntry ^js parent-handle filename))]
|
|
|
|
|
- (idb/remove-item! handle-path)
|
|
|
|
|
- (remove-nfs-file-handle! handle-path))
|
|
|
|
|
- (p/catch (fn [error]
|
|
|
|
|
- (log/error :unlink/path {:path fpath
|
|
|
|
|
- :error error}))))))
|
|
|
|
|
-
|
|
|
|
|
- (rmdir! [_this _dir]
|
|
|
|
|
- nil)
|
|
|
|
|
-
|
|
|
|
|
- (read-file [_this dir path _options]
|
|
|
|
|
- (p/let [fpath (path/path-join dir path)
|
|
|
|
|
- handle-path (str "handle/" fpath)]
|
|
|
|
|
- (p/let [handle (or (get-nfs-file-handle handle-path)
|
|
|
|
|
- (idb/get-item handle-path))
|
|
|
|
|
- local-file (and handle (.getFile handle))]
|
|
|
|
|
- (and local-file (.text local-file)))))
|
|
|
|
|
-
|
|
|
|
|
- (write-file! [_this repo dir path content opts]
|
|
|
|
|
- ;; TODO: file backup handling
|
|
|
|
|
- (let [fpath (path/path-join dir path)
|
|
|
|
|
- ext (util/get-file-ext path)
|
|
|
|
|
- file-handle-path (str "handle/" fpath)]
|
|
|
|
|
- (p/let [file-handle (get-nfs-file-handle file-handle-path)]
|
|
|
|
|
- (if file-handle
|
|
|
|
|
- ;; file exist
|
|
|
|
|
- (p/let [local-file (.getFile file-handle)
|
|
|
|
|
- disk-content (.text local-file)
|
|
|
|
|
- db-content (db/get-file repo path)
|
|
|
|
|
- contents-matched?' (contents-matched? disk-content db-content)]
|
|
|
|
|
- (if (and
|
|
|
|
|
- (not (string/blank? db-content))
|
|
|
|
|
- (not (:skip-compare? opts))
|
|
|
|
|
- (not contents-matched?')
|
|
|
|
|
- (not (contains? #{"excalidraw" "edn" "css"} ext))
|
|
|
|
|
- (not (string/includes? path "/.recycle/")))
|
|
|
|
|
- (state/pub-event! [:file/not-matched-from-disk path disk-content content])
|
|
|
|
|
- (p/let [_ (verify-permission repo true)
|
|
|
|
|
- _ (utils/writeFile file-handle content)
|
|
|
|
|
- file (.getFile file-handle)]
|
|
|
|
|
- (when file
|
|
|
|
|
- (db/set-file-content! repo path content)
|
|
|
|
|
- (nfs-saved-handler repo path file)))))
|
|
|
|
|
- ;; file no-exist, write via parent dir handle
|
|
|
|
|
- (p/let [basename (path/filename fpath)
|
|
|
|
|
- parent-dir (path/parent fpath)
|
|
|
|
|
- parent-dir-handle-path (str "handle/" parent-dir)
|
|
|
|
|
- parent-dir-handle (get-nfs-file-handle parent-dir-handle-path)]
|
|
|
|
|
-
|
|
|
|
|
- (if parent-dir-handle
|
|
|
|
|
- ;; create from directory handle
|
|
|
|
|
- (p/let [file-handle (.getFileHandle ^js parent-dir-handle basename #js {:create true})
|
|
|
|
|
- _ (add-nfs-file-handle! file-handle-path file-handle)
|
|
|
|
|
- file (.getFile file-handle)
|
|
|
|
|
- text (.text file)]
|
|
|
|
|
- (if (string/blank? text)
|
|
|
|
|
- (p/let [;; _ (idb/set-item! file-handle-path file-handle)
|
|
|
|
|
- _ (utils/writeFile file-handle content)
|
|
|
|
|
- file (.getFile file-handle)]
|
|
|
|
|
- (when file
|
|
|
|
|
- (nfs-saved-handler repo path file)))
|
|
|
|
|
- (do
|
|
|
|
|
- (notification/show! (str "The file " path " already exists, please append the content if you need it.\n Unsaved content: \n" content)
|
|
|
|
|
- :warning
|
|
|
|
|
- false)
|
|
|
|
|
- (state/pub-event! [:file/alter repo path text]))))
|
|
|
|
|
-
|
|
|
|
|
- ;; TODO(andelf): Create parent directory and write
|
|
|
|
|
- ;; Normally directory are created layer by layer. So it's safe to leave this unimplemented.
|
|
|
|
|
- (js/console.error "TODO: can not create directory hierarchy")))))))
|
|
|
|
|
-
|
|
|
|
|
- (rename! [this repo old-path new-path]
|
|
|
|
|
- (p/let [repo-dir (config/get-repo-dir repo)
|
|
|
|
|
- old-rpath (path/relative-path repo-dir old-path)
|
|
|
|
|
- new-rpath (path/relative-path repo-dir new-path)
|
|
|
|
|
- old-content (protocol/read-file this repo-dir old-rpath nil)
|
|
|
|
|
- _ (protocol/write-file! this repo repo-dir new-rpath old-content nil)
|
|
|
|
|
- _ (protocol/unlink! this repo old-path nil)]))
|
|
|
|
|
-
|
|
|
|
|
- (stat [_this fpath]
|
|
|
|
|
- (if-let [handle (get-nfs-file-handle (str "handle/" fpath))]
|
|
|
|
|
- (p/let [_ (verify-handle-permission handle true)
|
|
|
|
|
- file (.getFile handle)]
|
|
|
|
|
- (let [get-attr #(gobj/get file %)]
|
|
|
|
|
- {:last-modified-at (get-attr "lastModified")
|
|
|
|
|
- :size (get-attr "size")
|
|
|
|
|
- :path fpath
|
|
|
|
|
- :type (get-attr "type")}))
|
|
|
|
|
- (p/rejected "File not exists")))
|
|
|
|
|
-
|
|
|
|
|
- (open-dir [_this _dir]
|
|
|
|
|
- (p/let [files (utils/openDirectory #js {:recursive true
|
|
|
|
|
- :mode "readwrite"}
|
|
|
|
|
- (fn [path entry]
|
|
|
|
|
- (let [handle-path (str "handle/" path)]
|
|
|
|
|
- ;; Same all handles here, even for directories and ignored directories(for backing up)
|
|
|
|
|
- ;; FileSystemDirectoryHandle or FileSystemFileHandle
|
|
|
|
|
- (when-not (string/includes? path "/.")
|
|
|
|
|
- (add-nfs-file-handle! handle-path entry)))))
|
|
|
|
|
- dir-handle (first files) ;; FileSystemDirectoryHandle
|
|
|
|
|
- dir-name (.-name dir-handle)
|
|
|
|
|
- files (->> (next files)
|
|
|
|
|
- (remove (fn [file]
|
|
|
|
|
- (let [rpath (.-webkitRelativePath file) ;
|
|
|
|
|
- ; (string/replace-first (.-webkitRelativePath file) (str dir-name "/") "")
|
|
|
|
|
- ext (util/get-file-ext rpath)]
|
|
|
|
|
- (or (string/blank? rpath)
|
|
|
|
|
- (string/starts-with? rpath ".")
|
|
|
|
|
- (string/starts-with? rpath "logseq/bak")
|
|
|
|
|
- (string/starts-with? rpath "logseq/version-files")
|
|
|
|
|
- (not (contains? #{"md" "org" "excalidraw" "edn" "css"} ext))))))
|
|
|
|
|
- ;; Read out using .text, Promise<string>
|
|
|
|
|
- (map (fn [file]
|
|
|
|
|
- (js/console.log "handle" file)
|
|
|
|
|
- (p/let [content (.text file)]
|
|
|
|
|
- ;; path content size mtime
|
|
|
|
|
- {:name (.-name file)
|
|
|
|
|
- :path (-> (.-webkitRelativePath file)
|
|
|
|
|
- common-util/path-normalize)
|
|
|
|
|
- :mtime (.-lastModified file)
|
|
|
|
|
- :size (.-size file)
|
|
|
|
|
- :type (.-kind (.-handle file))
|
|
|
|
|
- :content content
|
|
|
|
|
- ;; expose the following, they are used by the file system
|
|
|
|
|
- :file/file file}))))
|
|
|
|
|
- files (p/all files)]
|
|
|
|
|
- (add-nfs-file-handle! (str "handle/" dir-name) dir-handle)
|
|
|
|
|
- (idb/set-item! (str "handle/" dir-name) dir-handle)
|
|
|
|
|
- {:path dir-name
|
|
|
|
|
- :files files}))
|
|
|
|
|
-
|
|
|
|
|
- (get-files [_this dir]
|
|
|
|
|
- (when (string/includes? dir "/")
|
|
|
|
|
- (js/console.error "BUG: get-files(nfs) only accepts repo-dir"))
|
|
|
|
|
- (p/let [handle-path (str "handle/" dir)
|
|
|
|
|
- handle (get-nfs-file-handle handle-path)
|
|
|
|
|
- files (get-files-and-reload-all-handles dir handle)]
|
|
|
|
|
- files))
|
|
|
|
|
-
|
|
|
|
|
- (watch-dir! [_this _dir _options]
|
|
|
|
|
- nil)
|
|
|
|
|
-
|
|
|
|
|
- (unwatch-dir! [_this _dir]
|
|
|
|
|
- nil))
|
|
|