123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- ;;; org-persist.el --- Persist data across Emacs sessions -*- lexical-binding: t; -*-
- ;; Copyright (C) 2021-2021 Free Software Foundation, Inc.
- ;; Author: Ihor Radchenko <yantar92 at gmail dot com>
- ;; Keywords: cache, storage
- ;; This file is part of GNU Emacs.
- ;; GNU Emacs is free software: you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
- ;; GNU Emacs is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;; You should have received a copy of the GNU General Public License
- ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
- ;;; Commentary:
- ;;
- ;; This file implements persistant data storage across Emacs sessions.
- ;; Both global and buffer-local data can be stored.
- ;;; Code:
- (require 'org-compat)
- (require 'org-id)
- (declare-function org-back-to-heading "org" (&optional invisible-ok))
- (declare-function org-next-visible-heading "org" (arg))
- (declare-function org-at-heading-p "org" (&optional invisible-not-ok))
- (defvar org-persist-path (org-file-name-concat user-emacs-directory "org-persist/")
- "Directory where the data is stored.")
- (defvar org-persist-index-file "index"
- "File name used to store the data index.")
- (defvar org-persist-before-write-hook nil
- "Abnormal hook ran before saving data for a single variable in a buffer.
- The hook must accept the same arguments as `org-persist-write'.
- The hooks will be evaluated until a hook returns non-nil.
- If any of the hooks return non-nil, do not save the data.")
- (defvar org-persist-before-read-hook nil
- "Abnormal hook ran before reading data for a single variable in a buffer.
- The hook must accept the same arguments as `org-persist-read'.
- The hooks will be evaluated until a hook returns non-nil.
- If any of the hooks return non-nil, do not read the data.")
- (defvar org-persist-after-read-hook nil
- "Abnormal hook ran after reading data for a single variable in a buffer.
- The hook must accept the same arguments as `org-persist-read'.")
- (defvar org-persist--index nil
- "Global index.
- The index is a list of plists. Each plist contains information about
- a data variable. Each plist contains the following properties:
- - `:variable' list of variables to be stored in single file
- - `:persist-file': data file name
- - `:path': buffer file path, if any
- - `:inode': buffer file inode, if any
- - `:hash': buffer hash, if any")
- (defun org-persist--get-index (var &optional buffer)
- "Return plist used to store VAR in BUFFER.
- When BUFFER is nil, return plist for global VAR."
- (let* ((buffer-file (when buffer (buffer-file-name (or (buffer-base-buffer buffer)
- buffer))))
- (inode (when buffer-file (file-attribute-inode-number (file-attributes buffer-file)))))
- (let ((result (seq-find (lambda (plist)
- (and (or (memq var (plist-get plist :variable))
- (eq var (plist-get plist :variable)))
- (or (equal inode (plist-get plist :inode))
- (equal buffer-file (plist-get plist :path)))))
- org-persist--index)))
- (when result
- (unless (equal buffer-file (plist-get result :path))
- (setf result (plist-put result :path buffer-file))))
- (unless result
- (push (list :variable (if (listp var) var (list var))
- :persist-file (replace-regexp-in-string "^.." "\\&/" (org-id-uuid))
- :path buffer-file
- :inode inode
- :hash (when buffer (secure-hash 'md5 buffer)))
- org-persist--index)
- (setf result (car org-persist--index)))
- result)))
- (defun org-persist--read-index ()
- "Read `org-persist--index'"
- (unless org-persist--index
- (when (file-exists-p (org-file-name-concat org-persist-path org-persist-index-file))
- (with-temp-buffer
- (insert-file-contents (org-file-name-concat org-persist-path org-persist-index-file))
- (setq org-persist--index (read (current-buffer)))))))
- (cl-defun org-persist-register (var &optional buffer &key inherit)
- "Register VAR in BUFFER to be persistent.
- Optional key INHERIT make VAR dependent on another variable. Such
- dependency means that data shared between variables will be preserved
- (see elisp#Circular Objects)."
- (unless org-persist--index (org-persist--read-index))
- (when inherit
- (let ((inherited-index (org-persist--get-index inherit buffer)))
- (unless (memq var (plist-get inherited-index :variable))
- (push var (plist-get inherited-index :variable)))))
- (org-persist--get-index var buffer)
- (when buffer
- (add-hook 'kill-buffer-hook #'org-persist-write-all-buffer 1000 'local)))
- (defun org-persist-unregister (var &optional buffer)
- "Unregister VAR in BUFFER to be persistent.
- When BUFFER is `all', unregister VAR in all buffers."
- (unless org-persist--index (org-persist--read-index))
- (setq org-persist--index
- (seq-remove
- (lambda (plist)
- (when (and (memq var (plist-get plist :variable))
- (or (eq buffer 'all)
- (eq (buffer-file-name
- (or (buffer-base-buffer buffer)
- buffer))
- (plist-get plist :path))))
- (if (length> (plist-get plist :variable) 1)
- (progn
- (setq plist
- (plist-put plist :variable
- (delq var (plist-get plist :variable))))
- ;; Do not remove the index though.
- nil)
- (let ((persist-file (org-file-name-concat org-persist-path (plist-get plist :persist-file))))
- (delete-file persist-file)
- (when (directory-empty-p (file-name-directory persist-file))
- (delete-directory (file-name-directory persist-file))))
- 'delete-from-index)))
- org-persist--index))
- (org-persist-gc))
- (defun org-persist-write (var &optional buffer)
- "Save buffer-local data in BUFFER for VAR."
- (unless (and buffer (not (get-buffer buffer)))
- (unless (listp var) (setq var (list var)))
- (with-current-buffer (or buffer (current-buffer))
- (let ((index (org-persist--get-index var buffer)))
- (setf index (plist-put index :hash (when buffer (secure-hash 'md5 buffer))))
- (let ((print-circle t)
- print-level
- print-length
- print-quoted
- (print-escape-control-characters t)
- (print-escape-nonascii t)
- (print-continuous-numbering t)
- print-number-table)
- (unless (seq-find (lambda (v)
- (run-hook-with-args-until-success 'org-persist-before-write-hook v buffer))
- (plist-get index :variable))
- (unless (file-exists-p org-persist-path)
- (make-directory org-persist-path))
- (with-temp-file (org-file-name-concat org-persist-path org-persist-index-file)
- (prin1 org-persist--index (current-buffer)))
- (let ((file (org-file-name-concat org-persist-path (plist-get index :persist-file)))
- (data (mapcar (lambda (s) (cons s (symbol-value s)))
- (plist-get index :variable))))
- (unless (file-exists-p (file-name-directory file))
- (make-directory (file-name-directory file) t))
- (with-temp-file file
- (prin1 data (current-buffer))))))))))
- (defun org-persist-write-all (&optional buffer)
- "Save all the persistent data."
- (dolist (index org-persist--index)
- (when (or (not (plist-get index :path))
- (and (get-file-buffer (plist-get index :path))
- (or (not buffer)
- (equal (buffer-file-name (or (buffer-base-buffer buffer)
- buffer))
- (plist-get index :path)))))
- (org-persist-write (plist-get index :variable)
- (when (plist-get index :path)
- (get-file-buffer (plist-get index :path)))))))
- (defun org-persist-write-all-buffer ()
- "Call `org-persist-write-all' in current buffer."
- (org-persist-write-all (current-buffer)))
- (defun org-persist-read (var &optional buffer)
- "Restore VAR data in BUFFER."
- (let* ((index (org-persist--get-index var buffer))
- (persist-file (org-file-name-concat org-persist-path (plist-get index :persist-file)))
- (data nil))
- (when (and (file-exists-p persist-file)
- (or (not buffer)
- (equal (secure-hash 'md5 buffer) (plist-get index :hash))))
- (unless (seq-find (lambda (v)
- (run-hook-with-args-until-success 'org-persist-before-read-hook v buffer))
- (plist-get index :variable))
- (with-temp-buffer
- (let ((coding-system-for-read 'utf-8)
- (read-circle t))
- (insert-file-contents persist-file))
- ;; FIXME: Reading sometimes fails to read circular objects.
- ;; I suspect that it happens when we have object reference
- ;; #N# read before object definition #N=. If it is really
- ;; #so, it should be Emacs bug - either in `read' or in
- ;; #`prin1'. Meanwhile, just fail silently when `read'
- ;; #fails to parse the saved cache object.
- (condition-case err
- (setq data (read (current-buffer)))
- (error
- (warn "Emacs reader failed to read data for %S:%S. The error was: %S"
- (or buffer "global") var (error-message-string err))
- (setq data nil))))
- (with-current-buffer (or buffer (current-buffer))
- (cl-loop for var1 in (plist-get index :variable)
- do
- (when (alist-get var1 data)
- (setf (symbol-value var1) (alist-get var1 data)))
- (run-hook-with-args 'org-persist-after-read-hook var1 buffer)))))))
- (defun org-persist-read-all (&optional buffer)
- "Restore all the persistent data in BUFFER."
- (unless org-persist--index (org-persist--read-index))
- (dolist (index org-persist--index)
- (when (equal (buffer-file-name (or (buffer-base-buffer buffer)
- buffer))
- (plist-get index :path))
- (org-persist-read (plist-get index :variable) buffer))))
- (defun org-persist-read-all-buffer ()
- "Call `org-persist-read-all' in current buffer."
- (org-persist-read-all (current-buffer)))
- (defun org-persist-gc ()
- "Remove stored data for not existing files or unregistered variables."
- (let (new-index)
- (dolist (index org-persist--index)
- (when-let ((file (plist-get index :path))
- (persist-file (org-file-name-concat
- org-persist-path
- (plist-get index :persist-file))))
- (if (file-exists-p file)
- (push index new-index)
- (when (file-exists-p persist-file)
- (delete-file persist-file)
- (when (directory-empty-p (file-name-directory persist-file))
- (delete-directory (file-name-directory persist-file)))))))
- (setq org-persist--index (nreverse new-index))))
- (add-hook 'kill-emacs-hook #'org-persist-gc)
- (add-hook 'kill-emacs-hook #'org-persist-write-all 1000)
- (add-hook 'after-init-hook #'org-persist-read-all)
- (provide 'org-persist)
- ;;; org-persist.el ends here
|