file.cljs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. (ns frontend.handler.file
  2. (:refer-clojure :exclude [load-file])
  3. (:require ["/frontend/utils" :as utils]
  4. [borkdude.rewrite-edn :as rewrite]
  5. [cljs-bean.core :as bean]
  6. [cljs-time.coerce :as tc]
  7. [cljs-time.core :as t]
  8. [cljs.core.async.interop :refer [<p!]]
  9. [clojure.core.async :as async]
  10. [clojure.string :as string]
  11. [frontend.config :as config]
  12. [frontend.db :as db]
  13. [frontend.db.model :as model]
  14. [frontend.format :as format]
  15. [frontend.fs :as fs]
  16. [frontend.fs.nfs :as nfs]
  17. [frontend.git :as git]
  18. [frontend.handler.common :as common-handler]
  19. [frontend.handler.extract :as extract-handler]
  20. [frontend.handler.route :as route-handler]
  21. [frontend.handler.ui :as ui-handler]
  22. [frontend.state :as state]
  23. [frontend.utf8 :as utf8]
  24. [frontend.util :as util]
  25. [lambdaisland.glogi :as log]
  26. [promesa.core :as p]
  27. [frontend.debug :as debug]
  28. [frontend.mobile.util :as mobile]))
  29. ;; TODO: extract all git ops using a channel
  30. (defn load-file
  31. [repo-url path]
  32. (->
  33. (p/let [content (fs/read-file (config/get-repo-dir repo-url) path)]
  34. content)
  35. (p/catch
  36. (fn [e]
  37. (println "Load file failed: " path)
  38. (js/console.error e)))))
  39. (defn load-multiple-files
  40. [repo-url paths]
  41. (doall
  42. (mapv #(load-file repo-url %) paths)))
  43. (defn- keep-formats
  44. [files formats]
  45. (filter
  46. (fn [file]
  47. (let [format (format/get-format file)]
  48. (contains? formats format)))
  49. files))
  50. (defn- only-supported-formats
  51. [files]
  52. (keep-formats files (config/supported-formats)))
  53. (defn- only-text-formats
  54. [files]
  55. (keep-formats files (config/text-formats)))
  56. (defn- only-image-formats
  57. [files]
  58. (keep-formats files (config/img-formats)))
  59. (defn restore-config!
  60. ([repo-url project-changed-check?]
  61. (restore-config! repo-url nil project-changed-check?))
  62. ([repo-url config-content project-changed-check?]
  63. (let [config-content (if config-content config-content
  64. (common-handler/get-config repo-url))]
  65. (when config-content
  66. (common-handler/reset-config! repo-url config-content)))))
  67. (defn load-files
  68. [repo-url]
  69. (state/set-cloning! false)
  70. (state/set-loading-files! true)
  71. (p/let [files (git/list-files repo-url)
  72. files (bean/->clj files)
  73. config-content (load-file repo-url
  74. (config/get-config-path repo-url))
  75. files (if config-content
  76. (let [config (restore-config! repo-url config-content true)]
  77. (common-handler/remove-hidden-files files config identity))
  78. files)]
  79. (only-supported-formats files)))
  80. (defn load-files-contents!
  81. [repo-url files ok-handler]
  82. (let [images (only-image-formats files)
  83. files (only-text-formats files)]
  84. (-> (p/all (load-multiple-files repo-url files))
  85. (p/then (fn [contents]
  86. (let [file-contents (cond->
  87. (zipmap files contents)
  88. (seq images)
  89. (merge (zipmap images (repeat (count images) ""))))
  90. file-contents (for [[file content] file-contents]
  91. {:file/path file
  92. :file/content content})]
  93. (ok-handler file-contents))))
  94. (p/catch (fn [error]
  95. (log/error :nfs/load-files-error repo-url)
  96. (log/error :exception error))))))
  97. (defn- remove-non-exists-refs!
  98. [data]
  99. (let [block-ids (->> (map :block/uuid data)
  100. (remove nil?)
  101. (set))
  102. keep-block-ref-f (fn [refs]
  103. (filter (fn [ref]
  104. (cond
  105. (and (vector? ref) (= :block/uuid (first ref)))
  106. (let [id (second ref)]
  107. (or (contains? block-ids id)
  108. (db/entity [:block/uuid id])))
  109. (and (map? ref) (contains? ref :block/journal?))
  110. (db/entity [:block/name (ref :block/name)]))) refs))]
  111. (map (fn [item]
  112. (update item :block/refs keep-block-ref-f))
  113. data)))
  114. (defn- page-exists-in-another-file
  115. [page file]
  116. (when-let [page-name (:block/name page)]
  117. (let [current-file (:file/path (db/get-page-file page-name))]
  118. (when (not= file current-file)
  119. current-file))))
  120. (defn reset-file!
  121. [repo-url file content]
  122. (let [electron-local-repo? (and (util/electron?)
  123. (config/local-db? repo-url))
  124. file (cond
  125. (and electron-local-repo?
  126. util/win32?
  127. (utils/win32 file))
  128. file
  129. (and electron-local-repo? (or
  130. util/win32?
  131. (not= "/" (first file))))
  132. (str (config/get-repo-dir repo-url) "/" file)
  133. (and (mobile/is-native-platform?) (not= "/" (first file)))
  134. (str (config/get-repo-dir repo-url) "/" file)
  135. :else
  136. file)
  137. new? (nil? (db/entity [:file/path file]))]
  138. (db/set-file-content! repo-url file content)
  139. (let [format (format/get-format file)
  140. utf8-content (utf8/encode content)
  141. file-content [{:file/path file}]]
  142. (p/let [tx (if (contains? config/mldoc-support-formats format)
  143. (p/let [delete-blocks (db/delete-file-blocks! repo-url file)
  144. [pages blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)
  145. _ (when-let [current-file (page-exists-in-another-file (first pages) file)]
  146. (when (not= file current-file)
  147. (let [error (str "Page already exists with another file: " current-file ", current file: " file)]
  148. (state/pub-event! [:notification/show
  149. {:content error
  150. :status :error
  151. :clear? false}]))))
  152. blocks (remove-non-exists-refs! blocks)
  153. block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks)
  154. pages (extract-handler/with-ref-pages pages blocks)]
  155. (concat file-content delete-blocks pages block-ids blocks))
  156. file-content)]
  157. (let [tx (concat tx [(let [t (tc/to-long (t/now))]
  158. (cond->
  159. {:file/path file}
  160. new?
  161. (assoc :file/created-at t)))])]
  162. (db/transact! repo-url tx))))))
  163. ;; TODO: Remove this function in favor of `alter-files`
  164. (defn alter-file
  165. [repo path content {:keys [reset? re-render-root? add-history? update-status? from-disk? skip-compare?]
  166. :or {reset? true
  167. re-render-root? false
  168. add-history? true
  169. update-status? false
  170. from-disk? false
  171. skip-compare? false}}]
  172. (let [edit-block (state/get-edit-block)
  173. original-content (db/get-file-no-sub repo path)
  174. write-file! (if from-disk?
  175. #(p/resolved nil)
  176. #(fs/write-file! repo (config/get-repo-dir repo) path content
  177. (assoc (when original-content {:old-content original-content})
  178. :skip-compare? skip-compare?)))]
  179. (p/let [_ (if reset?
  180. (do
  181. (when-let [page-id (db/get-file-page-id path)]
  182. (db/transact! repo
  183. [[:db/retract page-id :block/alias]
  184. [:db/retract page-id :block/tags]]))
  185. (reset-file! repo path content))
  186. (db/set-file-content! repo path content))]
  187. (util/p-handle (write-file!)
  188. (fn [_]
  189. (when (= path (config/get-config-path repo))
  190. (restore-config! repo true))
  191. (when (= path (config/get-custom-css-path repo))
  192. (ui-handler/add-style-if-exists!))
  193. (when re-render-root? (ui-handler/re-render-root!)))
  194. (fn [error]
  195. (println "Write file failed, path: " path ", content: " content)
  196. (log/error :write/failed error))))))
  197. (defn set-file-content!
  198. [repo path new-content]
  199. (alter-file repo path new-content {:reset? false
  200. :re-render-root? false}))
  201. (defn create!
  202. ([path]
  203. (create! path ""))
  204. ([path content]
  205. (when-let [repo (state/get-current-repo)]
  206. (when (and path content)
  207. (p/let [_ (alter-file repo path content {:reset? false
  208. :re-render-root? false
  209. :update-status? true})]
  210. (route-handler/redirect! {:to :file
  211. :path-params {:path path}}))))))
  212. (defn alter-files
  213. [repo files {:keys [add-history? update-status? finish-handler reset? update-db? chan chan-callback resolved-handler]
  214. :or {add-history? true
  215. update-status? true
  216. reset? false
  217. update-db? true}
  218. :as opts}]
  219. ;; old file content
  220. (let [file->content (let [paths (map first files)]
  221. (zipmap paths
  222. (map (fn [path] (db/get-file-no-sub repo path)) paths)))]
  223. ;; update db
  224. (when update-db?
  225. (doseq [[path content] files]
  226. (if reset?
  227. (reset-file! repo path content)
  228. (db/set-file-content! repo path content))))
  229. (when-let [chan (state/get-file-write-chan)]
  230. (let [chan-callback (:chan-callback opts)]
  231. (async/put! chan [repo files opts file->content])
  232. (when chan-callback
  233. (chan-callback))))))
  234. (defn alter-files-handler!
  235. [repo files {:keys [finish-handler chan]} file->content]
  236. (let [write-file-f (fn [[path content]]
  237. (when path
  238. (let [original-content (get file->content path)]
  239. (-> (p/let [_ (or
  240. (util/electron?)
  241. (nfs/check-directory-permission! repo))]
  242. (debug/set-ack-step! path :write-file)
  243. (fs/write-file! repo (config/get-repo-dir repo) path content
  244. {:old-content original-content}))
  245. (p/catch (fn [error]
  246. (state/pub-event! [:notification/show
  247. {:content (str "Failed to save the file " path ". Error: "
  248. (str error))
  249. :status :error
  250. :clear? false}])
  251. (state/pub-event! [:instrument {:type :write-file/failed
  252. :payload {:path path
  253. :content-length (count content)
  254. :error-str (str error)
  255. :error error}}])
  256. (log/error :write-file/failed {:path path
  257. :content content
  258. :error error})))))))
  259. finish-handler (fn []
  260. (when finish-handler
  261. (finish-handler))
  262. (ui-handler/re-render-file!))]
  263. (-> (p/all (map write-file-f files))
  264. (p/then (fn []
  265. (finish-handler)
  266. (when chan
  267. (async/put! chan true))))
  268. (p/catch (fn [error]
  269. (println "Alter files failed:")
  270. (js/console.error error)
  271. (async/put! chan false))))))
  272. (defn remove-file!
  273. [repo file]
  274. (when-not (string/blank? file)
  275. (->
  276. (p/let [_ (or (config/local-db? repo) (git/remove-file repo file))
  277. _ (fs/unlink! repo (config/get-repo-path repo file) nil)]
  278. (when-let [file (db/entity repo [:file/path file])]
  279. (common-handler/check-changed-files-status)
  280. (let [file-id (:db/id file)
  281. page-id (db/get-file-page-id (:file/path file))
  282. tx-data (map
  283. (fn [db-id]
  284. [:db.fn/retractEntity db-id])
  285. (remove nil? [file-id page-id]))]
  286. (when (seq tx-data)
  287. (db/transact! repo tx-data)))))
  288. (p/catch (fn [err]
  289. (js/console.error "error: " err))))))
  290. (defn run-writes-chan!
  291. []
  292. (let [chan (state/get-file-write-chan)]
  293. (async/go-loop []
  294. (let [args (async/<! chan)
  295. files (second args)]
  296. (doseq [path (map first files)]
  297. (debug/set-ack-step! path :start-write-file))
  298. ;; return a channel
  299. (try
  300. (<p! (apply alter-files-handler! args))
  301. (catch js/Error e
  302. (log/error :file/write-failed e))))
  303. (recur))
  304. chan))
  305. (defn watch-for-local-dirs!
  306. []
  307. (when (util/electron?)
  308. (let [repos (->> (state/get-repos)
  309. (filter (fn [repo]
  310. (config/local-db? (:url repo)))))
  311. directories (map (fn [repo] (config/get-repo-dir (:url repo))) repos)]
  312. (doseq [dir directories]
  313. (fs/watch-dir! dir)))))
  314. (defn create-metadata-file
  315. [repo-url encrypted?]
  316. (let [repo-dir (config/get-repo-dir repo-url)
  317. path (str config/app-name "/" config/metadata-file)
  318. file-path (str "/" path)
  319. default-content (if encrypted? "{:db/encrypted? true}" "{}")]
  320. (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
  321. file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
  322. (when-not file-exists?
  323. (reset-file! repo-url path default-content)))))
  324. (defn create-pages-metadata-file
  325. [repo-url]
  326. (let [repo-dir (config/get-repo-dir repo-url)
  327. path (str config/app-name "/" config/pages-metadata-file)
  328. file-path (str "/" path)
  329. default-content "{}"]
  330. (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
  331. file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
  332. (when-not file-exists?
  333. (reset-file! repo-url path default-content)))))
  334. (defn edn-file-set-key-value
  335. [path k v]
  336. (when-let [repo (state/get-current-repo)]
  337. (when-let [content (db/get-file-no-sub path)]
  338. (common-handler/read-config content)
  339. (let [result (try
  340. (rewrite/parse-string content)
  341. (catch js/Error e
  342. (println "Parsing config file failed: ")
  343. (js/console.dir e)
  344. {}))
  345. ks (if (vector? k) k [k])
  346. new-result (rewrite/assoc-in result ks v)]
  347. (let [new-content (str new-result)]
  348. (set-file-content! repo path new-content))))))
  349. ;; TODO:
  350. ;; (defn compare-latest-pages
  351. ;; []
  352. ;; (when-let [repo (state/get-current-repo)]
  353. ;; (doseq [{:block/keys [file name]} (db/get-latest-changed-pages repo)]
  354. ;; (when-let [path (:file/path (db/pull (:db/id file)))]
  355. ;; (p/let [content (load-file repo path)]
  356. ;; (when (not= (string/trim content) (string/trim (or (db/get-file repo path) "")))
  357. ;; ;; notify
  358. ;; ))))))