瀏覽代碼

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 年之前
父節點
當前提交
45e04e952b

+ 1 - 1
android/app/src/main/java/com/logseq/app/FolderPicker.java

@@ -63,7 +63,7 @@ public class FolderPicker extends Plugin {
         if (path == null || path.isEmpty()) {
             call.reject("Cannot support this directory type: " + docUri);
         } else {
-            ret.put("path", path);
+            ret.put("path", "file://" + path);
             call.resolve(ret);
         }
     }

+ 1 - 1
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -133,7 +133,7 @@ public class FsWatcher extends Plugin {
         // path.
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
-        obj.put("dir", mPath);
+        obj.put("dir", "file://" + mPath);
 
         switch (event) {
             case FileObserver.CLOSE_WRITE:

+ 3 - 3
src/electron/electron/backup_file.cljs

@@ -25,11 +25,11 @@
   (get-backup-dir* repo relative-path version-file-dir))
 
 (defn- truncate-old-versioned-files!
-  "reserve the latest 3 version files"
+  "reserve the latest 6 version files"
   [dir]
   (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
         files (mapv #(.-name %) files)
-        old-versioned-files (drop 3 (reverse (sort files)))]
+        old-versioned-files (drop 6 (reverse (sort files)))]
     (doseq [file old-versioned-files]
       (fs-extra/removeSync (path/join dir file)))))
 
@@ -44,7 +44,7 @@
                :version-file-dir (get-version-file-dir repo relative-path))
         new-path (path/join dir*
                             (str (string/replace (.toISOString (js/Date.)) ":" "_")
-                                 ext))]
+                                 ".Desktop" ext))]
     (fs-extra/ensureDirSync dir*)
     (fs/writeFileSync new-path content)
     (fs/statSync new-path)

+ 15 - 3
src/main/frontend/config.cljs

@@ -310,11 +310,15 @@
     path
     (util/node-path.join (get-repo-dir repo-url) path)))
 
+;; FIXME: There is another get-file-path at src/main/frontend/fs/capacitor_fs.cljs
 (defn get-file-path
   "Normalization happens here"
   [repo-url relative-path]
   (when (and repo-url relative-path)
     (let [path (cond
+                 (demo-graph?)
+                 nil
+
                  (and (util/electron?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)]
                    (if (string/starts-with? relative-path dir)
@@ -324,7 +328,7 @@
 
                  (and (mobile-util/native-ios?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)]
-                   (js/decodeURI (str dir relative-path)))
+                   (str dir relative-path))
 
                  (and (mobile-util/native-android?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)
@@ -332,14 +336,22 @@
                                    (string/starts-with? dir "content:"))
                              dir
                              (str "file:///" (string/replace dir #"^/+" "")))]
-                   (str (string/replace dir #"/+$" "") "/" relative-path))
+                   (util/safe-path-join dir relative-path))
 
                  (= "/" (first relative-path))
                  (subs relative-path 1)
 
                  :else
                  relative-path)]
-      (gp-util/path-normalize path))))
+      (and (not-empty path) (gp-util/path-normalize path)))))
+
+(defn get-page-file-path
+  "Get the path to the page file for the given page. This is used when creating new files."
+  [repo-url sub-dir page-name ext]
+  (let [page-basename (if (mobile-util/native-platform?)
+                        (util/url-encode page-name)
+                        page-name)]
+    (get-file-path repo-url (str sub-dir "/" page-basename "." ext))))
 
 (defn get-config-path
   ([]

+ 107 - 93
src/main/frontend/fs/capacitor_fs.cljs

@@ -18,27 +18,58 @@
     []
     (.ensureDocuments mobile-util/ios-file-container)))
 
-(defn check-permission-android []
-  (p/let [permission (.checkPermissions Filesystem)
-          permission (-> permission
-                         bean/->clj
-                         :publicStorage)]
-    (when-not (= permission "granted")
-      (p/do!
-       (.requestPermissions Filesystem)))))
+(when (mobile-util/native-android?)
+  (defn- android-check-permission []
+    (p/let [permission (.checkPermissions Filesystem)
+            permission (-> permission
+                           bean/->clj
+                           :publicStorage)]
+      (when-not (= permission "granted")
+        (p/do!
+         (.requestPermissions Filesystem))))))
 
-(defn- clean-uri
-  [uri]
-  (when (string? uri)
-    (util/url-decode uri)))
+(defn- <write-file-with-utf8
+  [path content]
+  (when-not (string/blank? path)
+    (-> (p/chain (.writeFile Filesystem (clj->js {:path path
+                                                  :data content
+                                                  :encoding (.-UTF8 Encoding)
+                                                  :recursive true}))
+                 #(js->clj % :keywordize-keys true))
+        (p/catch (fn [error]
+                   (js/console.error "writeFile Error: " path ": " error)
+                   nil)))))
 
-(defn- read-file-utf8
+(defn- <read-file-with-utf8
   [path]
   (when-not (string/blank? path)
-    (.readFile Filesystem
-               (clj->js
-                {:path path
-                 :encoding (.-UTF8 Encoding)}))))
+    (-> (p/chain (.readFile Filesystem (clj->js {:path path
+                                                 :encoding (.-UTF8 Encoding)}))
+                 #(js->clj % :keywordize-keys true)
+                 #(get % :data nil))
+        (p/catch (fn [error]
+                   (js/console.error "readFile Error: " path ": " error)
+                   nil)))))
+
+(defn- <readdir [path]
+  (-> (p/chain (.readdir Filesystem (clj->js {:path path}))
+               js->clj
+               #(get % "files" nil))
+      (p/catch (fn [error]
+                 (js/console.error "readdir Error: " path ": " error)
+                 nil))))
+
+(defn- <stat [path]
+  (-> (p/chain (.stat Filesystem (clj->js {:path path}))
+               #(js->clj % :keywordize-keys true)
+               #(update % :type (fn [v]
+                                  (case v
+                                    "NSFileTypeDirectory" "directory"
+                                    "NSFileTypeRegular" "file"
+                                    v))))
+      (p/catch (fn [error]
+                 (js/console.error "stat Error: " path ": " error)
+                 nil))))
 
 (defn readdir
   "readdir recursively"
@@ -48,10 +79,7 @@
                    (if (empty? dirs)
                      result
                      (p/let [d (first dirs)
-                             files (.readdir Filesystem (clj->js {:path d}))
-                             files (-> files
-                                       js->clj
-                                       (get "files" []))
+                             files (<readdir d)
                              files (->> files
                                         (remove (fn [file]
                                                   (or (string/starts-with? file ".")
@@ -61,44 +89,31 @@
                                                       (= file "bak")))))
                              files (->> files
                                         (map (fn [file]
+                                               ;; TODO: use uri-join
                                                (str (string/replace d #"/+$" "")
                                                     "/"
                                                     (if (mobile-util/native-ios?)
-                                                      (util/url-encode file)
+                                                      (js/encodeURI file)
                                                       file)))))
-                             files-with-stats (p/all
-                                               (mapv
-                                                (fn [file]
-                                                  (p/chain
-                                                   (.stat Filesystem (clj->js {:path file}))
-                                                   #(js->clj % :keywordize-keys true)))
-                                                files))
+                             files-with-stats (p/all (mapv <stat files))
                              files-dir (->> files-with-stats
-                                            (filterv
-                                             (fn [{:keys [type]}]
-                                               (contains? #{"directory" "NSFileTypeDirectory"} type)))
+                                            (filterv #(= (:type %) "directory"))
                                             (mapv :uri))
                              files-result
                              (p/all
                               (->> files-with-stats
-                                   (filter
-                                    (fn [{:keys [type]}]
-                                      (contains? #{"file" "NSFileTypeRegular"} type)))
+                                   (filter #(= (:type %) "file"))
                                    (filter
                                     (fn [{:keys [uri]}]
                                       (some #(string/ends-with? uri %)
                                             [".md" ".markdown" ".org" ".edn" ".css"])))
                                    (mapv
                                     (fn [{:keys [uri] :as file-result}]
-                                      (p/chain
-                                       (read-file-utf8 uri)
-                                       #(js->clj % :keywordize-keys true)
-                                       :data
-                                       #(assoc file-result :content %))))))]
+                                      (p/chain (<read-file-with-utf8 uri)
+                                               #(assoc file-result :content %))))))]
                        (p/recur (concat result files-result)
-                                (concat (rest dirs) files-dir)))))
-          result (js->clj result :keywordize-keys true)]
-    (map (fn [result] (update result :uri clean-uri)) result)))
+                                (concat (rest dirs) files-dir)))))]
+    (js->clj result :keywordize-keys true)))
 
 (defn- contents-matched?
   [disk-content db-content]
@@ -113,36 +128,50 @@
   [repo-dir path ext]
   (let [relative-path (-> (string/replace path repo-dir "")
                           (string/replace (str "." ext) ""))]
-    (str repo-dir backup-dir "/" relative-path)))
+    (util/safe-path-join repo-dir (str backup-dir "/" relative-path))))
 
 (defn- truncate-old-versioned-files!
-  "reserve the latest 3 version files"
+  "reserve the latest 6 version files"
   [dir]
   (p/let [files (readdir dir)
           files (js->clj files :keywordize-keys true)
-          old-versioned-files (drop 3 (reverse (sort-by :mtime files)))]
+          old-versioned-files (drop 6 (reverse (sort-by :mtime files)))]
     (mapv (fn [file]
-            (.deleteFile Filesystem (clj->js {:path (js/encodeURI (:uri file))})))
+            (.deleteFile Filesystem (clj->js {:path (:uri file)})))
           old-versioned-files)))
 
 (defn backup-file
   [repo-dir path content ext]
   (let [backup-dir (get-backup-dir repo-dir path ext)
         new-path (str backup-dir "/" (string/replace (.toISOString (js/Date.)) ":" "_") "." ext)]
-    (.writeFile Filesystem (clj->js {:data content
-                                     :path new-path
-                                     :encoding (.-UTF8 Encoding)
-                                     :recursive true}))
+    (<write-file-with-utf8 new-path content)
     (truncate-old-versioned-files! backup-dir)))
 
+(defn backup-file-handle-changed!
+  [repo-dir file-path content]
+  (let [divider-schema    "://"
+        file-schema       (string/split file-path divider-schema)
+        file-schema       (if (> (count file-schema) 1) (first file-schema) "")
+        dir-schema?       (and (string? repo-dir)
+                               (string/includes? repo-dir divider-schema))
+        repo-dir          (if-not dir-schema?
+                            (str file-schema divider-schema repo-dir) repo-dir)
+        backup-root       (util/safe-path-join repo-dir backup-dir)
+        backup-dir-parent (util/node-path.dirname file-path)
+        backup-dir-parent (string/replace backup-dir-parent repo-dir "")
+        backup-dir-name (util/node-path.name file-path)
+        file-extname (.extname util/node-path file-path)
+        file-root (util/safe-path-join backup-root backup-dir-parent backup-dir-name)
+        file-path (util/safe-path-join file-root
+                                       (str (string/replace (.toISOString (js/Date.)) ":" "_") "." (mobile-util/platform) file-extname))]
+    (<write-file-with-utf8 file-path content)
+    (truncate-old-versioned-files! file-root)))
+
 (defn- write-file-impl!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
     (p/catch
-     (p/let [result (.writeFile Filesystem (clj->js {:path path
-                                                     :data content
-                                                     :encoding (.-UTF8 Encoding)
-                                                     :recursive true}))]
+     (p/let [result (<write-file-with-utf8 path content)]
        (when ok-handler
          (ok-handler repo path result)))
      (fn [error]
@@ -150,16 +179,12 @@
          (error-handler error)
          (log/error :write-file-failed error))))
 
-    (p/let [disk-content (-> (p/chain (read-file-utf8 path)
-                                      #(js->clj % :keywordize-keys true)
-                                      :data)
-                             (p/catch (fn [error]
-                                        (js/console.error error)
-                                        nil)))
+    ;; Compare with disk content and backup if not equal
+    (p/let [disk-content (<read-file-with-utf8 path)
             disk-content (or disk-content "")
             repo-dir (config/get-local-dir repo)
-            ext (string/lower-case (util/get-file-ext path))
-            db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
+            ext (util/get-file-ext path)
+            db-content (or old-content (db/get-file repo path) "")
             contents-matched? (contents-matched? disk-content db-content)
             pending-writes (state/get-write-chan-length)]
       (cond
@@ -174,10 +199,7 @@
 
         :else
         (->
-         (p/let [result (.writeFile Filesystem (clj->js {:path path
-                                                         :data content
-                                                         :encoding (.-UTF8 Encoding)
-                                                         :recursive true}))
+         (p/let [result (<write-file-with-utf8 path content)
                  mtime (-> (js->clj stat :keywordize-keys true)
                            :mtime)]
            (when-not contents-matched?
@@ -186,7 +208,7 @@
            (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
                              (encrypt/decrypt content)
                              content)]
-             (db/set-file-content! repo (js/decodeURI path) content))
+             (db/set-file-content! repo path content))
            (when ok-handler
              (ok-handler repo path result))
            result)
@@ -196,25 +218,19 @@
                       (log/error :write-file-failed error)))))))))
 
 (defn get-file-path [dir path]
-  (let [[dir path] (map #(some-> %
-                                 js/decodeURI)
-                        [dir path])
-        dir (some-> dir (string/replace #"/+$" ""))
-        path (some-> path (string/replace #"^/+" ""))
-        path (cond (nil? path)
-                   dir
+  (let [dir (some-> dir (string/replace #"/+$" ""))
+        path (some-> path (string/replace #"^/+" ""))]
+    (cond (nil? path)
+          dir
 
-                   (nil? dir)
-                   path
+          (nil? dir)
+          path
 
-                   (string/starts-with? path dir)
-                   path
+          (string/starts-with? path dir)
+          path
 
-                   :else
-                   (str dir "/" path))]
-    (if (mobile-util/native-ios?)
-      (js/encodeURI (js/decodeURI path))
-      path)))
+          :else
+          (str dir "/" path))))
 
 (defn- local-container-path?
   "Check whether `path' is logseq's container `localDocumentsPath' on iOS"
@@ -263,7 +279,7 @@
                    (string/replace-first path "file://" "")
                    path)
             repo-dir (config/get-local-dir repo)
-            recycle-dir (str repo-dir config/app-name "/.recycle")
+            recycle-dir (str repo-dir config/app-name "/.recycle") ;; logseq/.recycle
             file-name (-> (string/replace path repo-dir "")
                           (string/replace "/" "_")
                           (string/replace "\\" "_"))
@@ -276,11 +292,7 @@
   (read-file [_this dir path _options]
     (let [path (get-file-path dir path)]
       (->
-       (p/let [content (read-file-utf8 path)
-               content (-> (js->clj content :keywordize-keys true)
-                           :data
-                           clj->js)]
-         content)
+       (<read-file-with-utf8 path)
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
   (write-file! [this repo dir path content opts]
@@ -288,6 +300,7 @@
       (p/let [stat (p/catch
                     (.stat Filesystem (clj->js {:path path}))
                     (fn [_e] :not-found))]
+        ;; `path` is full-path
         (write-file-impl! this repo dir path content opts stat))))
   (rename! [_this _repo old-path new-path]
     (let [[old-path new-path] (map #(get-file-path "" %) [old-path new-path])]
@@ -306,16 +319,17 @@
                                          }))]
         result)))
   (open-dir [_this _ok-handler]
-    (p/let [_    (when (= (mobile-util/platform) "android") (check-permission-android))
+    (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
             {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker)
                                                   (p/then #(js->clj % :keywordize-keys true))
                                                   (p/catch (fn [e]
                                                              (js/alert (str e))
-                                                             nil))) ;; NOTE: Can not pick folder, let it crash
+                                                             nil))) ;; NOTE: If pick folder fails, let it crash
             _ (when (and (mobile-util/native-ios?)
                          (not (or (local-container-path? path localDocumentsPath)
                                   (mobile-util/iCloud-container-path? path))))
                 (state/pub-event! [:modal/show-instruction]))
+            _ (js/console.log "Opening or Creating graph at directory: " path)
             files (readdir path)
             files (js->clj files :keywordize-keys true)]
       (into [] (concat [{:path path}] files))))
@@ -324,6 +338,6 @@
   (watch-dir! [_this dir]
     (p/do!
      (.unwatch mobile-util/fs-watcher)
-     (.watch mobile-util/fs-watcher #js {:path dir})))
+     (.watch mobile-util/fs-watcher (clj->js {:path dir}))))
   (unwatch-dir! [_this _dir]
     (.unwatch mobile-util/fs-watcher)))

+ 5 - 2
src/main/frontend/fs/watcher_handler.cljs

@@ -11,7 +11,6 @@
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [lambdaisland.glogi :as log]
-            [electron.ipc :as ipc]
             [promesa.core :as p]
             [frontend.state :as state]
             [frontend.encrypt :as encrypt]
@@ -35,7 +34,11 @@
   [repo path content db-content mtime backup?]
   (p/let [
           ;; save the previous content in a versioned bak file to avoid data overwritten.
-          _ (when backup? (ipc/ipc "backupDbFile" (config/get-local-dir repo) path db-content content))
+          _ (when backup?
+              (-> (when-let [repo-dir (config/get-local-dir repo)]
+                    (file-handler/backup-file! repo-dir path db-content content))
+                  (p/catch #(js/console.error "❌ Bak Error: " path %))))
+
           _ (file-handler/alter-file repo path content {:re-render-root? true
                                                         :from-disk? true})]
     (set-missing-block-ids! content)

+ 27 - 14
src/main/frontend/handler.cljs

@@ -1,5 +1,6 @@
 (ns frontend.handler
   (:require [cljs.reader :refer [read-string]]
+            [clojure.string :as string]
             [electron.ipc :as ipc]
             [electron.listener :as el]
             [frontend.components.block :as block]
@@ -8,7 +9,7 @@
             [frontend.components.reference :as reference]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.config :as config]
-            [frontend.context.i18n :as i18n]
+            [frontend.context.i18n :as i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db.conn :as conn]
             [frontend.db.persist :as db-persist]
@@ -30,6 +31,7 @@
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
             [frontend.storage :as storage]
+            [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.persist-var :as persist-var]
             [goog.object :as gobj]
@@ -107,7 +109,7 @@
            (state/set-db-restoring! false))))
       (p/then
        (fn []
-         (prn "db restored, setting up repo hooks")
+         (js/console.log "db restored, setting up repo hooks")
          (store-schema!)
 
          (state/pub-event! [:modal/nfs-ask-permission])
@@ -146,18 +148,6 @@
                                (.postMessage js/window #js {":datalog-console.remote/remote-message" (pr-str db)} "*")
 
                                nil)))))))
-(defn- get-repos
-  []
-  (p/let [nfs-dbs (db-persist/get-all-graphs)
-          nfs-dbs (map (fn [db]
-                         {:url db :nfs? true}) nfs-dbs)]
-    (cond
-      (seq nfs-dbs)
-      nfs-dbs
-
-      :else
-      [{:url config/local-repo
-        :example? true}])))
 
 (defn clear-cache!
   []
@@ -171,6 +161,29 @@
               (js/window.location.reload)))
      2000)))
 
+(defn- get-repos
+  []
+  (p/let [nfs-dbs (db-persist/get-all-graphs)]
+    ;; TODO: Better IndexDB migration handling
+    (cond
+      (and (mobile-util/native-platform?)
+           (some #(or (string/includes? % " ")
+                      (string/includes? % "logseq_local_/")) nfs-dbs))
+      (do (notification/show! ["DB version is not compatible, please clear cache then re-add your graph back."
+                               (ui/button
+                                (t :settings-page/clear-cache)
+                                :class    "text-sm p-1"
+                                :on-click clear-cache!)] :error false)
+          {:url config/local-repo
+           :example? true})
+
+      (seq nfs-dbs)
+      (map (fn [db] {:url db :nfs? true}) nfs-dbs)
+
+      :else
+      [{:url config/local-repo
+        :example? true}])))
+
 (defn- register-components-fns!
   []
   (state/set-page-blocks-cp! page/page-blocks-cp)

+ 4 - 3
src/main/frontend/handler/events.cljs

@@ -453,10 +453,11 @@
 (defmethod handle :file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
         payload (-> event
-                    (js->clj :keywordize-keys true)
-                    (update :path js/decodeURI))]
+                    (js->clj :keywordize-keys true))
+        ;; TODO: remove this
+        payload' (-> payload (update :path js/decodeURI))]
     (fs-watcher/handle-changed! type payload)
-    (sync/file-watch-handler type payload)))
+    (sync/file-watch-handler type payload')))
 
 (defmethod handle :rebuild-slash-commands-list [[_]]
   (page-handler/rebuild-slash-commands-list!))

+ 20 - 3
src/main/frontend/handler/file.cljs

@@ -8,11 +8,13 @@
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
+            [electron.ipc :as ipc]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
             [frontend.mobile.util :as mobile]
@@ -80,13 +82,28 @@
                    (log/error :nfs/load-files-error repo-url)
                    (log/error :exception error))))))
 
+(defn backup-file!
+  "Backup db content to bak directory"
+  [repo-url path db-content content]
+  (cond
+    (util/electron?)
+    (ipc/ipc "backupDbFile" repo-url path db-content content)
+
+    (mobile/native-platform?)
+    (capacitor-fs/backup-file-handle-changed! repo-url path db-content)
+
+    :else
+    nil))
+
 (defn- page-exists-in-another-file
   "Conflict of files towards same page"
   [repo-url page file]
   (when-let [page-name (:block/name page)]
     (let [current-file (:file/path (db/get-page-file repo-url page-name))]
       (when (not= file current-file)
-       current-file))))
+        (prn ::debug-page-exist-warn repo-url page file)
+        (js/console.trace)
+        current-file))))
 
 (defn- get-delete-blocks [repo-url first-page file]
   (let [delete-blocks (->
@@ -277,7 +294,7 @@
         path (str config/app-name "/" config/metadata-file)
         file-path (str "/" path)
         default-content (if encrypted? "{:db/encrypted? true}" "{}")]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
         (reset-file! repo-url path default-content)))))
@@ -288,7 +305,7 @@
         path (str config/app-name "/" config/pages-metadata-file)
         file-path (str "/" path)
         default-content "{}"]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
         (reset-file! repo-url path default-content)))))

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

@@ -61,7 +61,7 @@
                               "org" (rc/inline "contents.org")
                               "markdown" (rc/inline "contents.md")
                               "")]
-        (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" pages-dir))
+        (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir pages-dir))
                 file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
           (when-not file-exists?
             (file-handler/reset-file! repo-url path default-content)))))))
@@ -73,7 +73,7 @@
         path (str config/app-name "/" config/custom-css-file)
         file-path (str "/" path)
         default-content ""]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
         (file-handler/reset-file! repo-url path default-content)))))
@@ -84,7 +84,7 @@
   (let [repo-dir (config/get-repo-dir repo-url)
         path (str (config/get-pages-directory) "/how_to_make_dummy_notes.md")
         file-path (str "/" path)]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-pages-directory)))
             _file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
       (file-handler/reset-file! repo-url path content))))
 
@@ -110,14 +110,14 @@
 
                     :else
                     default-content)
-          path (str (config/get-journals-directory) "/" file-name "."
-                    (config/get-file-extension format))
+          path (util/safe-path-join (config/get-journals-directory) (str file-name "."
+                                                                         (config/get-file-extension format)))
           file-path (str "/" path)
           page-exists? (db/entity repo-url [:block/name (util/page-name-sanity-lc title)])
           empty-blocks? (db/page-empty? repo-url (util/page-name-sanity-lc title))]
       (when (or empty-blocks? (not page-exists?))
         (p/let [_ (nfs/check-directory-permission! repo-url)
-                _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
+                _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
                 file-exists? (fs/file-exists? repo-dir file-path)]
           (when-not file-exists?
             (p/let [_ (file-handler/reset-file! repo-url path content)]
@@ -133,9 +133,9 @@
   ([repo-url encrypted?]
    (spec/validate :repos/url repo-url)
    (let [repo-dir (config/get-repo-dir repo-url)]
-     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
-             _ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir))
-             _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
+     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
+             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (str config/app-name "/" config/recycle-dir)))
+             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
              _ (file-handler/create-metadata-file repo-url encrypted?)
              _ (create-config-file-if-not-exists repo-url)
              _ (create-contents-file repo-url)

+ 3 - 2
src/main/frontend/mobile/core.cljs

@@ -2,6 +2,7 @@
   (:require ["@capacitor/app" :refer [^js App]]
             ["@capacitor/keyboard" :refer [^js Keyboard]]
             [clojure.string :as string]
+            [promesa.core :as p]
             [frontend.fs.capacitor-fs :as mobile-fs]
             [frontend.handler.editor :as editor-handler]
             [frontend.mobile.deeplink :as deeplink]
@@ -20,8 +21,8 @@
 (defn- ios-init
   "Initialize iOS-specified event listeners"
   []
-  (let [path (mobile-fs/iOS-ensure-documents!)]
-    (println "iOS container path: " path))
+  (p/let [path (mobile-fs/iOS-ensure-documents!)]
+    (println "iOS container path: " (js->clj path)))
 
   (state/pub-event! [:validate-appId])
   

+ 6 - 10
src/main/frontend/modules/file/core.cljs

@@ -125,16 +125,12 @@
                        (date/date->file-name journal-page?)
                        (-> (or (:block/original-name page) (:block/name page))
                            (util/file-name-sanity)))
-            path (str
-                  (cond
-                    journal-page?    (config/get-journals-directory)
-                    whiteboard-page? (config/get-whiteboards-directory)
-                    :else            (config/get-pages-directory))
-                  "/"
-                  filename
-                  "."
-                  (if (= format "markdown") "md" format))
-            file-path (config/get-file-path repo path)
+            sub-dir (cond
+                      journal-page?    (config/get-journals-directory)
+                      whiteboard-page? (config/get-whiteboards-directory)
+                      :else            (config/get-pages-directory))
+            ext (if (= format "markdown") "md" format)
+            file-path (config/get-page-file-path repo sub-dir filename ext)
             file {:file/path file-path}
             tx [{:file/path file-path}
                 {:block/name (:block/name page)

+ 5 - 5
src/main/frontend/util.cljc

@@ -10,6 +10,7 @@
             [cljs-bean.core :as bean]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
+            [clojure.pprint]
             [dommy.core :as d]
             [frontend.mobile.util :refer [native-platform?]]
             [logseq.graph-parser.util :as gp-util]
@@ -487,6 +488,10 @@
   (if (string? s)
     (string/lower-case s) s))
 
+#?(:cljs
+   (defn safe-path-join [prefix & paths]
+     (apply node-path.join (cons prefix paths))))
+
 (defn trim-safe
   [s]
   (when s
@@ -891,11 +896,6 @@
      [string]
      (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
 
-#?(:cljs
-   (defn url-decode
-     [string]
-     (some-> string str (js/decodeURIComponent))))
-
 (def windows-reserved-chars #"[:\\*\\?\"<>|]+")
 
 #?(:cljs

+ 15 - 0
src/main/frontend/utils.js

@@ -310,5 +310,20 @@ export const nodePath = Object.assign({}, path, {
   extname (input) {
     input = toPosixPath(input)
     return path.extname(input)
+  },
+
+  join (input, ...paths) {
+    let orURI = null
+
+    try {
+      orURI = new URL(input)
+      input = input.replace(orURI.protocol + '//', '')
+        .replace(orURI.protocol, '')
+        .replace(/^\/+/, '/')
+    } catch (_e) {}
+
+    input = path.join(input, ...paths)
+
+    return (orURI ? (orURI.protocol + '//') : '') + input
   }
 })