export.cljs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. (ns ^:no-doc frontend.handler.export
  2. (:require ["/frontend/utils" :as utils]
  3. [clojure.string :as string]
  4. [frontend.config :as config]
  5. [frontend.db :as db]
  6. [frontend.extensions.zip :as zip]
  7. [frontend.handler.assets :as assets-handler]
  8. [frontend.handler.export.common :as export-common-handler]
  9. [frontend.handler.notification :as notification]
  10. [frontend.idb :as idb]
  11. [frontend.persist-db :as persist-db]
  12. [frontend.state :as state]
  13. [frontend.util :as util]
  14. [goog.dom :as gdom]
  15. [logseq.db :as ldb]
  16. [logseq.db.common.sqlite :as common-sqlite]
  17. [logseq.publishing.html :as publish-html]
  18. [promesa.core :as p]))
  19. (defn download-repo-as-html!
  20. "download public pages as html"
  21. [repo]
  22. (when-let [db (db/get-db repo)]
  23. (let [{:keys [asset-filenames html]}
  24. (publish-html/build-html db
  25. {:repo repo
  26. :app-state (select-keys @state/state
  27. [:ui/theme
  28. :ui/sidebar-collapsed-blocks])
  29. :repo-config (get-in @state/state [:config repo])
  30. :db-graph? true})
  31. html-str (str "data:text/html;charset=UTF-8,"
  32. (js/encodeURIComponent html))]
  33. (if (util/electron?)
  34. (js/window.apis.exportPublishAssets
  35. html
  36. (config/get-repo-dir repo)
  37. (clj->js asset-filenames)
  38. (util/mocked-open-dir-path))
  39. (when-let [anchor (gdom/getElement "download-as-html")]
  40. (.setAttribute anchor "href" html-str)
  41. (.setAttribute anchor "download" "index.html")
  42. (.click anchor))))))
  43. (defn db-based-export-repo-as-zip!
  44. [repo]
  45. (p/let [db-data (persist-db/<export-db repo {:return-data? true})
  46. filename "db.sqlite"
  47. repo-name (common-sqlite/sanitize-db-name repo)
  48. assets (assets-handler/<get-all-assets)
  49. files (cons [filename db-data] assets)
  50. zipfile (zip/make-zip repo-name files repo)]
  51. (when-let [anchor (gdom/getElement "download-as-zip")]
  52. (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
  53. (.setAttribute anchor "download" (.-name zipfile))
  54. (.click anchor))))
  55. (defn export-repo-as-zip!
  56. [repo]
  57. (db-based-export-repo-as-zip! repo))
  58. (defn- file-name [repo extension]
  59. (-> repo
  60. (string/replace #"^/+" "")
  61. (str "_" (quot (util/time-ms) 1000))
  62. (str "." (string/lower-case (name extension)))))
  63. (defn export-repo-as-debug-transit!
  64. [repo]
  65. (p/let [result (export-common-handler/<get-debug-datoms repo)
  66. filename (file-name (str repo "-debug-datoms") :transit)
  67. data-str (str "data:text/transit;charset=utf-8,"
  68. (js/encodeURIComponent (ldb/write-transit-str result)))]
  69. (when-let [anchor (gdom/getElement "download-as-transit-debug")]
  70. (.setAttribute anchor "href" data-str)
  71. (.setAttribute anchor "download" filename)
  72. (.click anchor))))
  73. (defn export-repo-as-sqlite-db!
  74. [repo]
  75. (->
  76. (p/let [data (persist-db/<export-db repo {:return-data? true})
  77. filename (file-name repo "sqlite")
  78. url (js/URL.createObjectURL (js/Blob. #js [data]))]
  79. (when-let [anchor (gdom/getElement "download-as-sqlite-db")]
  80. (.setAttribute anchor "href" url)
  81. (.setAttribute anchor "download" filename)
  82. (.click anchor)))
  83. (p/catch (fn [error]
  84. (js/console.error error)))))
  85. ;;;;;;;;;;;;;;;;;;;;;;;;;
  86. ;; Export to roam json ;;
  87. ;;;;;;;;;;;;;;;;;;;;;;;;;
  88. ;; https://roamresearch.com/#/app/help/page/Nxz8u0vXU
  89. ;; export to roam json according to above spec
  90. (comment
  91. (defn- <roam-data [repo]
  92. (p/let [pages (export-common-handler/<get-all-pages repo)]
  93. (let [non-empty-pages (remove #(empty? (:block/children %)) pages)]
  94. (roam-export/traverse
  95. [:page/title
  96. :block/string
  97. :block/uid
  98. :block/children]
  99. non-empty-pages))))
  100. (defn export-repo-as-roam-json!
  101. [repo]
  102. (p/let [data (<roam-data repo)
  103. json-str (-> data
  104. bean/->js
  105. js/JSON.stringify)
  106. data-str (str "data:text/json;charset=utf-8,"
  107. (js/encodeURIComponent json-str))]
  108. (when-let [anchor (gdom/getElement "download-as-roam-json")]
  109. (.setAttribute anchor "href" data-str)
  110. (.setAttribute anchor "download" (file-name (str repo "_roam") :json))
  111. (.click anchor)))))
  112. (defn- truncate-old-versioned-files!
  113. "reserve the latest 12 version files"
  114. [^js backups-handle]
  115. (p/let [files (utils/getFiles backups-handle true)
  116. old-versioned-files (drop 12 (reverse (sort-by (fn [^js file] (.-name file)) files)))]
  117. (p/map (fn [^js files]
  118. (doseq [^js file files]
  119. (.remove (.-handle file))))
  120. old-versioned-files)))
  121. (defn choose-backup-folder
  122. [repo]
  123. (p/let [result (utils/openDirectory #js {:mode "readwrite"})
  124. handle (first result)
  125. folder-name (.-name handle)]
  126. (js/console.dir handle)
  127. (idb/set-item!
  128. (str "handle/" (js/btoa repo) "/" folder-name) handle)
  129. (db/transact! [(ldb/kv :logseq.kv/graph-backup-folder folder-name)])
  130. [folder-name handle]))
  131. (defn- web-backup-db-graph
  132. [repo]
  133. (when (and repo (= repo (state/get-current-repo)))
  134. (when-let [backup-folder (ldb/get-key-value (db/get-db repo) :logseq.kv/graph-backup-folder)]
  135. ;; ensure file handle exists
  136. ;; ask user to choose a folder again when access expires
  137. (p/let [handle (try
  138. (idb/get-item (str "handle/" (js/btoa repo) "/" backup-folder))
  139. (catch :default _e
  140. (throw (ex-info "Backup file handle no longer exists" {:repo repo}))))
  141. [_folder handle] (when handle
  142. (try
  143. (utils/verifyPermission handle true)
  144. [backup-folder handle]
  145. (catch :default e
  146. (js/console.error e)
  147. (choose-backup-folder repo))))
  148. repo-name (common-sqlite/sanitize-db-name repo)]
  149. (if handle
  150. (->
  151. (p/let [graph-dir-handle (.getDirectoryHandle handle repo-name #js {:create true})
  152. backups-handle (.getDirectoryHandle graph-dir-handle "backups" #js {:create true})
  153. backup-handle ^js (.getFileHandle graph-dir-handle "db.sqlite" #js {:create true})
  154. file ^js (.getFile backup-handle)
  155. file-content (.text file)
  156. data (persist-db/<export-db repo {:return-data? true})
  157. decoded-content (.decode (js/TextDecoder.) data)]
  158. (if (= file-content decoded-content)
  159. (do
  160. (println "Graph has not been updated since last export.")
  161. :graph-not-changed)
  162. (p/do!
  163. (when (> (.-size file) 0)
  164. (.move backup-handle backups-handle (str (util/time-ms) ".db.sqlite")))
  165. (truncate-old-versioned-files! backups-handle)
  166. (p/let [new-backup-handle ^js (.getFileHandle graph-dir-handle "db.sqlite" #js {:create true})]
  167. (utils/writeFile new-backup-handle data))
  168. (println "Successfully created a backup for" repo-name "at" (str (js/Date.)) ".")
  169. true)))
  170. (p/catch (fn [error]
  171. (js/console.error error))))
  172. (p/do!
  173. ;; handle cleared
  174. (notification/show! "DB backup failed, please go to Export and specify a backup folder." :error)
  175. false))))))
  176. (defn backup-db-graph
  177. [repo]
  178. (when-not (util/capacitor?)
  179. (web-backup-db-graph repo)))
  180. (defonce *backup-interval (atom nil))
  181. (defn cancel-db-backup!
  182. []
  183. (when-let [i @*backup-interval]
  184. (js/clearInterval i)))
  185. (defn auto-db-backup!
  186. [repo]
  187. (when (and
  188. util/web-platform?
  189. (not (util/capacitor?))
  190. (ldb/get-key-value (db/get-db repo) :logseq.kv/graph-backup-folder))
  191. (cancel-db-backup!)
  192. ;; run backup every hour
  193. (let [interval (js/setInterval #(backup-db-graph repo)
  194. (* 1 60 60 1000))]
  195. (reset! *backup-interval interval))))