diff --git a/src/main/frontend/components/header.cljs b/src/main/frontend/components/header.cljs index a2b1fffc3..8c957f4ca 100644 --- a/src/main/frontend/components/header.cljs +++ b/src/main/frontend/components/header.cljs @@ -14,7 +14,8 @@ [frontend.components.svg :as svg] [frontend.components.repo :as repo] [frontend.components.page :as page] - [frontend.components.search :as search])) + [frontend.components.search :as search] + [frontend.handler.web.nfs :as nfs])) (rum/defc logo < rum/reactive [{:keys [white?]}] @@ -123,6 +124,11 @@ (new-block-mode) + [:a.text-sm.font-medium.login.opacity-70.hover:opacity-100.mr-4 + {:on-click (fn [] + (nfs/ls-dir-files))} + "Open a database"] + (when (and (not logged?) (not config/publishing?)) [:a.text-sm.font-medium.login.opacity-70.hover:opacity-100 @@ -161,5 +167,3 @@ [:a#download-as-html.hidden] (right-menu-button)])) - - diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index 3133c0eea..fc34158ab 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -269,3 +269,13 @@ (def markers #{"now" "later" "todo" "doing" "done" "wait" "waiting" "canceled" "cancelled" "started" "in-progress"}) + +(defonce local-db-prefix "logseq-local-") + +(defn local-db? + [s] + (string/starts-with? s local-db-prefix)) + +(defn get-local-dir + [s] + (string/replace s local-db-prefix "")) diff --git a/src/main/frontend/db.cljs b/src/main/frontend/db.cljs index a7f3a1c8c..fb6503561 100644 --- a/src/main/frontend/db.cljs +++ b/src/main/frontend/db.cljs @@ -14,7 +14,6 @@ [cljs-bean.core :as bean] [frontend.config :as config] [goog.object :as gobj] - ["localforage" :as localforage] [promesa.core :as p] [cljs.reader :as reader] [cljs-time.core :as t] @@ -24,17 +23,8 @@ [frontend.extensions.sci :as sci] [goog.array :as garray] [frontend.db-schema :as db-schema] - [clojure.core.async :as async])) - -;; offline db -(def store-name "dbs") -(.config localforage - (bean/->js - {:name "logseq-datascript" - :version 1.0 - :storeName store-name})) - -(defonce localforage-instance (.createInstance localforage store-name)) + [clojure.core.async :as async] + [frontend.idb :as idb])) ;; Query atom of map of Key ([repo q inputs]) -> atom ;; TODO: replace with LRUCache, only keep the latest 20 or 50 items? @@ -42,14 +32,6 @@ (defonce async-chan (atom nil)) -;; (defn clear-store! -;; [] -;; (p/let [_ (.clear localforage) -;; dbs (js/window.indexedDB.databases)] -;; (doseq [db dbs] -;; (js/window.indexedDB.deleteDatabase (gobj/get db "name"))))) - - (defn get-repo-path [url] (if (util/starts-with? url "http") @@ -69,11 +51,11 @@ (defn remove-db! [repo] - (.removeItem localforage-instance (datascript-db repo))) + (idb/remove-item! (datascript-db repo))) (defn remove-files-db! [repo] - (.removeItem localforage-instance (datascript-files-db repo))) + (idb/remove-item! (datascript-files-db repo))) (def react util/react) @@ -126,11 +108,10 @@ ;; persisting DB between page reloads (defn persist [repo db files-db?] - (.setItem localforage-instance - (if files-db? + (let [key (if files-db? (datascript-files-db repo) - (datascript-db repo)) - (db->string db))) + (datascript-db repo))] + (idb/set-item! key (db->string db)))) (defn reset-conn! [conn db] (reset! conn db)) @@ -958,6 +939,8 @@ contents)) all-data (-> (concat delete-files delete-blocks files blocks-pages) (util/remove-nils))] + (prn {:repo-url repo-url + :all-data all-data}) (transact! repo-url all-data))) (defn get-block-by-uuid @@ -1857,16 +1840,20 @@ config))) (defn start-db-conn! - [me repo] - (let [files-db-name (datascript-files-db repo) - files-db-conn (d/create-conn db-schema/files-db-schema) - db-name (datascript-db repo) - db-conn (d/create-conn db-schema/schema)] - (swap! conns assoc files-db-name files-db-conn) - (swap! conns assoc db-name db-conn) - (d/transact! db-conn [{:schema/version db-schema/version}]) - (when me - (d/transact! db-conn [(me-tx (d/db db-conn) me)])))) + ([me repo] + (start-db-conn! me repo {})) + ([me repo {:keys [db-type]}] + (let [files-db-name (datascript-files-db repo) + files-db-conn (d/create-conn db-schema/files-db-schema) + db-name (datascript-db repo) + db-conn (d/create-conn db-schema/schema)] + (swap! conns assoc files-db-name files-db-conn) + (swap! conns assoc db-name db-conn) + (d/transact! db-conn [(cond-> {:schema/version db-schema/version} + db-type + (assoc :db/type db-type))]) + (when me + (d/transact! db-conn [(me-tx (d/db db-conn) me)]))))) (defn restore! [{:keys [repos] :as me} restore-config-handler db-schema-changed-handler] @@ -1878,7 +1865,7 @@ db-conn (d/create-conn db-schema/files-db-schema)] (swap! conns assoc db-name db-conn) (-> - (p/let [stored (-> (.getItem localforage-instance db-name) + (p/let [stored (-> (idb/get-item db-name) (p/then (fn [result] result)) (p/catch (fn [error] @@ -1891,7 +1878,7 @@ db-conn (d/create-conn db-schema/schema) _ (d/transact! db-conn [{:schema/version db-schema/version}]) _ (swap! conns assoc db-name db-conn) - stored (.getItem localforage-instance db-name) + stored (idb/get-item db-name) _ (if stored (let [stored-db (string->db stored) attached-db (d/db-with stored-db [(me-tx stored-db me)])] @@ -2434,6 +2421,14 @@ datoms (d/datoms filtered-db :eavt)] @(d/conn-from-datoms datoms db-schema/schema))))) +(defn get-db-type + [repo] + (get-key-value repo :db/type)) + +(defn local-native-fs? + [repo] + (= :local-native-fs (get-db-type repo))) + ;; shortcut for query a block with string ref (defn qb [string-id] diff --git a/src/main/frontend/db_schema.cljs b/src/main/frontend/db_schema.cljs index 868a49830..ca80aca02 100644 --- a/src/main/frontend/db_schema.cljs +++ b/src/main/frontend/db_schema.cljs @@ -4,13 +4,16 @@ (def files-db-schema {:file/path {:db/unique :db.unique/identity} - :file/content {}}) + :file/content {} + :file/last-modified-at {} + :file/handle {}}) ;; A page can corresponds to multiple files (same title), ;; a month journal file can have multiple pages, ;; also, each block can be treated as a page too. (def schema - {:schema/version {} + {:schema/version {} + :db/type {} :db/ident {:db/unique :db.unique/identity} ;; user diff --git a/src/main/frontend/dicts.cljs b/src/main/frontend/dicts.cljs index 54549197f..d17e597d8 100644 --- a/src/main/frontend/dicts.cljs +++ b/src/main/frontend/dicts.cljs @@ -67,13 +67,13 @@ title: How to take dummy notes? ## Hello, I'm a block! :PROPERTIES: -:custom_id: 5f713e91-8a3c-4b04-a33a-c39482428e2d +:id: 5f713e91-8a3c-4b04-a33a-c39482428e2d :END: ### I'm a child block! ### I'm another child block! ## Hey, I'm another block! :PROPERTIES: -:custom_id: 5f713ea8-8cba-403d-ac00-9964b1ec7190 +:id: 5f713ea8-8cba-403d-ac00-9964b1ec7190 :END: " :on-boarding/title "Hi, welcome to Logseq!" diff --git a/src/main/frontend/fs.cljs b/src/main/frontend/fs.cljs index 0e0f24e31..ab1d3207c 100644 --- a/src/main/frontend/fs.cljs +++ b/src/main/frontend/fs.cljs @@ -1,10 +1,53 @@ (ns frontend.fs - (:require [frontend.util :as util])) + (:require [frontend.util :as util] + [frontend.config :as config] + [clojure.string :as string] + [frontend.idb :as idb] + [promesa.core :as p] + ["/frontend/utils" :as utils])) + +;; TODO: +;; We need to support several platforms: +;; 1. Chrome native file system API (lighting-fs wip) +;; 2. IndexedDB (lighting-fs) +;; 3. NodeJS +#_(defprotocol Fs + (mkdir! [this dir]) + (readdir! [this dir]) + (unlink! [this path opts]) + (rename! [this old-path new-path]) + (rmdir! [this dir]) + (read-file [dir path option]) + (write-file! [dir path content]) + (stat [dir path])) + +(defn local-db? + [dir] + (and (string? dir) + (config/local-db? (subs dir 1)))) (defn mkdir [dir] - (when (and dir js/window.pfs) - (js/window.pfs.mkdir dir))) + (cond + (local-db? dir) + (let [[root new-dir] (rest (string/split dir "/")) + root-handle (str "handle-" root)] + (p/let [handle (idb/get-item root-handle)] + (when (and handle new-dir + (not (string/blank? new-dir))) + (-> (p/let [handle (.getDirectoryHandle ^js handle new-dir + #js {:create true}) + _ (idb/set-item! (str root-handle "/" new-dir) handle)] + (println "Stored handle: " (str root-handle "/" new-dir))) + (p/catch (fn [error] + (println "mkdir error: " error) + (js/console.error error))))))) + + (and dir js/window.pfs) + (js/window.pfs.mkdir dir) + + :else + (println (str "mkdir " dir " failed")))) (defn readdir [dir] @@ -20,22 +63,50 @@ (js/window.pfs.rename old-path new-path)) (defn rmdir + "Remove the directory recursively." [dir] (js/window.workerThread.rimraf dir)) (defn read-file - [dir path] - (js/window.pfs.readFile (str dir "/" path) - (clj->js {:encoding "utf8"}))) - -(defn read-file-2 - [dir path] - (js/window.pfs.readFile (str dir "/" path) - (clj->js {}))) + ([dir path] + (read-file dir path (clj->js {:encoding "utf8"}))) + ([dir path option] + (js/window.pfs.readFile (str dir "/" path) option))) (defn write-file [dir path content] - (and js/window.pfs (js/window.pfs.writeFile (str dir "/" path) content))) + (cond + (local-db? dir) + (let [parts (string/split path "/") + basename (last parts) + sub-dir (->> (butlast parts) + (remove string/blank?) + (string/join "/")) + handle-path (str "handle-" + (subs dir 1) + (if sub-dir + (str "/" sub-dir))) + basename-handle-path (str handle-path "/" basename)] + (p/let [file-handle (idb/get-item basename-handle-path)] + (if file-handle + (utils/writeFile file-handle content) + ;; create file handle + (-> + (p/let [handle (idb/get-item handle-path)] + (if handle + (p/let [file-handle (.getFileHandle ^js handle basename #js {:create true}) + _ (idb/set-item! basename-handle-path file-handle)] + (utils/writeFile file-handle content)) + (println "Error: directory handle not exists: " handle-path))) + (p/catch (fn [error] + (println "Write local file failed: " {:path path}) + (js/console.error error))))))) + + js/window.pfs + (js/window.pfs.writeFile (str dir "/" path) content) + + :else + nil)) (defn stat [dir path] diff --git a/src/main/frontend/handler.cljs b/src/main/frontend/handler.cljs index b6ce65ae2..16699fd5a 100644 --- a/src/main/frontend/handler.cljs +++ b/src/main/frontend/handler.cljs @@ -12,6 +12,7 @@ [frontend.handler.repo :as repo-handler] [frontend.handler.file :as file-handler] [frontend.handler.ui :as ui-handler] + [frontend.handler.web.nfs :as nfs] [frontend.ui :as ui] [goog.object :as gobj])) @@ -132,6 +133,7 @@ (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false) (state/set-indexedb-support! false))) + (nfs/trigger-check!) (restore-and-setup! me repos logged?) (periodically-persist-repo-to-indexeddb!) diff --git a/src/main/frontend/handler/common.cljs b/src/main/frontend/handler/common.cljs index 797947d56..eb61af4aa 100644 --- a/src/main/frontend/handler/common.cljs +++ b/src/main/frontend/handler/common.cljs @@ -18,8 +18,6 @@ ;; TODO: what if the remote is not named "origin", check the api from isomorphic-git (git/resolve-ref repo-url (str "refs/remotes/origin/" branch)))) - -;; Should include un-pushed committed files too (defn check-changed-files-status ([] (check-changed-files-status (state/get-current-repo))) diff --git a/src/main/frontend/handler/git.cljs b/src/main/frontend/handler/git.cljs index eab82aee1..229ca435a 100644 --- a/src/main/frontend/handler/git.cljs +++ b/src/main/frontend/handler/git.cljs @@ -10,6 +10,7 @@ [frontend.handler.notification :as notification] [frontend.handler.route :as route-handler] [frontend.handler.common :as common-handler] + [frontend.config :as config] [cljs-time.local :as tl])) (defn- set-git-status! @@ -30,12 +31,13 @@ ([repo-url file] (git-add repo-url file true)) ([repo-url file update-status?] - (-> (p/let [result (git/add repo-url file)] - (when update-status? - (common-handler/check-changed-files-status))) - (p/catch (fn [error] - (println "git add '" file "' failed: " error) - (js/console.error error)))))) + (when-not (config/local-db? repo-url) + (-> (p/let [result (git/add repo-url file)] + (when update-status? + (common-handler/check-changed-files-status))) + (p/catch (fn [error] + (println "git add '" file "' failed: " error) + (js/console.error error))))))) (defn commit-and-force-push! [commit-message pushing?] diff --git a/src/main/frontend/handler/image.cljs b/src/main/frontend/handler/image.cljs index f681b535c..374a3e41e 100644 --- a/src/main/frontend/handler/image.cljs +++ b/src/main/frontend/handler/image.cljs @@ -33,8 +33,9 @@ (subs path 1) path)] (util/p-handle - (fs/read-file-2 (util/get-repo-dir (state/get-current-repo)) - path) + (fs/read-file (util/get-repo-dir (state/get-current-repo)) + path + {}) (fn [blob] (let [blob (js/Blob. (array blob) (clj->js {:type "image"})) img-url (image/create-object-url blob)] diff --git a/src/main/frontend/handler/repo.cljs b/src/main/frontend/handler/repo.cljs index 4d803774d..83544a53b 100644 --- a/src/main/frontend/handler/repo.cljs +++ b/src/main/frontend/handler/repo.cljs @@ -95,32 +95,22 @@ (p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/config-file) default-content)] (let [path (str app-dir "/" config/config-file) old-content (when file-exists? - (db/get-file repo-url path)) - content (or - (and old-content - (string/replace old-content "heading" "block")) - default-content)] - (db/reset-file! repo-url path content) - (db/reset-config! repo-url content) - (when-not (= content old-content) - (git-handler/git-add repo-url path)))) - ;; (p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/metadata-file) default-content)] - ;; (let [path (str app-dir "/" config/metadata-file)] - ;; (when-not file-exists? - ;; (db/reset-file! repo-url path "{:tx-data []}") - ;; (git-handler/git-add repo-url path)))) -)))) + (db/get-file repo-url path))] + (db/reset-file! repo-url path default-content) + (db/reset-config! repo-url default-content) + (when (not= default-content old-content) + (git-handler/git-add repo-url path)))))))) (defn create-contents-file [repo-url] (let [repo-dir (util/get-repo-dir repo-url) format (state/get-preferred-format) - path (str "pages/contents." (if (= (name format) "markdown") - "md" - (name format))) + path (str (state/get-pages-directory) + "/contents." + (if (= (name format) "markdown") "md" (name format))) file-path (str "/" path) default-content (util/default-content-with-title format "contents")] - (p/let [_ (-> (fs/mkdir (str repo-dir "/pages")) + (p/let [_ (-> (fs/mkdir (str repo-dir "/" (state/get-pages-directory))) (p/catch (fn [_e]))) file-exists? (fs/create-if-not-exists repo-dir file-path default-content)] (when-not file-exists? @@ -189,48 +179,58 @@ (defn create-default-files! [repo-url] - (when-let [name (get-in @state/state [:me :name])] - (create-config-file-if-not-exists repo-url) - (create-today-journal-if-not-exists repo-url) - (create-contents-file repo-url) - (create-custom-theme repo-url))) + (create-config-file-if-not-exists repo-url) + (create-today-journal-if-not-exists repo-url) + (create-contents-file repo-url) + (create-custom-theme repo-url)) + +(defn- parse-files-and-load-to-db! + [repo-url files contents {:keys [first-clone? delete-files delete-blocks re-render? additional-files-info]}] + (state/set-state! :repo/loading-files? false) + (state/set-state! :repo/importing-to-db? true) + (let [parsed-files (filter + (fn [[file _]] + (let [format (format/get-format file)] + (contains? config/mldoc-support-formats format))) + contents) + blocks-pages (if (seq parsed-files) + (db/extract-all-blocks-pages repo-url parsed-files) + [])] + (db/reset-contents-and-blocks! repo-url contents blocks-pages delete-files delete-blocks) + (let [config-file (str config/app-name "/" config/config-file)] + (if (contains? (set files) config-file) + (when-let [content (get contents config-file)] + (file-handler/restore-config! repo-url content true)))) + (when first-clone? (create-default-files! repo-url)) + (state/set-state! :repo/importing-to-db? false) + (when re-render? + (ui-handler/re-render-root!)))) (defn load-repo-to-db! - [repo-url diffs first-clone?] - (let [load-contents (fn [files delete-files delete-blocks re-render?] + [repo-url {:keys [first-clone? diffs nfs-files nfs-contents additional-files-info]}] + (let [load-contents (fn [files option] (file-handler/load-files-contents! repo-url files - (fn [contents] - (state/set-state! :repo/loading-files? false) - (state/set-state! :repo/importing-to-db? true) - (let [parsed-files (filter - (fn [[file _]] - (let [format (format/get-format file)] - (contains? config/mldoc-support-formats format))) - contents) - blocks-pages (if (seq parsed-files) - (db/extract-all-blocks-pages repo-url parsed-files) - [])] - (db/reset-contents-and-blocks! repo-url contents blocks-pages delete-files delete-blocks) - (let [config-file (str config/app-name "/" config/config-file)] - (if (contains? (set files) config-file) - (when-let [content (get contents config-file)] - (file-handler/restore-config! repo-url content true)))) - (when first-clone? (create-default-files! repo-url)) - (state/set-state! :repo/importing-to-db? false) - (when re-render? - (ui-handler/re-render-root!))))))] - (if first-clone? + (fn [contents] (parse-files-and-load-to-db! repo-url files contents option))))] + (cond + (seq nfs-files) + (parse-files-and-load-to-db! repo-url nfs-files nfs-contents + {:first-clone? true + :additional-files-info additional-files-info}) + + first-clone? (-> (p/let [files (file-handler/load-files repo-url)] - (load-contents files nil nil false)) + (load-contents files {:first-clone? first-clone?})) (p/catch (fn [error] (println "loading files failed: ") (js/console.dir error) ;; Empty repo (create-default-files! repo-url) (state/set-state! :repo/loading-files? false)))) + + :else (when (seq diffs) (let [filter-diffs (fn [type] (->> (filter (fn [f] (= type (:type f))) diffs) (map :path))) @@ -244,7 +244,11 @@ (db/delete-pages-by-files remove-files) []) add-or-modify-files (util/remove-nils (concat add-files modify-files))] - (load-contents add-or-modify-files (concat delete-files delete-pages) delete-blocks true)))))) + (load-contents add-or-modify-files + {:first-clone? first-clone? + :delete-files (concat delete-files delete-pages) + :delete-blocks delete-blocks + :re-render? true})))))) (defn persist-repo! [repo] @@ -256,7 +260,8 @@ (defn load-db-and-journals! [repo-url diffs first-clone?] (when (or diffs first-clone?) - (load-repo-to-db! repo-url diffs first-clone?))) + (load-repo-to-db! repo-url {:first-clone? first-clone? + :diffs diffs}))) (defn transact-react-and-alter-file! [repo tx transact-option files] @@ -520,6 +525,11 @@ (fn [error] (prn "Delete repo failed, error: " error)))) +(defn start-repo-db-if-not-exists! + [repo option] + (state/set-current-repo! repo) + (db/start-db-conn! nil repo option)) + (defn setup-local-repo-if-not-exists! [] (if js/window.pfs diff --git a/src/main/frontend/handler/user.cljs b/src/main/frontend/handler/user.cljs index 71ba4fdc1..f7ccfe251 100644 --- a/src/main/frontend/handler/user.cljs +++ b/src/main/frontend/handler/user.cljs @@ -2,6 +2,7 @@ (:require [frontend.util :as util :refer-macros [profile]] [frontend.state :as state] [frontend.db :as db] + [frontend.idb :as idb] [frontend.config :as config] [frontend.storage :as storage] [promesa.core :as p] @@ -58,19 +59,12 @@ (notification/show! "Workflow set successfully!" :success)) (fn [_e]))))) -(defn- clear-store! - [] - (p/let [_ (.clear db/localforage-instance) - dbs (js/window.indexedDB.databases)] - (doseq [db dbs] - (js/window.indexedDB.deleteDatabase (gobj/get db "name"))))) - (defn sign-out! [e] (-> (do (storage/clear) - (clear-store!)) + (idb/clear-store!)) (p/catch (fn [e] (println "sign out error: ") (js/console.dir e))) diff --git a/src/main/frontend/handler/web/nfs.cljs b/src/main/frontend/handler/web/nfs.cljs new file mode 100644 index 000000000..4836720e2 --- /dev/null +++ b/src/main/frontend/handler/web/nfs.cljs @@ -0,0 +1,93 @@ +(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.idb :as idb] + [frontend.state :as state] + [clojure.string :as string] + [frontend.ui :as ui] + [frontend.config :as config])) + +(defn ls-dir-files + [] + (-> + (p/let [result (utils/openDirectory #js {:recursive true}) + root-handle (nth result 0) + dir-name (gobj/get root-handle "name") + repo (str config/local-db-prefix dir-name) + _ (idb/set-item! (str "handle-" repo) root-handle) + result (nth result 1) + result (flatten (bean/->clj result)) + files (doall + (map (fn [file] + (let [handle (gobj/get file "handle") + get-attr #(gobj/get file %)] + {:file/path (get-attr "webkitRelativePath") + :file/last-modified-at (get-attr "lastModified") + :file/size (get-attr "size") + :file/type (get-attr "type") + :file/file file + :file/handle handle})) result)) + text-files (filter (fn [file] (contains? #{"org" "md" "markdown"} (util/get-file-ext (:file/path file)))) files)] + (doseq [file text-files] + (idb/set-item! (str "handle-" repo "/" (:file/path file)) + (:file/handle file))) + (-> (p/all (map (fn [file] + (p/let [content (.text (:file/file file))] + (assoc file :file/content content))) text-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 (map :file/path files) + :nfs-contents (mapv (fn [f] [(:file/path f) (:file/content f)]) files) + :additional-files-info files}) + ;; create default directories and files + ))) + (p/catch (fn [error] + (println "Load files content error: ") + (js/console.dir error))))) + (p/catch (fn [error] + (println "Open directory error: ") + (js/console.dir 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))] + (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)))) diff --git a/src/main/frontend/idb.cljs b/src/main/frontend/idb.cljs new file mode 100644 index 000000000..4ee2622a7 --- /dev/null +++ b/src/main/frontend/idb.cljs @@ -0,0 +1,34 @@ +(ns frontend.idb + (:require ["localforage" :as localforage] + [cljs-bean.core :as bean] + [goog.object :as gobj] + [promesa.core :as p])) + +;; offline db +(def store-name "dbs") +(.config localforage + (bean/->js + {:name "logseq-datascript" + :version 1.0 + :storeName store-name})) + +(defonce localforage-instance (.createInstance localforage store-name)) + +(defn clear-store! + [] + (p/let [_ (.clear localforage-instance) + dbs (js/window.indexedDB.databases)] + (doseq [db dbs] + (js/window.indexedDB.deleteDatabase (gobj/get db "name"))))) + +(defn remove-item! + [key] + (.removeItem localforage-instance key)) + +(defn set-item! + [key value] + (.setItem localforage-instance key value)) + +(defn get-item + [key] + (.getItem localforage-instance key)) diff --git a/src/main/frontend/protocol/fs.cljs b/src/main/frontend/protocol/fs.cljs new file mode 100644 index 000000000..ed952ff6a --- /dev/null +++ b/src/main/frontend/protocol/fs.cljs @@ -0,0 +1,7 @@ +(ns frontend.protocol.fs) + +(defprotocol Fs + (load-directory! [this]) + (write-file! [this path content]) + (delete-file! [this path]) + (get-file-stats! [this path])) diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 00638f5ed..61bf409d9 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -183,8 +183,10 @@ (defn get-pages-directory [] - (when-let [repo (get-current-repo)] - (:pages-directory (get-config repo)))) + (or + (when-let [repo (get-current-repo)] + (:pages-directory (get-config repo))) + "pages")) (defn org-mode-file-link? [repo] diff --git a/src/main/frontend/storage.cljs b/src/main/frontend/storage.cljs index cf62710a1..6cf84a86c 100644 --- a/src/main/frontend/storage.cljs +++ b/src/main/frontend/storage.cljs @@ -2,7 +2,6 @@ (:refer-clojure :exclude [get set remove]) (:require [cljs.reader :as reader])) -;; TODO: deprecate this, will persistent datascript (defn get [key] (reader/read-string ^js (.getItem js/localStorage (name key)))) diff --git a/src/main/frontend/utils.js b/src/main/frontend/utils.js index 93c414018..0a7ff8bc6 100644 --- a/src/main/frontend/utils.js +++ b/src/main/frontend/utils.js @@ -54,3 +54,68 @@ export var timeConversion = function (millisec) { return days + "d" } } + +// Modified from https://github.com/GoogleChromeLabs/browser-nativefs +// because shadow-cljs doesn't handle this babel transform +const getFiles = async function (dirHandle, recursive) { + const dirs = []; + const files = []; + const path = dirHandle.name; + for await (const entry of dirHandle.values()) { + const nestedPath = `${path}/${entry.name}`; + if (entry.kind === 'file') { + files.push( + entry.getFile().then((file) => { + Object.defineProperty(file, 'webkitRelativePath', { + configurable: true, + enumerable: true, + get: () => nestedPath, + }); + Object.defineProperty(file, 'handle', { + configurable: true, + enumerable: true, + get: () => entry, + }); + return file; + } + ) + ); + } else if (entry.kind === 'directory' && recursive) { + dirs.push(getFiles(entry, recursive, nestedPath)); + } + } + + return [(await Promise.all(dirs)), (await Promise.all(files))]; +}; + +export var openDirectory = async function (options = {}) { + options.recursive = options.recursive || false; + const handle = await window.showDirectoryPicker({ mode: 'readwrite' }); + return [handle, getFiles(handle, options.recursive)]; +}; + +export var writeFile = async function (fileHandle, contents) { + // Create a FileSystemWritableFileStream to write to. + const writable = await fileHandle.createWritable(); + // Write the contents of the file to the stream. + await writable.write(contents); + // Close the file and write the contents to disk. + await writable.close(); +}; + +export var verifyPermission = async function (handle, readWrite) { + const options = {}; + if (readWrite) { + options.mode = 'readwrite'; + } + // Check if permission was already granted. If so, return true. + if ((await handle.queryPermission(options)) === 'granted') { + return true; + } + // Request permission. If the user grants permission, return true. + if ((await handle.requestPermission(options)) === 'granted') { + return true; + } + // The user didn't grant permission, so return false. + return false; +}