123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- (ns frontend.handler.repo
- (:refer-clojure :exclude [clone])
- (:require [clojure.string :as string]
- [frontend.config :as config]
- [frontend.context.i18n :refer [t]]
- [frontend.date :as date]
- [frontend.db :as db]
- [frontend.fs :as fs]
- [frontend.fs.nfs :as nfs]
- [frontend.handler.common :as common-handler]
- [frontend.handler.file :as file-handler]
- [frontend.handler.route :as route-handler]
- [frontend.handler.ui :as ui-handler]
- [frontend.handler.metadata :as metadata-handler]
- [frontend.idb :as idb]
- [frontend.search :as search]
- [frontend.spec :as spec]
- [frontend.state :as state]
- [frontend.util :as util]
- [frontend.util.fs :as util-fs]
- [lambdaisland.glogi :as log]
- [promesa.core :as p]
- [shadow.resource :as rc]
- [frontend.db.persist :as db-persist]
- [logseq.graph-parser.util :as gp-util]
- [logseq.graph-parser :as graph-parser]
- [electron.ipc :as ipc]
- [cljs-bean.core :as bean]
- [clojure.core.async :as async]
- [frontend.encrypt :as encrypt]
- [frontend.mobile.util :as mobile-util]))
- ;; Project settings should be checked in two situations:
- ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
- ;; 2. Git pulls the new change (fn: load-files)
- (defn create-config-file-if-not-exists
- [repo-url]
- (spec/validate :repos/url repo-url)
- (let [repo-dir (config/get-repo-dir repo-url)
- app-dir config/app-name
- dir (str repo-dir "/" app-dir)]
- (p/let [_ (fs/mkdir-if-not-exists dir)]
- (let [default-content config/config-default-content
- path (str app-dir "/" config/config-file)]
- (p/let [file-exists? (fs/create-if-not-exists repo-url repo-dir (str app-dir "/" config/config-file) default-content)]
- (when-not file-exists?
- (file-handler/reset-file! repo-url path default-content)
- (common-handler/reset-config! repo-url default-content)))))))
- (defn create-contents-file
- [repo-url]
- (spec/validate :repos/url repo-url)
- (p/let [repo-dir (config/get-repo-dir repo-url)
- pages-dir (state/get-pages-directory)
- [org-path md-path] (map #(str "/" pages-dir "/contents." %) ["org" "md"])
- contents-file-exist? (some #(fs/file-exists? repo-dir %) [org-path md-path])]
- (when-not contents-file-exist?
- (let [format (state/get-preferred-format)
- path (str pages-dir "/contents."
- (config/get-file-extension format))
- file-path (str "/" path)
- default-content (case (name format)
- "org" (rc/inline "contents.org")
- "markdown" (rc/inline "contents.md")
- "")]
- (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)))))))
- (defn create-custom-theme
- [repo-url]
- (spec/validate :repos/url repo-url)
- (let [repo-dir (config/get-repo-dir repo-url)
- path (str config/app-name "/" config/custom-css-file)
- file-path (str "/" path)
- default-content ""]
- (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)))))
- (defn create-dummy-notes-page
- [repo-url content]
- (spec/validate :repos/url repo-url)
- (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 (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))))
- (defn- create-today-journal-if-not-exists
- [repo-url {:keys [content]}]
- (spec/validate :repos/url repo-url)
- (when (state/enable-journals? repo-url)
- (let [repo-dir (config/get-repo-dir repo-url)
- format (state/get-preferred-format repo-url)
- title (date/today)
- file-name (date/journal-title->default title)
- default-content (util/default-content-with-title format)
- template (state/get-default-journal-template)
- template (when (and template
- (not (string/blank? template)))
- template)
- content (cond
- content
- content
- template
- (str default-content template)
- :else
- default-content)
- 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 (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)]
- (p/let [_ (fs/create-if-not-exists repo-url repo-dir file-path content)]
- (when-not (state/editing?)
- (ui-handler/re-render-root!)))))
- (when-not (state/editing?)
- (ui-handler/re-render-root!)))))))
- (defn create-default-files!
- ([repo-url]
- (create-default-files! repo-url false))
- ([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 (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)
- _ (create-custom-theme repo-url)]
- (state/pub-event! [:page/create-today-journal repo-url])))))
- (defn- load-pages-metadata!
- "force?: if set true, skip the metadata timestamp range check"
- ([repo file-paths files]
- (load-pages-metadata! repo file-paths files false))
- ([repo file-paths files force?]
- (try
- (let [file (config/get-pages-metadata-path)]
- (when (contains? (set file-paths) file)
- (when-let [content (some #(when (= (:file/path %) file) (:file/content %)) files)]
- (let [metadata (common-handler/safe-read-string content "Parsing pages metadata file failed: ")
- pages (db/get-all-pages repo)
- pages (zipmap (map :block/name pages) pages)
- metadata (->>
- (filter (fn [{:block/keys [name created-at updated-at]}]
- (when-let [page (get pages name)]
- (and
- (>= updated-at created-at) ;; metadata validation
- (or force? ;; when force is true, shortcut timestamp range check
- (and (or (nil? (:block/created-at page))
- (>= created-at (:block/created-at page)))
- (or (nil? (:block/updated-at page))
- (>= updated-at (:block/created-at page)))))
- (or ;; persistent metadata is the gold standard
- (not= created-at (:block/created-at page))
- (not= updated-at (:block/created-at page)))))) metadata)
- (remove nil?))]
- (when (seq metadata)
- (db/transact! repo metadata {:new-graph? true}))))))
- (catch js/Error e
- (log/error :exception e)))))
- (defn update-pages-metadata!
- "update pages meta content -> db. Only accept non-encrypted content!"
- [repo content force?]
- (let [path (config/get-pages-metadata-path)
- files [{:file/path path
- :file/content content}]
- file-paths [path]]
- (load-pages-metadata! repo file-paths files force?)))
- (defn- parse-and-load-file!
- [repo-url file {:keys [new-graph? verbose]}]
- (try
- (file-handler/alter-file repo-url
- (:file/path file)
- (:file/content file)
- (merge {:new-graph? new-graph?
- :re-render-root? false
- :from-disk? true}
- (when (some? verbose) {:verbose verbose})))
- (catch :default e
- (state/set-parsing-state! (fn [m]
- (update m :failed-parsing-files conj [(:file/path file) e])))))
- (state/set-parsing-state! (fn [m]
- (update m :finished inc))))
- (defn- after-parse
- [repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan]
- (load-pages-metadata! repo-url file-paths files true)
- (when (or (:new-graph? opts) (not (:refresh? opts)))
- (if (and (not db-encrypted?) (state/enable-encryption? repo-url))
- (state/pub-event! [:modal/encryption-setup-dialog repo-url
- #(create-default-files! repo-url %)])
- (create-default-files! repo-url db-encrypted?)))
- (when re-render?
- (ui-handler/re-render-root! re-render-opts))
- (state/pub-event! [:graph/added repo-url opts])
- (state/reset-parsing-state!)
- (state/set-loading-files! repo-url false)
- (async/offer! graph-added-chan true))
- (defn- parse-files-and-create-default-files-inner!
- [repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts]
- (let [supported-files (graph-parser/filter-files files)
- delete-data (->> (concat delete-files delete-blocks)
- (remove nil?))
- chan (async/to-chan! supported-files)
- graph-added-chan (async/promise-chan)]
- (when (seq delete-data) (db/transact! repo-url delete-data))
- (state/set-current-repo! repo-url)
- (state/set-parsing-state! {:total (count supported-files)})
- ;; Synchronous for tests for not breaking anything
- (if util/node-test?
- (do
- (doseq [file supported-files]
- (state/set-parsing-state! (fn [m]
- (assoc m :current-parsing-file (:file/path file))))
- (parse-and-load-file! repo-url file (select-keys opts [:new-graph? :verbose])))
- (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))
- (async/go-loop []
- (if-let [file (async/<! chan)]
- (do
- (state/set-parsing-state! (fn [m]
- (assoc m :current-parsing-file (:file/path file))))
- (async/<! (async/timeout 10))
- (parse-and-load-file! repo-url file (select-keys opts [:new-graph? :verbose]))
- (recur))
- (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))))
- graph-added-chan))
- (defn- parse-files-and-create-default-files!
- [repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts]
- (if db-encrypted?
- (p/let [files (p/all
- (map (fn [file]
- (p/let [content (encrypt/decrypt (:file/content file))]
- (assoc file :file/content content)))
- files))]
- (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts))
- (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts)))
- (defn parse-files-and-load-to-db!
- [repo-url files {:keys [delete-files delete-blocks re-render? re-render-opts _refresh?] :as opts
- :or {re-render? true}}]
- (let [file-paths (map :file/path files)
- metadata-file (config/get-metadata-path)
- metadata-content (some #(when (= (:file/path %) metadata-file)
- (:file/content %)) files)
- metadata (when metadata-content
- (common-handler/read-metadata! metadata-content))
- db-encrypted? (:db/encrypted? metadata)
- db-encrypted-secret (if db-encrypted? (:db/encrypted-secret metadata) nil)]
- (if db-encrypted?
- (let [close-fn #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts)]
- (state/set-state! :encryption/graph-parsing? true)
- (state/pub-event! [:modal/encryption-input-secret-dialog repo-url
- db-encrypted-secret
- close-fn]))
- (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts))))
- (defn load-repo-to-db!
- [repo-url {:keys [diffs nfs-files refresh? new-graph? empty-graph?]}]
- (spec/validate :repos/url repo-url)
- (route-handler/redirect-to-home!)
- (state/set-parsing-state! {:graph-loading? true})
- (let [config (or (when-let [content (some-> (first (filter #(= (config/get-config-path repo-url) (:file/path %)) nfs-files))
- :file/content)]
- (common-handler/read-config content))
- (state/get-config repo-url))
- ;; NOTE: Use config while parsing. Make sure it's the corrent journal title format
- _ (state/set-config! repo-url config)
- relate-path-fn (fn [m k]
- (some-> (get m k)
- (string/replace (js/decodeURI (config/get-local-dir repo-url)) "")))
- nfs-files (common-handler/remove-hidden-files nfs-files config #(relate-path-fn % :file/path))
- diffs (common-handler/remove-hidden-files diffs config #(relate-path-fn % :path))
- load-contents (fn [files option]
- (file-handler/load-files-contents!
- repo-url
- files
- (fn [files-contents]
- (parse-files-and-load-to-db! repo-url files-contents (assoc option :refresh? refresh?)))))]
- (cond
- (and (not (seq diffs)) nfs-files)
- (parse-files-and-load-to-db! repo-url nfs-files {:new-graph? new-graph?
- :empty-graph? empty-graph?})
- :else
- (when (seq diffs)
- (let [filter-diffs (fn [type] (->> (filter (fn [f] (= type (:type f))) diffs)
- (map :path)))
- remove-files (filter-diffs "remove")
- modify-files (filter-diffs "modify")
- add-files (filter-diffs "add")
- delete-files (when (seq remove-files)
- (db/delete-files remove-files))
- delete-blocks (db/delete-blocks repo-url remove-files true)
- delete-blocks (->>
- (concat
- delete-blocks
- (db/delete-blocks repo-url modify-files false))
- (remove nil?))
- delete-pages (if (seq remove-files)
- (db/delete-pages-by-files remove-files)
- [])
- add-or-modify-files (some->>
- (concat modify-files add-files)
- (gp-util/remove-nils))
- options {:delete-files (concat delete-files delete-pages)
- :delete-blocks delete-blocks
- :re-render? true}]
- (if (seq nfs-files)
- (parse-files-and-load-to-db! repo-url nfs-files
- (assoc options
- :refresh? refresh?
- :re-render-opts {:clear-all-query-state? true}))
- (load-contents add-or-modify-files options)))))))
- (defn remove-repo!
- [{:keys [url] :as repo}]
- (let [delete-db-f (fn []
- (let [graph-exists? (db/get-db url)]
- (db/remove-conn! url)
- (db-persist/delete-graph! url)
- (search/remove-db! url)
- (state/delete-repo! repo)
- (when graph-exists? (ipc/ipc "graphUnlinked" repo))
- (when (= (state/get-current-repo) url)
- (state/set-current-repo! (:url (first (state/get-repos)))))))]
- (when (or (config/local-db? url) (= url "local"))
- (p/let [_ (idb/clear-local-db! url)] ; clear file handles
- (delete-db-f)))))
- (defn start-repo-db-if-not-exists!
- [repo]
- (state/set-current-repo! repo)
- (db/start-db-conn! repo))
- (defn- setup-local-repo-if-not-exists-impl!
- []
- ;; loop query if js/window.pfs is ready, interval 100ms
- (if js/window.pfs
- (let [repo config/local-repo]
- (p/do! (fs/mkdir-if-not-exists (str "/" repo))
- (state/set-current-repo! repo)
- (db/start-db-conn! repo)
- (when-not config/publishing?
- (let [dummy-notes (t :tutorial/dummy-notes)]
- (create-dummy-notes-page repo dummy-notes)))
- (when-not config/publishing?
- (let [tutorial (t :tutorial/text)
- tutorial (string/replace-first tutorial "$today" (date/today))]
- (create-today-journal-if-not-exists repo {:content tutorial})))
- (create-config-file-if-not-exists repo)
- (create-contents-file repo)
- (create-custom-theme repo)
- (state/set-db-restoring! false)
- (ui-handler/re-render-root!)))
- (p/then (p/delay 100) ;; TODO Junyi remove the string
- setup-local-repo-if-not-exists-impl!)))
- (defn setup-local-repo-if-not-exists!
- []
- ;; ensure `(state/set-db-restoring! false)` at exit
- (-> (setup-local-repo-if-not-exists-impl!)
- (p/timeout 3000)
- (p/catch (fn []
- (state/set-db-restoring! false)
- (prn "setup-local-repo failed! timeout 3000ms")))))
- (defn restore-and-setup-repo!
- "Restore the db of a graph from the persisted data, and setup. Create a new
- conn, or replace the conn in state with a new one."
- [repo]
- (p/let [_ (state/set-db-restoring! true)
- _ (db/restore-graph! repo)]
- (file-handler/restore-config! repo)
- ;; Don't have to unlisten the old listerner, as it will be destroyed with the conn
- (db/listen-and-persist! repo)
- (ui-handler/add-style-if-exists!)
- (state/set-db-restoring! false)))
- (defn rebuild-index!
- [url]
- (when-not (state/unlinked-dir? (config/get-repo-dir url))
- (when url
- (search/reset-indice! url)
- (db/remove-conn! url)
- (db/clear-query-state!)
- (-> (p/do! (db-persist/delete-graph! url))
- (p/catch (fn [error]
- (prn "Delete repo failed, error: " error)))))))
- (defn re-index!
- [nfs-rebuild-index! ok-handler]
- (when-let [repo (state/get-current-repo)]
- (let [dir (config/get-repo-dir repo)]
- (when-not (state/unlinked-dir? dir)
- (route-handler/redirect-to-home!)
- (let [local? (config/local-db? repo)]
- (if local?
- (p/let [_ (metadata-handler/set-pages-metadata! repo)]
- (nfs-rebuild-index! repo ok-handler))
- (rebuild-index! repo))
- (js/setTimeout
- (route-handler/redirect-to-home!)
- 500))))))
- (defn persist-db!
- ([]
- (persist-db! {}))
- ([handlers]
- (persist-db! (state/get-current-repo) handlers))
- ([repo {:keys [before on-success on-error]}]
- (->
- (p/do!
- (when before
- (before))
- (metadata-handler/set-pages-metadata! repo)
- (db/persist! repo)
- (when on-success
- (on-success)))
- (p/catch (fn [error]
- (js/console.error error)
- (when on-error
- (on-error)))))))
- (defn broadcast-persist-db!
- "Only works for electron
- Call backend to handle persisting a specific db on other window
- Skip persisting if no other windows is open (controlled by electron)
- step 1. [In HERE] a window ---broadcastPersistGraph----> electron
- step 2. electron ---------persistGraph-------> window holds the graph
- step 3. window w/ graph --broadcastPersistGraphDone-> electron
- step 4. [In HERE] a window <---broadcastPersistGraph---- electron"
- [graph]
- (p/let [_ (ipc/ipc "broadcastPersistGraph" graph)] ;; invoke for chaining promise
- nil))
- (defn get-repos
- []
- (p/let [nfs-dbs (db-persist/get-all-graphs)
- nfs-dbs (map (fn [db]
- {:url db
- :root (config/get-local-dir db)
- :nfs? true}) nfs-dbs)
- nfs-dbs (and (seq nfs-dbs)
- (cond (util/electron?)
- (ipc/ipc :inflateGraphsInfo nfs-dbs)
- (mobile-util/native-platform?)
- (util-fs/inflate-graphs-info nfs-dbs)
- :else
- nil))
- nfs-dbs (seq (bean/->clj nfs-dbs))]
- (cond
- (seq nfs-dbs)
- nfs-dbs
- :else
- [{:url config/local-repo
- :example? true}])))
- (defn combine-local-&-remote-graphs
- [local-repos remote-repos]
- (when-let [repos' (seq (concat (map #(if-let [sync-meta (seq (:sync-meta %))]
- (assoc % :GraphUUID (second sync-meta)) %)
- local-repos)
- (some->> remote-repos
- (map #(assoc % :remote? true)))))]
- (let [repos' (group-by :GraphUUID repos')
- repos'' (mapcat (fn [[k vs]]
- (if-not (nil? k)
- [(merge (first vs) (second vs))] vs))
- repos')]
- (sort-by (fn [repo]
- (let [graph-name (or (:GraphName repo)
- (last (string/split (:root repo) #"/")))]
- [(:remote? repo) (string/lower-case graph-name)])) repos''))))
- (defn get-detail-graph-info
- [url]
- (when-let [graphs (seq (and url (combine-local-&-remote-graphs
- (state/get-repos)
- (state/get-remote-repos))))]
- (first (filter #(when-let [url' (:url %)]
- (= url url')) graphs))))
- (defn refresh-repos!
- []
- (p/let [repos (get-repos)]
- (state/set-repos! repos)
- repos))
- (defn graph-ready!
- "Call electron that the graph is loaded."
- [graph]
- (ipc/ipc "graphReady" graph))
|