file.cljs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. (ns frontend.handler.file
  2. (:refer-clojure :exclude [load-file])
  3. (:require [frontend.util :as util :refer-macros [profile]]
  4. [frontend.fs :as fs]
  5. [frontend.fs.nfs :as nfs]
  6. [promesa.core :as p]
  7. [frontend.state :as state]
  8. [frontend.db :as db]
  9. [frontend.git :as git]
  10. [frontend.handler.common :as common-handler]
  11. [frontend.handler.git :as git-handler]
  12. [frontend.handler.extract :as extract-handler]
  13. [frontend.handler.ui :as ui-handler]
  14. [frontend.handler.route :as route-handler]
  15. [cljs-bean.core :as bean]
  16. [frontend.config :as config]
  17. [frontend.format :as format]
  18. [clojure.string :as string]
  19. [frontend.history :as history]
  20. [frontend.handler.project :as project-handler]
  21. [lambdaisland.glogi :as log]
  22. [clojure.core.async :as async]
  23. [cljs.core.async.interop :refer-macros [<p!]]
  24. [goog.object :as gobj]
  25. [cljs-time.core :as t]
  26. [cljs-time.coerce :as tc]
  27. [frontend.utf8 :as utf8]
  28. ["/frontend/utils" :as utils]))
  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- hidden?
  60. [path patterns]
  61. (let [path (if (and (string? path)
  62. (= \/ (first path)))
  63. (subs path 1)
  64. path)]
  65. (some (fn [pattern]
  66. (let [pattern (if (and (string? pattern)
  67. (not= \/ (first pattern)))
  68. (str "/" pattern)
  69. pattern)]
  70. (string/starts-with? (str "/" path) pattern))) patterns)))
  71. (defn restore-config!
  72. ([repo-url project-changed-check?]
  73. (restore-config! repo-url nil project-changed-check?))
  74. ([repo-url config-content project-changed-check?]
  75. (let [config-content (if config-content config-content
  76. (common-handler/get-config repo-url))]
  77. (when config-content
  78. (let [old-project (:project (state/get-config))
  79. new-config (common-handler/reset-config! repo-url config-content)]
  80. (when (and (not (config/local-db? repo-url))
  81. project-changed-check?)
  82. (let [new-project (:project new-config)
  83. project-name (:name old-project)]
  84. (when-not (= new-project old-project)
  85. (project-handler/sync-project-settings! project-name new-project)))))))))
  86. (defn load-files
  87. [repo-url]
  88. (state/set-cloning! false)
  89. (state/set-loading-files! true)
  90. (p/let [files (git/list-files repo-url)
  91. files (bean/->clj files)
  92. config-content (load-file repo-url
  93. (config/get-config-path repo-url))
  94. files (if config-content
  95. (let [config (restore-config! repo-url config-content true)]
  96. (if-let [patterns (seq (:hidden config))]
  97. (remove (fn [path] (hidden? path patterns)) files)
  98. files))
  99. files)]
  100. (only-supported-formats files)))
  101. (defn load-files-contents!
  102. [repo-url files ok-handler]
  103. (let [images (only-image-formats files)
  104. files (only-text-formats files)]
  105. (-> (p/all (load-multiple-files repo-url files))
  106. (p/then (fn [contents]
  107. (let [file-contents (cond->
  108. (zipmap files contents)
  109. (seq images)
  110. (merge (zipmap images (repeat (count images) ""))))
  111. file-contents (for [[file content] file-contents]
  112. {:file/path file
  113. :file/content content})]
  114. (ok-handler file-contents))))
  115. (p/catch (fn [error]
  116. (log/error :load-files-error error))))))
  117. (defn reset-file!
  118. [repo-url file content]
  119. (let [electron-local-repo? (and (util/electron?)
  120. (config/local-db? repo-url))
  121. ;; FIXME: store relative path in db
  122. file (cond
  123. (and electron-local-repo?
  124. util/win32?
  125. (utils/win32 file))
  126. file
  127. (and electron-local-repo? (or
  128. util/win32?
  129. (not= "/" (first file))))
  130. (str (config/get-repo-dir repo-url) "/" file)
  131. :else
  132. file)
  133. new? (nil? (db/entity [:file/path file]))]
  134. (db/set-file-content! repo-url file content)
  135. (let [format (format/get-format file)
  136. utf8-content (utf8/encode content)
  137. file-content [{:file/path file}]
  138. tx (if (contains? config/mldoc-support-formats format)
  139. (let [delete-blocks (db/delete-file-blocks! repo-url file)
  140. [pages block-ids blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)]
  141. (concat file-content delete-blocks pages block-ids blocks))
  142. file-content)
  143. tx (concat tx [(let [t (tc/to-long (t/now))]
  144. (cond->
  145. {:file/path file}
  146. new?
  147. (assoc :file/created-at t)))])]
  148. (db/transact! repo-url tx))))
  149. ;; TODO: Remove this function in favor of `alter-files`
  150. (defn alter-file
  151. [repo path content {:keys [reset? re-render-root? add-history? update-status?]
  152. :or {reset? true
  153. re-render-root? false
  154. add-history? true
  155. update-status? false}}]
  156. (let [edit-block (state/get-edit-block)
  157. original-content (db/get-file-no-sub repo path)]
  158. (if reset?
  159. (do
  160. (when-let [page-id (db/get-file-page-id path)]
  161. (db/transact! repo
  162. [[:db/retract page-id :page/alias]
  163. [:db/retract page-id :page/tags]]))
  164. (reset-file! repo path content))
  165. (db/set-file-content! repo path content))
  166. (util/p-handle
  167. (fs/write-file! repo (config/get-repo-dir repo) path content {:old-content original-content})
  168. (fn [_]
  169. (git-handler/git-add repo path update-status?)
  170. (when (= path (config/get-config-path repo))
  171. (restore-config! repo true))
  172. (when (= path (config/get-custom-css-path repo))
  173. (ui-handler/add-style-if-exists!))
  174. (when re-render-root? (ui-handler/re-render-root!))
  175. (when add-history?
  176. (history/add-history! repo [[path original-content content]])))
  177. (fn [error]
  178. (println "Write file failed, path: " path ", content: " content)
  179. (log/error :write/failed error)))))
  180. (defn create!
  181. ([path]
  182. (create! path ""))
  183. ([path content]
  184. (when-let [repo (state/get-current-repo)]
  185. (when (and path content)
  186. (p/let [_ (alter-file repo path content {:reset? false
  187. :re-render-root? false
  188. :update-status? true})]
  189. (route-handler/redirect! {:to :file
  190. :path-params {:path path}}))))))
  191. (defn alter-files
  192. [repo files {:keys [add-history? update-status? git-add-cb reset? update-db? chan chan-callback resolved-handler]
  193. :or {add-history? true
  194. update-status? true
  195. reset? false
  196. update-db? true}
  197. :as opts}]
  198. ;; old file content
  199. (let [file->content (let [paths (map first files)]
  200. (zipmap paths
  201. (map (fn [path] (db/get-file-no-sub repo path)) paths)))]
  202. ;; update db
  203. (when update-db?
  204. (doseq [[path content] files]
  205. (if reset?
  206. (reset-file! repo path content)
  207. (db/set-file-content! repo path content))))
  208. (when-let [chan (state/get-file-write-chan)]
  209. (let [chan-callback (:chan-callback opts)]
  210. (async/put! chan [repo files opts file->content])
  211. (when chan-callback
  212. (chan-callback))))))
  213. (defn alter-files-handler!
  214. [repo files {:keys [add-history? update-status? git-add-cb reset? chan]
  215. :or {add-history? true
  216. update-status? true
  217. reset? false}} file->content]
  218. (let [write-file-f (fn [[path content]]
  219. (let [original-content (get file->content path)]
  220. (-> (p/let [_ (nfs/check-directory-permission! repo)]
  221. (fs/write-file! repo (config/get-repo-dir repo) path content
  222. {:old-content original-content}))
  223. (p/catch (fn [error]
  224. (log/error :write-file/failed {:path path
  225. :content content
  226. :error error}))))))
  227. git-add-f (fn []
  228. (let [add-helper
  229. (fn []
  230. (map
  231. (fn [[path content]]
  232. (git-handler/git-add repo path update-status?))
  233. files))]
  234. (-> (p/all (add-helper))
  235. (p/then (fn [_]
  236. (when git-add-cb
  237. (git-add-cb))))
  238. (p/catch (fn [error]
  239. (println "Git add failed:")
  240. (js/console.error error)))))
  241. (ui-handler/re-render-file!)
  242. (when add-history?
  243. (let [files-tx (mapv (fn [[path content]]
  244. (let [original-content (get file->content path)]
  245. [path original-content content])) files)]
  246. (history/add-history! repo files-tx))))]
  247. (-> (p/all (map write-file-f files))
  248. (p/then (fn []
  249. (git-add-f)
  250. (when chan
  251. (async/put! chan true))))
  252. (p/catch (fn [error]
  253. (println "Alter files failed:")
  254. (js/console.error error)
  255. (async/put! chan false))))))
  256. (defn remove-file!
  257. [repo file]
  258. (when-not (string/blank? file)
  259. (->
  260. (p/let [_ (or (config/local-db? repo) (git/remove-file repo file))
  261. result (fs/unlink! (config/get-repo-path repo file) nil)]
  262. (when-let [file (db/entity repo [:file/path file])]
  263. (common-handler/check-changed-files-status)
  264. (let [file-id (:db/id file)
  265. page-id (db/get-file-page-id (:file/path file))
  266. tx-data (map
  267. (fn [db-id]
  268. [:db.fn/retractEntity db-id])
  269. (remove nil? [file-id page-id]))]
  270. (when (seq tx-data)
  271. (db/transact! repo tx-data)))))
  272. (p/catch (fn [err]
  273. (js/console.error "error: " err))))))
  274. (defn re-index!
  275. [file]
  276. (when-let [repo (state/get-current-repo)]
  277. (let [path (:file/path file)
  278. content (db/get-file path)]
  279. (alter-file repo path content {:re-render-root? true}))))
  280. ;; TODO: batch writes, how to deal with file history?
  281. (defn run-writes-chan!
  282. []
  283. (let [chan (state/get-file-write-chan)]
  284. (async/go-loop []
  285. (let [args (async/<! chan)]
  286. ;; return a channel
  287. (<p! (apply alter-files-handler! args)))
  288. (recur))
  289. chan))
  290. (defn watch-for-local-dirs!
  291. []
  292. (when (util/electron?)
  293. (let [repos (->> (state/get-repos)
  294. (filter (fn [repo]
  295. (config/local-db? (:url repo)))))
  296. directories (map (fn [repo] (config/get-repo-dir (:url repo))) repos)]
  297. (doseq [dir directories]
  298. (fs/watch-dir! dir)))))