org-persist.el 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. ;;; org-persist.el --- Persist data across Emacs sessions -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2021-2022 Free Software Foundation, Inc.
  3. ;; Author: Ihor Radchenko <yantar92 at gmail dot com>
  4. ;; Keywords: cache, storage
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;
  18. ;; This file implements persistant data storage across Emacs sessions.
  19. ;; Both global and buffer-local data can be stored.
  20. ;;; Code:
  21. (require 'org-compat)
  22. (require 'org-id)
  23. (require 'xdg nil t)
  24. (declare-function org-back-to-heading "org" (&optional invisible-ok))
  25. (declare-function org-next-visible-heading "org" (arg))
  26. (declare-function org-at-heading-p "org" (&optional invisible-not-ok))
  27. (defgroup org-persist nil
  28. "Persistent cache for Org mode."
  29. :tag "Org persist"
  30. :group 'org)
  31. (defcustom org-persist-directory (expand-file-name
  32. (org-file-name-concat
  33. (let ((cache-dir (when (fboundp 'xdg-cache-home)
  34. (xdg-cache-home))))
  35. (if (or (seq-empty-p cache-dir)
  36. (not (file-exists-p cache-dir))
  37. (file-exists-p (org-file-name-concat
  38. user-emacs-directory
  39. "org-persist")))
  40. user-emacs-directory
  41. cache-dir))
  42. "org-persist/"))
  43. "Directory where the data is stored."
  44. :group 'org-persist
  45. :type 'directory)
  46. (defvar org-persist-index-file "index"
  47. "File name used to store the data index.")
  48. (defvar org-persist-before-write-hook nil
  49. "Abnormal hook ran before saving data for a single variable in a buffer.
  50. The hook must accept the same arguments as `org-persist-write'.
  51. The hooks will be evaluated until a hook returns non-nil.
  52. If any of the hooks return non-nil, do not save the data.")
  53. (defvar org-persist-before-read-hook nil
  54. "Abnormal hook ran before reading data for a single variable in a buffer.
  55. The hook must accept the same arguments as `org-persist-read'.
  56. The hooks will be evaluated until a hook returns non-nil.
  57. If any of the hooks return non-nil, do not read the data.")
  58. (defvar org-persist-after-read-hook nil
  59. "Abnormal hook ran after reading data for a single variable in a buffer.
  60. The hook must accept the same arguments as `org-persist-read'.")
  61. (defvar org-persist--index nil
  62. "Global index.
  63. The index is a list of plists. Each plist contains information about
  64. a data variable. Each plist contains the following properties:
  65. - `:variable' list of variables to be stored in single file
  66. - `:persist-file': data file name
  67. - `:path': buffer file path, if any
  68. - `:inode': buffer file inode, if any
  69. - `:hash': buffer hash, if any")
  70. (defvar org-persist--report-time 0.5
  71. "Whether to report read/write time.
  72. When the value is a number, it is a threshold number of seconds. If
  73. the read/write time of a single variable exceeds the threashold, a
  74. message is displayed.
  75. When the value is a non-nil non-number, always display the message.
  76. When the value is nil, never diplay the message.")
  77. (defun org-persist--get-index (var &optional buffer)
  78. "Return plist used to store VAR in BUFFER.
  79. When BUFFER is nil, return plist for global VAR."
  80. (org-persist--read-index)
  81. (let* ((buffer-file (when buffer (buffer-file-name (or (buffer-base-buffer buffer)
  82. buffer))))
  83. (inode (when buffer-file
  84. (and (fboundp 'file-attribute-inode-number)
  85. (file-attribute-inode-number (file-attributes buffer-file)))))
  86. (buffer-hash (when buffer (secure-hash 'md5 buffer))))
  87. (let ((result (seq-find (lambda (plist)
  88. (and (or (memq var (plist-get plist :variable))
  89. (eq var (plist-get plist :variable)))
  90. (or (and inode (equal inode (plist-get plist :inode)))
  91. (and buffer-file (equal buffer-file (plist-get plist :path)))
  92. (and buffer-hash (equal buffer-hash (plist-get plist :hash))))))
  93. org-persist--index)))
  94. (when result
  95. (unless (equal buffer-file (plist-get result :path))
  96. (setf result (plist-put result :path buffer-file))))
  97. (unless result
  98. (push (list :variable (if (listp var) var (list var))
  99. :persist-file (replace-regexp-in-string "^.." "\\&/" (org-id-uuid))
  100. :path buffer-file
  101. :inode inode
  102. :hash buffer-hash)
  103. org-persist--index)
  104. (setf result (car org-persist--index)))
  105. result)))
  106. (defun org-persist--read-index ()
  107. "Read `org-persist--index'"
  108. (unless org-persist--index
  109. (when (file-exists-p (org-file-name-concat org-persist-directory org-persist-index-file))
  110. (with-temp-buffer
  111. (insert-file-contents (org-file-name-concat org-persist-directory org-persist-index-file))
  112. (setq org-persist--index
  113. (condition-case err
  114. (read (current-buffer))
  115. ;; Recover gracefully if index file is corrupted.
  116. (error
  117. (warn "Emacs reader failed to read data for `org-persist--index' from %S. The error was: %S"
  118. (org-file-name-concat org-persist-directory org-persist-index-file)
  119. (error-message-string err))
  120. nil)))))))
  121. (cl-defun org-persist-register (var &optional buffer &key inherit)
  122. "Register VAR in BUFFER to be persistent.
  123. Optional key INHERIT make VAR dependent on another variable. Such
  124. dependency means that data shared between variables will be preserved
  125. (see elisp#Circular Objects)."
  126. (unless org-persist--index (org-persist--read-index))
  127. (when inherit
  128. (let ((inherited-index (org-persist--get-index inherit buffer)))
  129. (unless (memq var (plist-get inherited-index :variable))
  130. (setq inherited-index
  131. (plist-put inherited-index :variable
  132. (cons var (plist-get inherited-index :variable)))))))
  133. (org-persist--get-index var buffer)
  134. (when buffer
  135. (add-hook 'kill-buffer-hook #'org-persist-write-all-buffer nil 'local)))
  136. (defun org-persist-unregister (var &optional buffer)
  137. "Unregister VAR in BUFFER to be persistent.
  138. When BUFFER is `all', unregister VAR in all buffers."
  139. (unless org-persist--index (org-persist--read-index))
  140. (setq org-persist--index
  141. (seq-remove
  142. (lambda (plist)
  143. (when (and (memq var (plist-get plist :variable))
  144. (or (eq buffer 'all)
  145. (string= (buffer-file-name
  146. (or (buffer-base-buffer buffer)
  147. buffer))
  148. (or (plist-get plist :path) ""))))
  149. (if (> (length (plist-get plist :variable)) 1)
  150. (progn
  151. (setq plist
  152. (plist-put plist :variable
  153. (delq var (plist-get plist :variable))))
  154. ;; Do not remove the index though.
  155. nil)
  156. (let ((persist-file (org-file-name-concat org-persist-directory (plist-get plist :persist-file))))
  157. (delete-file persist-file)
  158. (when (org-directory-empty-p (file-name-directory persist-file))
  159. (delete-directory (file-name-directory persist-file))))
  160. 'delete-from-index)))
  161. org-persist--index))
  162. (org-persist-gc))
  163. (defun org-persist-write (var &optional buffer)
  164. "Save buffer-local data in BUFFER for VAR."
  165. (unless (and buffer (not (get-buffer buffer)))
  166. (unless (listp var) (setq var (list var)))
  167. (with-current-buffer (or buffer (current-buffer))
  168. (let ((index (org-persist--get-index var buffer))
  169. (start-time (float-time)))
  170. (setf index (plist-put index :hash (when buffer (secure-hash 'md5 buffer))))
  171. (let ((print-circle t)
  172. print-level
  173. print-length
  174. print-quoted
  175. (print-escape-control-characters t)
  176. (print-escape-nonascii t)
  177. (print-continuous-numbering t)
  178. print-number-table)
  179. (unless (seq-find (lambda (v)
  180. (run-hook-with-args-until-success 'org-persist-before-write-hook v buffer))
  181. (plist-get index :variable))
  182. (unless (file-exists-p org-persist-directory)
  183. (make-directory org-persist-directory))
  184. (unless (file-exists-p org-persist-directory)
  185. (warn "Failed to create org-persist storage in %s."
  186. org-persist-directory)
  187. (let ((dir (directory-file-name
  188. (file-name-as-directory org-persist-directory))))
  189. (while (and (not (file-exists-p dir))
  190. (not (equal dir (setq dir (directory-file-name
  191. (file-name-directory dir)))))))
  192. (unless (file-writable-p dir)
  193. (message "Missing write access rights to org-persist-directory: %S"
  194. org-persist-directory))))
  195. (when (file-exists-p org-persist-directory)
  196. (with-temp-file (org-file-name-concat org-persist-directory org-persist-index-file)
  197. (prin1 org-persist--index (current-buffer)))
  198. (let ((file (org-file-name-concat org-persist-directory (plist-get index :persist-file)))
  199. (data (mapcar (lambda (s) (cons s (symbol-value s)))
  200. (plist-get index :variable))))
  201. (unless (file-exists-p (file-name-directory file))
  202. (make-directory (file-name-directory file) t))
  203. (with-temp-file file
  204. (prin1 data (current-buffer)))
  205. (let ((duration (- (float-time) start-time)))
  206. (when (or (and org-persist--report-time
  207. (numberp org-persist--report-time)
  208. (>= duration org-persist--report-time))
  209. (and org-persist--report-time
  210. (not (numberp org-persist--report-time))))
  211. (if buffer
  212. (message "org-persist: Writing %S from %S took %.2f sec"
  213. var buffer duration)
  214. (message "org-persist: Writing %S took %.2f sec"
  215. var duration))))))))))))
  216. (defun org-persist-write-all (&optional buffer)
  217. "Save all the persistent data."
  218. (unless (and buffer (not (buffer-file-name buffer)))
  219. (dolist (index org-persist--index)
  220. (when (or (and (not (plist-get index :path))
  221. (not buffer))
  222. (and (plist-get index :path)
  223. (get-file-buffer (plist-get index :path))
  224. (equal (buffer-file-name
  225. (or buffer
  226. (get-file-buffer (plist-get index :path))))
  227. (plist-get index :path))))
  228. (org-persist-write (plist-get index :variable)
  229. (when (plist-get index :path)
  230. (get-file-buffer (plist-get index :path))))))))
  231. (defun org-persist-write-all-buffer ()
  232. "Call `org-persist-write-all' in current buffer."
  233. (org-persist-write-all (current-buffer)))
  234. (defun org-persist-read (var &optional buffer)
  235. "Restore VAR data in BUFFER."
  236. (let* ((index (org-persist--get-index var buffer))
  237. (persist-file (org-file-name-concat org-persist-directory (plist-get index :persist-file)))
  238. (data nil)
  239. (start-time (float-time)))
  240. (when (and index
  241. (file-exists-p persist-file)
  242. (or (not buffer)
  243. (equal (secure-hash 'md5 buffer) (plist-get index :hash))))
  244. (unless (seq-find (lambda (v)
  245. (run-hook-with-args-until-success 'org-persist-before-read-hook v buffer))
  246. (plist-get index :variable))
  247. (with-temp-buffer
  248. (let ((coding-system-for-read 'utf-8)
  249. (read-circle t))
  250. (insert-file-contents persist-file))
  251. ;; FIXME: Reading sometimes fails to read circular objects.
  252. ;; I suspect that it happens when we have object reference
  253. ;; #N# read before object definition #N=. If it is really
  254. ;; so, it should be Emacs bug - either in `read' or in
  255. ;; `prin1'. Meanwhile, just fail silently when `read'
  256. ;; fails to parse the saved cache object.
  257. (condition-case err
  258. (setq data (read (current-buffer)))
  259. (error
  260. ;; Do not report the known error to user.
  261. (unless (string-match-p "Invalid read syntax" (error-message-string err))
  262. (warn "Emacs reader failed to read data for %S:%S. The error was: %S"
  263. (or buffer "global") var (error-message-string err)))
  264. (setq data nil))))
  265. (with-current-buffer (or buffer (current-buffer))
  266. (cl-loop for var1 in (plist-get index :variable)
  267. do
  268. (when (alist-get var1 data)
  269. (setf (symbol-value var1) (alist-get var1 data)))
  270. (run-hook-with-args 'org-persist-after-read-hook var1 buffer)))
  271. (let ((duration (- (float-time) start-time)))
  272. (when (or (and org-persist--report-time
  273. (numberp org-persist--report-time)
  274. (>= duration org-persist--report-time))
  275. (and org-persist--report-time
  276. (not (numberp org-persist--report-time))))
  277. (if buffer
  278. (message "org-persist: Reading %S from %S took %.2f sec"
  279. var buffer duration)
  280. (message "org-persist: Reading %S took %.2f sec"
  281. var duration))))))))
  282. (defun org-persist-read-all (&optional buffer)
  283. "Restore all the persistent data in BUFFER."
  284. (unless org-persist--index (org-persist--read-index))
  285. (dolist (index org-persist--index)
  286. (org-persist-read (plist-get index :variable) buffer)))
  287. (defun org-persist-read-all-buffer ()
  288. "Call `org-persist-read-all' in current buffer."
  289. (org-persist-read-all (current-buffer)))
  290. (defun org-persist-gc ()
  291. "Remove stored data for not existing files or unregistered variables."
  292. (let (new-index)
  293. (dolist (index org-persist--index)
  294. (let ((file (plist-get index :path))
  295. (persist-file (when (plist-get index :persist-file)
  296. (org-file-name-concat
  297. org-persist-directory
  298. (plist-get index :persist-file)))))
  299. (when (and file persist-file)
  300. (if (file-exists-p file)
  301. (push index new-index)
  302. (when (file-exists-p persist-file)
  303. (delete-file persist-file)
  304. (when (org-directory-empty-p (file-name-directory persist-file))
  305. (delete-directory (file-name-directory persist-file))))))))
  306. (setq org-persist--index (nreverse new-index))))
  307. ;; Automatically write the data, but only when we have write access.
  308. (let ((dir (directory-file-name
  309. (file-name-as-directory org-persist-directory))))
  310. (while (and (not (file-exists-p dir))
  311. (not (equal dir (setq dir (directory-file-name
  312. (file-name-directory dir)))))))
  313. (if (not (file-writable-p dir))
  314. (message "Missing write access rights to org-persist-directory: %S"
  315. org-persist-directory)
  316. (add-hook 'kill-emacs-hook #'org-persist-write-all)
  317. ;; `org-persist-gc' should run before `org-persist-write-all'. So we are adding the
  318. ;; hook after `org-persist-write-all'.
  319. (add-hook 'kill-emacs-hook #'org-persist-gc)))
  320. (add-hook 'after-init-hook #'org-persist-read-all)
  321. (provide 'org-persist)
  322. ;;; org-persist.el ends here