Просмотр исходного кода

Merge remote-tracking branch 'origin/feat/electron' into feat/electron

charlie 4 лет назад
Родитель
Сommit
d06ec5cc6e

+ 3 - 0
package.json

@@ -26,6 +26,7 @@
     },
     "scripts": {
         "watch": "run-p gulp:build gulp:watch cljs:watch",
+        "electron-watch": "run-p gulp:build gulp:watch cljs:electron-watch",
         "release": "run-s gulp:build cljs:release",
         "watch-app": "run-p gulp:watch cljs:watch-app",
         "release-app": "run-s gulp:build cljs:release-app",
@@ -40,6 +41,7 @@
         "gulp:watch": "gulp watch",
         "gulp:build": "cross-env NODE_ENV=production gulp build",
         "cljs:watch": "clojure -M:cljs watch app publishing electron",
+        "cljs:electron-watch": "clojure -M:cljs watch app electron",
         "cljs:release": "clojure -M:cljs release app publishing",
         "cljs:test": "clojure -A:test compile test",
         "cljs:run-test": "node static/tests.js",
@@ -55,6 +57,7 @@
         "codemirror": "^5.58.1",
         "diff": "5.0.0",
         "diff-match-patch": "^1.0.5",
+        "electron": "^11.2.0",
         "fs": "^0.0.1-security",
         "fuzzysort": "^1.1.4",
         "gulp-cached": "^1.1.1",

+ 1 - 6
resources/dev.html

@@ -21,10 +21,5 @@
     </a>
   </h3>
 </div>
-<script>
-  require('electron').ipcRenderer.on('hello', (e, v) => {
-    console.info('[hello] ', v)
-  })
-</script>
 </body>
-</html>
+</html>

+ 7 - 0
resources/js/preload.js

@@ -0,0 +1,7 @@
+const {ipcRenderer, contextBridge} = require('electron');
+
+contextBridge.exposeInMainWorld('api', {
+  doAction: async (arg) => {
+    return await ipcRenderer.invoke('main', arg);
+  }
+});

+ 4 - 1
shadow-cljs.edn

@@ -35,7 +35,10 @@
 
   :electron {:target :node-script
              :output-to "static/electron.js"
-             :main electron.core/main}
+             :main electron.core/main
+             :devtools
+             {:before-load electron.core/stop
+              :after-load electron.core/start}}
 
   :test
   {:target :node-test

+ 5 - 0
src/dev-cljs/shadow/user.clj

@@ -5,3 +5,8 @@
   []
   (api/watch :app)
   (api/repl :app))
+
+(defn electron-repl
+  []
+  (api/watch :electron)
+  (api/repl :electron))

+ 13 - 5
src/electron/electron/core.cljs

@@ -1,5 +1,5 @@
 (ns electron.core
-  (:require [electron.init :refer [init-channel]]
+  (:require [electron.handler :as handler]
             ["fs" :as fs]
             ["path" :as path]
             ["electron" :refer [BrowserWindow app] :as electron]
@@ -24,8 +24,10 @@
   (let [win-opts {:width  980
                   :height 700
                   :webPreferences
-                  {:nodeIntegration true            ;; FIXME
-}}
+                  {:nodeIntegration false
+                   :nodeIntegrationInWorker false
+                   :contextIsolation true
+                   :preload (path/join js/__dirname "js/preload.js")}}
         url MAIN_WINDOW_ENTRY
         win (BrowserWindow. (clj->js win-opts))]
     (.loadURL win url)
@@ -72,7 +74,7 @@
            (setup-updater! nil)
 
            ;; init stuffs
-           (init-channel win)
+           (handler/set-ipc-handler! win)
 
            ;; main window events
            (.on win "close" #(if (or @*quitting? win32?)
@@ -80,4 +82,10 @@
                                (do (.preventDefault ^js/Event %)
                                    (.hide win))))
            (.on app "before-quit" #(reset! *quitting? true))
-           (.on app "activate" #(if @*win (.show win)))))))
+           (.on app "activate" #(if @*win (.show win)))))))
+
+(defn start []
+  (js/console.log "Main - start"))
+
+(defn stop []
+  (js/console.log "Main - stop"))

+ 8 - 0
src/electron/electron/handler.cljs

@@ -0,0 +1,8 @@
+(ns electron.handler
+  (:require ["electron" :refer [ipcMain]]))
+
+(defn set-ipc-handler! [window]
+  (.handle ipcMain "main"
+           (fn [event args-js]
+             (prn "receive event: " args-js)
+             args-js)))

+ 0 - 9
src/electron/electron/init.cljs

@@ -1,9 +0,0 @@
-(ns electron.init)
-
-;;; FIXME: real world
-(defn init-channel
-  "init main process IPC channel wrapper"
-  [^js win]
-  (js/setInterval
-   #(.. win -webContents (send "hello" "msg from Background :)"))
-   3000))

+ 7 - 0
src/main/electron/ipc.cljs

@@ -0,0 +1,7 @@
+(ns electron.ipc
+  (:require [cljs-bean.core :as bean]
+            [promesa.core :as p]))
+
+(defn ipc
+  [& args]
+  (js/window.api.doAction (bean/->js args)))

+ 48 - 256
src/main/frontend/fs.cljs

@@ -1,263 +1,78 @@
 (ns frontend.fs
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
-            [frontend.state :as state]
             [clojure.string :as string]
-            [frontend.idb :as idb]
-            [frontend.db :as db]
-            [frontend.handler.common :as common-handler]
             [promesa.core :as p]
-            [goog.object :as gobj]
-            [clojure.set :as set]
             [lambdaisland.glogi :as log]
-            ["/frontend/utils" :as utils]))
+            [frontend.fs.protocol :as protocol]
+            [frontend.fs.nfs :as nfs]
+            [frontend.fs.bfs :as bfs]
+            [frontend.fs.node :as node]))
 
-;; We need to 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]
-  (swap! nfs-file-handles-cache assoc handle-path handle))
-
-(defn remove-nfs-file-handle!
-  [handle-path]
-  (swap! nfs-file-handles-cache dissoc handle-path))
-
-;; 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]))
+(defonce nfs-record (nfs/->Nfs))
+(defonce bfs-record (bfs/->Bfs))
+(defonce node-record (node/->Node))
 
 (defn local-db?
   [dir]
   (and (string? dir)
        (config/local-db? (subs dir 1))))
 
-(defn mkdir
+(defn get-fs
   [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 handle (common-handler/verify-permission nil handle true))]
-         (when (and handle new-dir
-                    (not (string/blank? new-dir)))
-           (p/let [handle (.getDirectoryHandle ^js handle new-dir
-                                               #js {:create true})
-                   handle-path (str root-handle "/" new-dir)
-                   _ (idb/set-item! handle-path handle)]
-             (add-nfs-file-handle! handle-path handle)
-             (println "Stored handle: " (str root-handle "/" new-dir)))))
-       (p/catch (fn [error]
-                  (js/console.debug "mkdir error: " error ", dir: " dir)
-                  (throw error)))))
+    (util/electron?)
+    node-record
 
-    (and dir js/window.pfs)
-    (js/window.pfs.mkdir dir)
+    (local-db? dir)
+    nfs-record
 
     :else
-    (println (str "mkdir " dir " failed"))))
+    bfs-record))
 
-(defn readdir
+(defn mkdir!
   [dir]
-  (cond
-    (local-db? dir)
-    (let [prefix (str "handle/" dir)
-          cached-files (keys @nfs-file-handles-cache)]
-      (p/resolved
-       (->> (filter #(string/starts-with? % (str prefix "/")) cached-files)
-            (map (fn [path]
-                   (string/replace path prefix ""))))))
-
-    (and dir js/window.pfs)
-    (js/window.pfs.readdir dir)
+  (protocol/mkdir! (get-fs dir) dir))
 
-    :else
-    nil))
+(defn readdir
+  [dir]
+  (protocol/readdir (get-fs dir) dir))
 
-(defn unlink
+(defn unlink!
   [path opts]
-  (cond
-    (local-db? path)
-    (let [[dir basename] (util/get-dir-and-basename path)
-          handle-path (str "handle" path)]
-      (->
-       (p/let [handle (idb/get-item (str "handle" dir))
-               _ (idb/remove-item! handle-path)]
-         (when handle
-           (.removeEntry ^js handle basename))
-         (remove-nfs-file-handle! handle-path))
-       (p/catch (fn [error]
-                  (log/error :unlink/path {:path path
-                                           :error error})))))
-
-    :else
-    (p/let [stat (js/window.pfs.stat path)]
-      (if (= (.-type stat) "file")
-        (js/window.pfs.unlink path opts)
-        (p/rejected "Unlinking a directory is not allowed")))))
+  (protocol/unlink! (get-fs path) path opts))
 
-(defn rmdir
-  "Remove the directory recursively."
+(defn rmdir!
+  "Remove the directory recursively.
+   Warning: only run it for browser cache."
   [dir]
-  (cond
-    (local-db? dir)
-    nil
-
-    :else
-    (js/window.workerThread.rimraf dir)))
+  (protocol/rmdir! (get-fs dir) dir))
 
 (defn read-file
-  ([dir path]
-   (read-file dir path (clj->js {:encoding "utf8"})))
-  ([dir path option]
-   (cond
-     (local-db? dir)
-     (let [handle-path (str "handle" dir "/" path)]
-       (p/let [handle (idb/get-item handle-path)
-               local-file (and handle (.getFile handle))]
-         (and local-file (.text local-file))))
-
-     :else
-     (js/window.pfs.readFile (str dir "/" path) option))))
-
-(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)]
-      (db/set-file-last-modified-at! repo path last-modified))))
-
-(defn write-file
-  ([repo dir path content]
-   (write-file repo dir path content nil))
-  ([repo dir path content {:keys [old-content last-modified-at]}]
-   (->
-    (cond
-      (local-db? dir)
-      (let [parts (string/split path "/")
-            basename (last parts)
-            sub-dir (->> (butlast parts)
-                         (remove string/blank?)
-                         (string/join "/"))
-            sub-dir-handle-path (str "handle/"
-                                     (subs dir 1)
-                                     (if sub-dir
-                                       (str "/" sub-dir)))
-            handle-path (if (= "/" (last sub-dir-handle-path))
-                          (subs sub-dir-handle-path 0 (dec (count sub-dir-handle-path)))
-                          sub-dir-handle-path)
-            basename-handle-path (str handle-path "/" basename)]
-        (p/let [file-handle (idb/get-item basename-handle-path)]
-          (when file-handle
-            (add-nfs-file-handle! basename-handle-path file-handle))
-          (if file-handle
-            (p/let [local-file (.getFile file-handle)
-                    local-content (.text local-file)
-                    local-last-modified-at (gobj/get local-file "lastModified")
-                    current-time (util/time-ms)
-                    new? (> current-time local-last-modified-at)
-                    new-created? (nil? last-modified-at)
-                    not-changed? (= last-modified-at local-last-modified-at)
-                    format (-> (util/get-file-ext path)
-                               (config/get-file-format))
-                    pending-writes (state/get-write-chan-length)]
-              ;; (println {:last-modified-at last-modified-at
-              ;;           :local-last-modified-at local-last-modified-at
-              ;;           :not-changed? not-changed?
-              ;;           :new-created? new-created?
-              ;;           :pending-writes pending-writes
-              ;;           :local-content local-content
-              ;;           :old-content old-content
-              ;;           :new? new?})
-              (if (and local-content old-content new?
-                       (or
-                        (> pending-writes 0)
-                        not-changed?
-                        new-created?))
-                (do
-                  (p/let [_ (common-handler/verify-permission repo file-handle true)
-                          _ (utils/writeFile file-handle content)
-                          file (.getFile file-handle)]
-                    (when file
-                      (nfs-saved-handler repo path file))))
-                (do
-                  (js/alert (str "The file has been modified in your local disk! File path: " path
-                                 ", save your changes and click the refresh button to reload it.")))))
-            ;; create file handle
-            (->
-             (p/let [handle (idb/get-item handle-path)]
-               (if handle
-                 (do
-                   (p/let [_ (common-handler/verify-permission repo handle true)
-                           file-handle (.getFileHandle ^js handle basename #js {:create true})
-                           _ (idb/set-item! basename-handle-path file-handle)
-                           _ (utils/writeFile file-handle content)
-                           file (.getFile file-handle)]
-                     (when file
-                       (nfs-saved-handler repo path file))))
-                 (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)
-    (p/catch (fn [error]
-               (log/error :file/write-failed? {:dir dir
-                                               :path path
-                                               :error error})
-               ;; Disable this temporarily
-               ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
-)))))
-
-(defn rename
+  [dir path]
+  (protocol/read-file (get-fs dir) dir path))
+
+(defn write-file!
+  [repo dir path content opts]
+  (->
+   (protocol/write-file! (get-fs dir) repo dir path content opts)
+   (p/catch (fn [error]
+              (log/error :file/write-failed? {:dir dir
+                                              :path path
+                                              :error error})
+              ;; Disable this temporarily
+              ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
+))))
+
+(defn rename!
   [repo old-path new-path]
   (cond
     ; See https://github.com/isomorphic-git/lightning-fs/issues/41
     (= old-path new-path)
     (p/resolved nil)
 
-    (local-db? old-path)
-    ;; create new file
-    ;; delete old file
-    (p/let [[dir basename] (util/get-dir-and-basename old-path)
-            [_ new-basename] (util/get-dir-and-basename new-path)
-            parts (->> (string/split new-path "/")
-                       (remove string/blank?))
-            dir (str "/" (first parts))
-            new-path (->> (rest parts)
-                          (string/join "/"))
-            handle (idb/get-item (str "handle" old-path))
-            file (.getFile handle)
-            content (.text file)
-            _ (write-file repo dir new-path content)]
-      (unlink old-path nil))
-
     :else
-    (js/window.pfs.rename old-path new-path)))
+    (protocol/rename! (get-fs old-path) repo old-path new-path)))
 
 (defn stat
   [dir path]
@@ -267,32 +82,16 @@
                              (subs path 1)
                              path))
                       "")]
-    (cond
-      (local-db? dir)
-      (if-let [file (get-nfs-file-handle (str "handle/"
-                                              (string/replace-first dir "/" "")
-                                              append-path))]
-        (p/let [file (.getFile file)]
-          (let [get-attr #(gobj/get file %)]
-            {:file/last-modified-at (get-attr "lastModified")
-             :file/size (get-attr "size")
-             :file/type (get-attr "type")}))
-        (p/rejected "File not exists"))
-
-      :else
-      (do
-        (js/window.pfs.stat (str dir append-path))))))
+    (protocol/stat (get-fs dir) dir path)))
 
 (defn mkdir-if-not-exists
   [dir]
   (when dir
-    (let [local? (config/local-db? dir)]
-      (when (or local? js/window.pfs)
-        (util/p-handle
-         (stat dir nil)
-         (fn [_stat])
-         (fn [error]
-           (mkdir dir)))))))
+    (util/p-handle
+     (stat dir nil)
+     (fn [_stat])
+     (fn [error]
+       (mkdir! dir)))))
 
 (defn create-if-not-exists
   ([repo dir path]
@@ -306,7 +105,7 @@
         true)
       (p/catch
        (fn [_error]
-         (p/let [_ (write-file repo dir path initial-content)]
+         (p/let [_ (write-file! repo dir path initial-content nil)]
            false)))))))
 
 (defn file-exists?
@@ -315,10 +114,3 @@
    (stat dir path)
    (fn [_stat] true)
    (fn [_e] false)))
-
-(defn check-directory-permission!
-  [repo]
-  (when (config/local-db? repo)
-    (p/let [handle (idb/get-item (str "handle/" repo))]
-      (when handle
-        (common-handler/verify-permission repo handle true)))))

+ 31 - 0
src/main/frontend/fs/bfs.cljs

@@ -0,0 +1,31 @@
+(ns frontend.fs.bfs
+  (:require [frontend.fs.protocol :as protocol]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [promesa.core :as p]))
+
+(defrecord Bfs []
+  protocol/Fs
+  (mkdir! [this dir]
+    (when js/window.pfs
+      (js/window.pfs.mkdir dir)))
+  (readdir [this dir]
+    (when js/window.pfs
+      (js/window.pfs.readdir dir)))
+  (unlink! [this path opts]
+    (when js/window.pfs
+      (p/let [stat (js/window.pfs.stat path)]
+        (if (= (.-type stat) "file")
+          (js/window.pfs.unlink path opts)
+          (p/rejected "Unlinking a directory is not allowed")))))
+  (rmdir! [this dir]
+    (js/window.workerThread.rimraf dir))
+  (read-file [this dir path]
+    (let [option (clj->js {:encoding "utf8"})]
+      (js/window.pfs.readFile (str dir "/" path) option)))
+  (write-file! [this repo dir path content opts]
+    (js/window.pfs.writeFile (str dir "/" path) content))
+  (rename! [this repo old-path new-path]
+    (js/window.pfs.rename old-path new-path))
+  (stat [this dir path]
+    (js/window.pfs.stat (str dir path))))

+ 196 - 0
src/main/frontend/fs/nfs.cljs

@@ -0,0 +1,196 @@
+(ns frontend.fs.nfs
+  (: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/utils" :as utils]))
+
+;; We need to 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]
+  (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-permission
+  [repo handle read-write?]
+  (let [repo (or repo (state/get-current-repo))]
+    (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-db? repo)
+    (p/let [handle (idb/get-item (str "handle/" repo))]
+      (when handle
+        (verify-permission repo handle true)))))
+
+(defrecord Nfs []
+  protocol/Fs
+  (mkdir! [this dir]
+    (let [[root new-dir] (rest (string/split dir "/"))
+          root-handle (str "handle/" root)]
+      (->
+       (p/let [handle (idb/get-item root-handle)
+               _ (when handle (verify-permission nil handle true))]
+         (when (and handle new-dir
+                    (not (string/blank? new-dir)))
+           (p/let [handle (.getDirectoryHandle ^js handle new-dir
+                                               #js {:create true})
+                   handle-path (str root-handle "/" new-dir)
+                   _ (idb/set-item! handle-path handle)]
+             (add-nfs-file-handle! handle-path handle)
+             (println "Stored handle: " (str root-handle "/" new-dir)))))
+       (p/catch (fn [error]
+                  (js/console.debug "mkdir error: " error ", dir: " dir)
+                  (throw error))))))
+
+  (readdir [this dir]
+    (let [prefix (str "handle/" dir)
+          cached-files (keys @nfs-file-handles-cache)]
+      (p/resolved
+       (->> (filter #(string/starts-with? % (str prefix "/")) cached-files)
+            (map (fn [path]
+                   (string/replace path prefix "")))))))
+
+  (unlink! [this path opts]
+    (let [[dir basename] (util/get-dir-and-basename path)
+          handle-path (str "handle" path)]
+      (->
+       (p/let [handle (idb/get-item (str "handle" dir))
+               _ (idb/remove-item! handle-path)]
+         (when handle
+           (.removeEntry ^js handle basename))
+         (remove-nfs-file-handle! handle-path))
+       (p/catch (fn [error]
+                  (log/error :unlink/path {:path path
+                                           :error error}))))))
+
+  (rmdir! [this dir]
+    nil)
+
+  (read-file [this dir path]
+    (let [handle-path (str "handle" dir "/" path)]
+      (p/let [handle (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]
+    (let [{:keys [old-content last-modified-at]} opts
+          parts (string/split path "/")
+          basename (last parts)
+          sub-dir (->> (butlast parts)
+                       (remove string/blank?)
+                       (string/join "/"))
+          sub-dir-handle-path (str "handle/"
+                                   (subs dir 1)
+                                   (if sub-dir
+                                     (str "/" sub-dir)))
+          handle-path (if (= "/" (last sub-dir-handle-path))
+                        (subs sub-dir-handle-path 0 (dec (count sub-dir-handle-path)))
+                        sub-dir-handle-path)
+          basename-handle-path (str handle-path "/" basename)]
+      (p/let [file-handle (idb/get-item basename-handle-path)]
+        (when file-handle
+          (add-nfs-file-handle! basename-handle-path file-handle))
+        (if file-handle
+          (p/let [local-file (.getFile file-handle)
+                  local-content (.text local-file)
+                  local-last-modified-at (gobj/get local-file "lastModified")
+                  current-time (util/time-ms)
+                  new? (> current-time local-last-modified-at)
+                  new-created? (nil? last-modified-at)
+                  not-changed? (= last-modified-at local-last-modified-at)
+                  format (-> (util/get-file-ext path)
+                             (config/get-file-format))
+                  pending-writes (state/get-write-chan-length)]
+             ;; (println {:last-modified-at last-modified-at
+             ;;           :local-last-modified-at local-last-modified-at
+             ;;           :not-changed? not-changed?
+             ;;           :new-created? new-created?
+             ;;           :pending-writes pending-writes
+             ;;           :local-content local-content
+             ;;           :old-content old-content
+             ;;           :new? new?})
+            (if (and local-content old-content new?
+                     (or
+                      (> pending-writes 0)
+                      not-changed?
+                      new-created?))
+              (do
+                (p/let [_ (verify-permission repo file-handle true)
+                        _ (utils/writeFile file-handle content)
+                        file (.getFile file-handle)]
+                  (when file
+                    (nfs-saved-handler repo path file))))
+              (do
+                (js/alert (str "The file has been modified in your local disk! File path: " path
+                               ", save your changes and click the refresh button to reload it.")))))
+           ;; create file handle
+          (->
+           (p/let [handle (idb/get-item handle-path)]
+             (if handle
+               (do
+                 (p/let [_ (verify-permission repo handle true)
+                         file-handle (.getFileHandle ^js handle basename #js {:create true})
+                         _ (idb/set-item! basename-handle-path file-handle)
+                         _ (utils/writeFile file-handle content)
+                         file (.getFile file-handle)]
+                   (when file
+                     (nfs-saved-handler repo path file))))
+               (println "Error: directory handle not exists: " handle-path)))
+           (p/catch (fn [error]
+                      (println "Write local file failed: " {:path path})
+                      (js/console.error error))))))))
+
+  (rename! [this repo old-path new-path]
+    (p/let [[dir basename] (util/get-dir-and-basename old-path)
+            [_ new-basename] (util/get-dir-and-basename new-path)
+            parts (->> (string/split new-path "/")
+                       (remove string/blank?))
+            dir (str "/" (first parts))
+            new-path (->> (rest parts)
+                          (string/join "/"))
+            handle (idb/get-item (str "handle" old-path))
+            file (.getFile handle)
+            content (.text file)
+            _ (protocol/write-file! this repo dir new-path content nil)]
+      (protocol/unlink! this old-path nil)))
+  (stat [this dir path]
+    (if-let [file (get-nfs-file-handle (str "handle/"
+                                            (string/replace-first dir "/" "")
+                                            path))]
+      (p/let [file (.getFile file)]
+        (let [get-attr #(gobj/get file %)]
+          {:file/last-modified-at (get-attr "lastModified")
+           :file/size (get-attr "size")
+           :file/type (get-attr "type")}))
+      (p/rejected "File not exists"))))

+ 41 - 0
src/main/frontend/fs/node.cljs

@@ -0,0 +1,41 @@
+(ns frontend.fs.node
+  (:require [frontend.fs.protocol :as protocol]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [promesa.core :as p]
+            [electron.ipc :as ipc]))
+
+;; (defonce fs (when (util/electron?)
+;;               (js/require "fs")))
+
+;; (defonce path (when (util/electron?)
+;;                 (js/require "path")))
+
+;; (defn ls-dir [dir]
+;;   (->> (tree-seq
+;;         (fn [f] (.isDirectory (.statSync fs f) ()))
+;;         (fn [d] (map #(.join path d %) (.readdirSync fs d)))
+;;         dir)
+;;        (apply concat)
+;;        (doall)))
+
+(defrecord Node []
+  protocol/Fs
+  (mkdir! [this dir]
+    (ipc/ipc "mkdir" {:path dir}))
+  (readdir [this dir]                   ; recursive
+    (ipc/ipc "readdir" {:dir dir}))
+  (unlink! [this path _opts]
+    (ipc/ipc "unlink" {:path path}))
+  (rmdir! [this dir]
+    nil)
+  (read-file [this dir path]
+    (ipc/ipc "readFile" {:path (str dir "/" path)}))
+  (write-file! [this repo dir path content opts]
+    (ipc/ipc "writeFile" {:path (str dir "/" path)
+                          :content content}))
+  (rename! [this repo old-path new-path]
+    (ipc/ipc "rename" {:old-path old-path
+                       :new-path new-path}))
+  (stat [this dir path]
+    (ipc/ipc "stat" {:path (str dir path)})))

+ 11 - 0
src/main/frontend/fs/protocol.cljs

@@ -0,0 +1,11 @@
+(ns frontend.fs.protocol)
+
+(defprotocol Fs
+  (mkdir! [this dir])
+  (readdir [this dir])
+  (unlink! [this path opts])
+  (rmdir! [this dir])
+  (read-file [this dir path])
+  (write-file! [this repo dir path content opts])
+  (rename! [this repo old-path new-path])
+  (stat [this dir path]))

+ 28 - 38
src/main/frontend/handler/common.cljs

@@ -68,8 +68,7 @@
             diffs (git/get-diffs repo local-oid remote-oid)]
       (println {:local-oid local-oid
                 :remote-oid remote-oid
-                :diffs diffs})))
-  )
+                :diffs diffs}))))
 
 (defn get-config
   [repo-url]
@@ -91,20 +90,20 @@
   [ok-handler error-handler]
   (let [repos (state/get-repos)
         installation-ids (->> (map :installation_id repos)
-                           (remove nil?)
-                           (distinct))]
+                              (remove nil?)
+                              (distinct))]
     (when (or (seq repos)
-            (seq installation-ids))
+              (seq installation-ids))
       (util/post (str config/api "refresh_github_token")
-        {:installation-ids installation-ids
-         :repos repos}
-        (fn [result]
-          (state/set-github-installation-tokens! result)
-          (when ok-handler (ok-handler)))
-        (fn [error]
-          (log/error :token/http-request-failed error)
-          (js/console.dir error)
-          (when error-handler (error-handler)))))))
+                 {:installation-ids installation-ids
+                  :repos repos}
+                 (fn [result]
+                   (state/set-github-installation-tokens! result)
+                   (when ok-handler (ok-handler)))
+                 (fn [error]
+                   (log/error :token/http-request-failed error)
+                   (js/console.dir error)
+                   (when error-handler (error-handler)))))))
 
 (defn- get-github-token*
   [repo]
@@ -114,7 +113,7 @@
           (state/get-github-token repo)]
       (spec/validate :repos/repo token-state)
       (if (and (map? token-state)
-            (string? expires_at))
+               (string? expires_at))
         (let [expires-at (tf/parse (tf/formatters :date-time-no-ms) expires_at)
               now (t/now)
               expired? (t/after? now expires-at)]
@@ -129,26 +128,17 @@
   ([repo]
    (when-not (config/local-db? repo)
      (js/Promise.
-       (fn [resolve reject]
-         (let [{:keys [expired? token exist?]} (get-github-token* repo)
-               valid-token? (and exist? (not expired?))]
-           (if valid-token?
-             (resolve token)
-             (request-app-tokens!
-               (fn []
-                 (let [{:keys [expired? token exist?] :as token-m} (get-github-token* repo)
-                       valid-token? (and exist? (not expired?))]
-                   (if valid-token?
-                     (resolve token)
-                     (do (log/error :token/failed-get-token token-m)
-                         (reject)))))
-               nil))))))))
-
-(defn verify-permission
-  [repo handle read-write?]
-  (let [repo (or repo (state/get-current-repo))]
-    (p/then
-      (utils/verifyPermission handle read-write?)
-      (fn []
-        (state/set-state! [:nfs/user-granted? repo] true)
-        true))))
+      (fn [resolve reject]
+        (let [{:keys [expired? token exist?]} (get-github-token* repo)
+              valid-token? (and exist? (not expired?))]
+          (if valid-token?
+            (resolve token)
+            (request-app-tokens!
+             (fn []
+               (let [{:keys [expired? token exist?] :as token-m} (get-github-token* repo)
+                     valid-token? (and exist? (not expired?))]
+                 (if valid-token?
+                   (resolve token)
+                   (do (log/error :token/failed-get-token token-m)
+                       (reject)))))
+             nil))))))))

+ 5 - 5
src/main/frontend/handler/draw.cljs

@@ -49,10 +49,10 @@
   [repo]
   (when repo
     (let [repo-dir (util/get-repo-dir repo)]
-     (util/p-handle
-      (fs/mkdir (str repo-dir (str "/" config/default-draw-directory)))
-      (fn [_result] nil)
-      (fn [_error] nil)))))
+      (util/p-handle
+       (fs/mkdir! (str repo-dir (str "/" config/default-draw-directory)))
+       (fn [_result] nil)
+       (fn [_error] nil)))))
 
 (defn save-excalidraw!
   [file data ok-handler]
@@ -63,7 +63,7 @@
         (->
          (p/do!
           (create-draws-directory! repo)
-          (fs/write-file repo repo-dir path data)
+          (fs/write-file! repo repo-dir path data nil)
           (git-handler/git-add repo path)
           (ok-handler file)
           (let [modified-at (tc/to-long (t/now))]

+ 6 - 6
src/main/frontend/handler/editor.cljs

@@ -609,10 +609,10 @@
              ;; fix editing template with multiple headings
              (when (> (count blocks) 1)
                (let [new-value (-> (text/remove-level-spaces (:block/content (first blocks)) (:block/format (first blocks)))
-                                  (string/trim-newline))
-                    edit-input-id (state/get-edit-input-id)]
-                (when edit-input-id
-                  (state/set-edit-content! edit-input-id new-value))))
+                                   (string/trim-newline))
+                     edit-input-id (state/get-edit-input-id)]
+                 (when edit-input-id
+                   (state/set-edit-content! edit-input-id new-value))))
 
              (when (or (seq retract-refs) pre-block?)
                (ui-handler/re-render-root!))
@@ -1565,7 +1565,7 @@
             filename (str (gen-filename index file) ext)
             filename (str path "/" filename)]
         ;(js/console.debug "Write asset #" filename file)
-        (p/then (fs/write-file repo dir filename (.stream file))
+        (p/then (fs/write-file! repo dir filename (.stream file) nil)
                 #(p/resolved [filename file])))))))
 
 (defonce *assets-url-cache (atom {}))
@@ -1595,7 +1595,7 @@
     (save-block! repo block content)
     (when local?
       ;; FIXME: should be relative to current block page path
-      (fs/unlink (str (util/get-repo-dir repo) (string/replace href #"^../" "/")) nil))))
+      (fs/unlink! (str (util/get-repo-dir repo) (string/replace href #"^../" "/")) nil))))
 
 (defn upload-image
   [id files format uploading? drop-or-paste?]

+ 12 - 10
src/main/frontend/handler/file.cljs

@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [load-file])
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [promesa.core :as p]
             [frontend.state :as state]
             [frontend.db :as db]
@@ -161,8 +162,8 @@
         (reset-file! repo path content))
       (db/set-file-content! repo path content))
     (util/p-handle
-     (fs/write-file repo (util/get-repo-dir repo) path content {:old-content original-content
-                                                                :last-modified-at (db/get-file-last-modified-at repo path)})
+     (fs/write-file! repo (util/get-repo-dir repo) path content {:old-content original-content
+                                                                 :last-modified-at (db/get-file-last-modified-at repo path)})
      (fn [_]
        (git-handler/git-add repo path update-status?)
        (when (= path (str config/app-name "/" config/config-file))
@@ -220,10 +221,10 @@
                     reset? false}} file->content]
   (let [write-file-f (fn [[path content]]
                        (let [original-content (get file->content path)]
-                         (-> (p/let [_ (fs/check-directory-permission! repo)]
-                               (fs/write-file repo (util/get-repo-dir repo) path content
-                                              {:old-content original-content
-                                               :last-modified-at (db/get-file-last-modified-at repo path)}))
+                         (-> (p/let [_ (nfs/check-directory-permission! repo)]
+                               (fs/write-file! repo (util/get-repo-dir repo) path content
+                                               {:old-content original-content
+                                                :last-modified-at (db/get-file-last-modified-at repo path)}))
                              (p/catch (fn [error]
                                         (log/error :write-file/failed {:path path
                                                                        :content content
@@ -263,10 +264,11 @@
   (when-not (string/blank? file)
     (->
      (p/let [_ (git/remove-file repo file)
-             result (fs/unlink (str (util/get-repo-dir repo)
-                                    "/"
-                                    file)
-                               nil)]
+             result (fs/unlink! (str
+                                 (util/get-repo-dir repo)
+                                 "/"
+                                 file)
+                                nil)]
        (when-let [file (db/entity repo [:file/path file])]
          (common-handler/check-changed-files-status)
          (let [file-id (:db/id file)

+ 1 - 2
src/main/frontend/handler/image.cljs

@@ -35,8 +35,7 @@
                      path)]
           (util/p-handle
            (fs/read-file (util/get-repo-dir (state/get-current-repo))
-                           path
-                           {})
+                         path)
            (fn [blob]
              (let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
                    img-url (image/create-object-url blob)]

+ 8 - 8
src/main/frontend/handler/page.cljs

@@ -50,7 +50,7 @@
          journal-page? (date/valid-journal-title? title)
          directory (get-directory journal-page?)]
      (when dir
-       (p/let [_ (-> (fs/mkdir (str dir "/" directory))
+       (p/let [_ (-> (fs/mkdir! (str dir "/" directory))
                      (p/catch (fn [_e])))]
          (let [format (name (state/get-preferred-format))
                page (string/lower-case title)
@@ -273,10 +273,10 @@
               ;; remove file
               (->
                (p/let [_ (git/remove-file repo file-path)
-                       _ (fs/unlink (str (util/get-repo-dir repo)
-                                         "/"
-                                         file-path)
-                                    nil)]
+                       _ (fs/unlink! (str (util/get-repo-dir repo)
+                                          "/"
+                                          file-path)
+                                     nil)]
                  (common-handler/check-changed-files-status)
                  (repo-handler/push-if-auto-enabled! repo))
                (p/catch (fn [err]
@@ -310,9 +310,9 @@
         (d/transact! conn [{:db/id (:db/id file)
                             :file/path new-path}])))
     (->
-     (p/let [_ (fs/rename repo
-                          (str (util/get-repo-dir repo) "/" old-path)
-                          (str (util/get-repo-dir repo) "/" new-path))
+     (p/let [_ (fs/rename! repo
+                           (str (util/get-repo-dir repo) "/" old-path)
+                           (str (util/get-repo-dir repo) "/" new-path))
              _ (when-not (config/local-db? repo)
                  (git/rename repo old-path new-path))]
        (common-handler/check-changed-files-status)

+ 6 - 6
src/main/frontend/handler/repo.cljs

@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [clone])
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [promesa.core :as p]
             [lambdaisland.glogi :as log]
             [frontend.state :as state]
@@ -136,7 +137,7 @@
            empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
        (when (or empty-blocks?
                  (not page-exists?))
-         (p/let [_ (fs/check-directory-permission! repo-url)
+         (p/let [_ (nfs/check-directory-permission! repo-url)
                  _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
                  file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
            (when-not file-exists?
@@ -319,7 +320,7 @@
                                      (util/get-block-idx-inside-container block-element))]
        (when (and idx container)
          (state/set-state! :editor/last-edit-block {:block edit-block
-                                               :idx idx
+                                                    :idx idx
                                                     :container (gobj/get container "id")})))
 
      (db/transact-react!
@@ -331,8 +332,7 @@
          (when (seq children-tx)
            (db/transact! repo children-tx))))
      (when (seq files)
-       (file-handler/alter-files repo files opts))
-     )))
+       (file-handler/alter-files repo files opts)))))
 
 (declare push)
 
@@ -539,7 +539,7 @@
                       (db/remove-conn! url)
                       (db/remove-db! url)
                       (db/remove-files-db! url)
-                      (fs/rmdir (util/get-repo-dir url))
+                      (fs/rmdir! (util/get-repo-dir url))
                       (state/delete-repo! repo))]
     (if (config/local-db? url)
       (p/let [_ (idb/clear-local-db! url)] ; clear file handles
@@ -645,7 +645,7 @@
     (db/clear-query-state!)
     (-> (p/do! (db/remove-db! url)
                (db/remove-files-db! url)
-               (fs/rmdir (util/get-repo-dir url))
+               (fs/rmdir! (util/get-repo-dir url))
                (clone-and-load-db url))
         (p/catch (fn [error]
                    (prn "Delete repo failed, error: " error))))))

+ 7 - 6
src/main/frontend/handler/web/nfs.cljs

@@ -16,6 +16,7 @@
             [clojure.set :as set]
             [frontend.ui :as ui]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.config :as config]
@@ -86,7 +87,7 @@
                          [handle-path handle]))
                      handles)]
     (doseq [[path handle] handles]
-      (fs/add-nfs-file-handle! path handle))
+      (nfs/add-nfs-file-handle! path handle))
     (set-files-aux! handles)))
 
 (defn ls-dir-files
@@ -103,7 +104,7 @@
              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)
+             _ (nfs/add-nfs-file-handle! root-handle-path root-handle)
              result (nth result 1)
              files (-> (->db-files dir-name result)
                        remove-ignore-files)
@@ -164,7 +165,7 @@
      (ui/button
       "Grant"
       :on-click (fn []
-                  (fs/check-directory-permission! repo)
+                  (nfs/check-directory-permission! repo)
                   (close-fn)))]))
 
 (defn ask-permission-if-local? []
@@ -205,7 +206,7 @@
        (->
         (p/let [handle (idb/get-item handle-path)]
           (when handle
-            (p/let [_ (when handle (common-handler/verify-permission repo handle true))
+            (p/let [_ (when handle (nfs/verify-permission repo handle true))
                     files-result (utils/getFiles handle true
                                                  (fn [path handle]
                                                    (swap! path-handles assoc path handle)))
@@ -232,7 +233,7 @@
                           (p/all (map (fn [path]
                                         (let [handle-path (str handle-path path)]
                                           (idb/remove-item! handle-path)
-                                          (fs/remove-nfs-file-handle! handle-path))) deleted))))
+                                          (nfs/remove-nfs-file-handle! handle-path))) deleted))))
                     added-or-modified (set (concat added modified))
                     _ (when (seq added-or-modified)
                         (p/all (map (fn [path]
@@ -289,4 +290,4 @@
 
 (defn supported?
   []
-  (utils/nfsSupported))
+  (utils/nfsSupported))

+ 6 - 0
src/main/frontend/util.cljc

@@ -50,6 +50,12 @@
       (when-not node-test?
         (re-find #"Mobi" js/navigator.userAgent))))
 
+#?(:cljs
+   (defn electron?
+     []
+     (let [ua (string/lower-case js/navigator.userAgent)]
+       (string/includes? ua " electron"))))
+
 (defn format
   [fmt & args]
   #?(:cljs (apply gstring/format fmt args)

+ 3 - 3
yarn.lock

@@ -1349,9 +1349,9 @@ copy-props@^2.0.1:
     is-plain-object "^2.0.1"
 
 core-js@^3.6.5:
-  version "3.8.2"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.2.tgz#0a1fd6709246da9ca8eff5bb0cbd15fba9ac7044"
-  integrity sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
+  integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
 
 core-util-is@~1.0.0:
   version "1.0.2"