| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 | ;;; org-persist.el --- Persist data across Emacs sessions         -*- lexical-binding: t; -*-;; Copyright (C) 2021-2022 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)(require 'xdg nil t)(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))(defgroup org-persist nil  "Persistent cache for Org mode."  :tag "Org persist"  :group 'org)(defcustom org-persist-directory (expand-file-name                       (org-file-name-concat                        (let ((cache-dir (when (fboundp 'xdg-cache-home)                                           (xdg-cache-home))))                          (if (or (seq-empty-p cache-dir)                                  (not (file-exists-p cache-dir))                                  (file-exists-p (org-file-name-concat                                                  user-emacs-directory                                                  "org-persist")))                              user-emacs-directory                            cache-dir))                        "org-persist/"))  "Directory where the data is stored."  :group 'org-persist  :type 'directory)(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 abouta 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")(defvar org-persist--report-time 0.5  "Whether to report read/write time.When the value is a number, it is a threshold number of seconds.  Ifthe read/write time of a single variable exceeds the threashold, amessage is displayed.When the value is a non-nil non-number, always display the message.When the value is nil, never diplay the message.")(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."  (org-persist--read-index)  (let* ((buffer-file (when buffer (buffer-file-name (or (buffer-base-buffer buffer)                                                         buffer))))         (inode (when buffer-file                  (and (fboundp 'file-attribute-inode-number)                       (file-attribute-inode-number (file-attributes buffer-file)))))         (buffer-hash (when buffer (secure-hash 'md5 buffer))))    (let ((result (seq-find (lambda (plist)                              (and (or (memq var (plist-get plist :variable))                                       (eq var (plist-get plist :variable)))                                   (or (and inode (equal inode (plist-get plist :inode)))                                       (and buffer-file (equal buffer-file (plist-get plist :path)))                                       (and buffer-hash (equal buffer-hash (plist-get plist :hash))))))                            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 buffer-hash)              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-directory org-persist-index-file))      (with-temp-buffer        (insert-file-contents (org-file-name-concat org-persist-directory org-persist-index-file))        (setq org-persist--index              (condition-case err                  (read (current-buffer))                ;; Recover gracefully if index file is corrupted.                (error                 (warn "Emacs reader failed to read data for `org-persist--index' from %S. The error was: %S"                       (org-file-name-concat org-persist-directory org-persist-index-file)                       (error-message-string err))                 nil)))))))(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.  Suchdependency 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))        (setq inherited-index              (plist-put inherited-index :variable                         (cons var (plist-get inherited-index :variable)))))))  (org-persist--get-index var buffer)  (when buffer    (add-hook 'kill-buffer-hook #'org-persist-write-all-buffer nil '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)                          (string= (buffer-file-name                                    (or (buffer-base-buffer buffer)                                        buffer))                                   (or (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-directory (plist-get plist :persist-file))))                 (delete-file persist-file)                 (when (org-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))            (start-time (float-time)))        (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-directory)              (make-directory org-persist-directory))            (unless (file-exists-p org-persist-directory)              (warn "Failed to create org-persist storage in %s."                    org-persist-directory)              (let ((dir (directory-file-name                          (file-name-as-directory org-persist-directory))))                (while (and (not (file-exists-p dir))                            (not (equal dir (setq dir (directory-file-name                                                     (file-name-directory dir)))))))                (unless (file-writable-p dir)                  (message "Missing write access rights to org-persist-directory: %S"                           org-persist-directory))))            (when (file-exists-p org-persist-directory)              (with-temp-file (org-file-name-concat org-persist-directory org-persist-index-file)                (prin1 org-persist--index (current-buffer)))              (let ((file (org-file-name-concat org-persist-directory (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)))                (let ((duration (- (float-time) start-time)))                  (when (or (and org-persist--report-time                                 (numberp org-persist--report-time)                                 (>= duration org-persist--report-time))                            (and org-persist--report-time                                 (not (numberp org-persist--report-time))))                    (if buffer                        (message "org-persist: Writing %S from %S took %.2f sec"                                 var buffer duration)                      (message "org-persist: Writing %S took %.2f sec"                               var duration))))))))))))(defun org-persist-write-all (&optional buffer)  "Save all the persistent data."  (unless (and buffer (not (buffer-file-name buffer)))    (dolist (index org-persist--index)      (when (or (and (not (plist-get index :path))                     (not buffer))                (and (plist-get index :path)                     (get-file-buffer (plist-get index :path))                     (equal (buffer-file-name                             (or buffer                                 (get-file-buffer (plist-get index :path))))                            (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-directory (plist-get index :persist-file)))         (data nil)         (start-time (float-time)))    (when (and index               (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             ;; Do not report the known error to user.             (unless (string-match-p "Invalid read syntax" (error-message-string err))               (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)))        (let ((duration (- (float-time) start-time)))          (when (or (and org-persist--report-time                         (numberp org-persist--report-time)                         (>= duration org-persist--report-time))                    (and org-persist--report-time                         (not (numberp org-persist--report-time))))            (if buffer                (message "org-persist: Reading %S from %S took %.2f sec"                         var buffer duration)              (message "org-persist: Reading %S took %.2f sec"                       var duration))))))))(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)    (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)      (let ((file (plist-get index :path))            (persist-file (when (plist-get index :persist-file)                            (org-file-name-concat                             org-persist-directory                             (plist-get index :persist-file)))))        (when (and file persist-file)          (if (file-exists-p file)              (push index new-index)            (when (file-exists-p persist-file)              (delete-file persist-file)              (when (org-directory-empty-p (file-name-directory persist-file))                (delete-directory (file-name-directory persist-file))))))))    (setq org-persist--index (nreverse new-index))));; Automatically write the data, but only when we have write access.(let ((dir (directory-file-name            (file-name-as-directory org-persist-directory))))  (while (and (not (file-exists-p dir))              (not (equal dir (setq dir (directory-file-name                                       (file-name-directory dir)))))))  (if (not (file-writable-p dir))      (message "Missing write access rights to org-persist-directory: %S"               org-persist-directory)    (add-hook 'kill-emacs-hook #'org-persist-write-all)    ;; `org-persist-gc' should run before `org-persist-write-all'.  So we are adding the    ;; hook after `org-persist-write-all'.    (add-hook 'kill-emacs-hook #'org-persist-gc)))(add-hook 'after-init-hook #'org-persist-read-all)(provide 'org-persist);;; org-persist.el ends here
 |