Merge branch 'master' of https://git.gnus.org/gnus
[gnus] / lisp / gnus-cache.el
1 ;;; gnus-cache.el --- cache interface for Gnus
2
3 ;; Copyright (C) 1995-2011 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;;; Code:
26
27 ;; For Emacs <22.2 and XEmacs.
28 (eval-and-compile
29   (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
30
31 (eval-when-compile (require 'cl))
32
33 (require 'gnus)
34 (require 'gnus-sum)
35
36 (eval-when-compile
37   (unless (fboundp 'gnus-agent-load-alist)
38     (defun gnus-agent-load-alist (group))))
39
40 (defcustom gnus-cache-active-file
41   (expand-file-name "active" gnus-cache-directory)
42   "*The cache active file."
43   :group 'gnus-cache
44   :type 'file)
45
46 (defcustom gnus-cache-enter-articles '(ticked dormant)
47   "Classes of articles to enter into the cache."
48   :group 'gnus-cache
49   :type '(set (const ticked) (const dormant) (const unread) (const read)))
50
51 (defcustom gnus-cache-remove-articles '(read)
52   "Classes of articles to remove from the cache."
53   :group 'gnus-cache
54   :type '(set (const ticked) (const dormant) (const unread) (const read)))
55
56 (defcustom gnus-cacheable-groups nil
57   "*Groups that match this regexp will be cached.
58
59 If you only want to cache your nntp groups, you could set this
60 variable to \"^nntp\".
61
62 If a group matches both gnus-cacheable-groups and gnus-uncacheable-groups
63 it's not cached."
64   :group 'gnus-cache
65   :type '(choice (const :tag "off" nil)
66                  regexp))
67
68 (defcustom gnus-uncacheable-groups nil
69   "*Groups that match this regexp will not be cached.
70
71 If you want to avoid caching your nnml groups, you could set this
72 variable to \"^nnml\".
73
74 If a group matches both gnus-cacheable-groups and gnus-uncacheable-groups
75 it's not cached."
76   :group 'gnus-cache
77   :type '(choice (const :tag "off" nil)
78                  regexp))
79
80 (defvar gnus-cache-overview-coding-system 'raw-text
81   "Coding system used on Gnus cache files.")
82
83 (defvar gnus-cache-coding-system 'raw-text
84   "Coding system used on Gnus cache files.")
85
86 \f
87
88 ;;; Internal variables.
89
90 (defvar gnus-cache-removable-articles nil)
91 (defvar gnus-cache-buffer nil)
92 (defvar gnus-cache-active-hashtb nil)
93 (defvar gnus-cache-active-altered nil)
94 (defvar gnus-cache-total-fetched-hashtb nil)
95
96 (declare-function nnvirtual-find-group-art "nnvirtual" (group article))
97
98 (autoload 'nnml-generate-nov-databases-directory "nnml")
99 (autoload 'nnvirtual-find-group-art "nnvirtual")
100
101 \f
102
103 ;;; Functions called from Gnus.
104
105 (defun gnus-cache-open ()
106   "Initialize the cache."
107   (when (or (file-exists-p gnus-cache-directory)
108             (and gnus-use-cache
109                  (not (eq gnus-use-cache 'passive))))
110     (gnus-cache-read-active)))
111
112 ;; Complexities of byte-compiling make this kludge necessary.  Eeek.
113 (ignore-errors
114   (gnus-add-shutdown 'gnus-cache-close 'gnus))
115
116 (defun gnus-cache-close ()
117   "Shut down the cache."
118   (gnus-cache-write-active)
119   (gnus-cache-save-buffers)
120   (setq gnus-cache-active-hashtb nil))
121
122 (defun gnus-cache-save-buffers ()
123   ;; save the overview buffer if it exists and has been modified
124   ;; delete empty cache subdirectories
125   (when gnus-cache-buffer
126     (let ((buffer (cdr gnus-cache-buffer))
127           (overview-file (gnus-cache-file-name
128                           (car gnus-cache-buffer) ".overview")))
129       ;; write the overview only if it was modified
130       (when (and (buffer-live-p buffer) (buffer-modified-p buffer))
131         (with-current-buffer buffer
132           (if (> (buffer-size) 0)
133               ;; Non-empty overview, write it to a file.
134               (let ((coding-system-for-write
135                      gnus-cache-overview-coding-system))
136                 (gnus-write-buffer overview-file))
137             (let ((file-name-coding-system nnmail-pathname-coding-system))
138               ;; Empty overview file, remove it
139               (when (file-exists-p overview-file)
140                 (delete-file overview-file))
141               ;; If possible, remove group's cache subdirectory.
142               (condition-case nil
143                   ;; FIXME: we can detect the error type and warn the user
144                   ;; of any inconsistencies (articles w/o nov entries?).
145                   ;; for now, just be conservative...delete only if safe -- sj
146                   (delete-directory (file-name-directory overview-file))
147                 (error))))
148
149           (gnus-cache-update-overview-total-fetched-for
150            (car gnus-cache-buffer) overview-file)))
151       ;; Kill the buffer -- it's either unmodified or saved.
152       (gnus-kill-buffer buffer)
153       (setq gnus-cache-buffer nil))))
154
155 (defun gnus-cache-possibly-enter-article
156   (group article ticked dormant unread &optional force)
157   (when (and (or force (not (eq gnus-use-cache 'passive)))
158              (numberp article)
159              (> article 0))             ; This might be a dummy article.
160     (let ((number article)
161           file headers lines-chars
162           (file-name-coding-system nnmail-pathname-coding-system))
163       ;; If this is a virtual group, we find the real group.
164       (when (gnus-virtual-group-p group)
165         (let ((result (nnvirtual-find-group-art
166                        (gnus-group-real-name group) article)))
167           (setq group (car result)
168                 number (cdr result))))
169       (when (and number
170                  (> number 0)           ; Reffed article.
171                  (or force
172                      (and (gnus-cache-fully-p group)
173                           (gnus-cache-member-of-class
174                            gnus-cache-enter-articles ticked dormant unread)))
175                  (not (file-exists-p (setq file (gnus-cache-file-name
176                                                  group number)))))
177         ;; Possibly create the cache directory.
178         (gnus-make-directory (file-name-directory file))
179         ;; Save the article in the cache.
180         (if (file-exists-p file)
181             t                           ; The article already is saved.
182           (with-current-buffer nntp-server-buffer
183             (require 'gnus-art)
184             (let ((gnus-use-cache nil)
185                   (gnus-article-decode-hook nil))
186               (gnus-request-article-this-buffer number group))
187             (when (> (buffer-size) 0)
188               (let ((coding-system-for-write gnus-cache-coding-system))
189                 (gnus-write-buffer file)
190                 (gnus-cache-update-file-total-fetched-for group file))
191               (setq lines-chars (nnheader-get-lines-and-char))
192               (nnheader-remove-body)
193               (setq headers (nnheader-parse-naked-head))
194               (mail-header-set-number headers number)
195               (mail-header-set-lines headers (car lines-chars))
196               (mail-header-set-chars headers (cadr lines-chars))
197               (gnus-cache-change-buffer group)
198               (set-buffer (cdr gnus-cache-buffer))
199               (goto-char (point-max))
200               (forward-line -1)
201               (while (condition-case ()
202                          (when (not (bobp))
203                            (> (read (current-buffer)) number))
204                        (error
205                         ;; The line was malformed, so we just remove it!!
206                         (gnus-delete-line)
207                         t))
208                 (forward-line -1))
209               (if (bobp)
210                   (if (not (eobp))
211                       (progn
212                         (beginning-of-line)
213                         (when (< (read (current-buffer)) number)
214                           (forward-line 1)))
215                     (beginning-of-line))
216                 (forward-line 1))
217               (beginning-of-line)
218               (nnheader-insert-nov headers)
219               ;; Update the active info.
220               (set-buffer gnus-summary-buffer)
221               (gnus-cache-possibly-update-active group (cons number number))
222               (setq gnus-newsgroup-cached
223                     (gnus-add-to-sorted-list gnus-newsgroup-cached article))
224               (gnus-summary-update-secondary-mark article))
225             t))))))
226
227 (defun gnus-cache-enter-remove-article (article)
228   "Mark ARTICLE for later possible removal."
229   (when article
230     (push article gnus-cache-removable-articles)))
231
232 (defun gnus-cache-possibly-remove-articles ()
233   "Possibly remove some of the removable articles."
234   (if (not (gnus-virtual-group-p gnus-newsgroup-name))
235       (gnus-cache-possibly-remove-articles-1)
236     (let ((arts gnus-cache-removable-articles)
237           ga)
238       (while arts
239         (when (setq ga (nnvirtual-find-group-art
240                         (gnus-group-real-name gnus-newsgroup-name) (pop arts)))
241           (let ((gnus-cache-removable-articles (list (cdr ga)))
242                 (gnus-newsgroup-name (car ga)))
243             (gnus-cache-possibly-remove-articles-1)))))
244     (setq gnus-cache-removable-articles nil)))
245
246 (defun gnus-cache-possibly-remove-articles-1 ()
247   "Possibly remove some of the removable articles."
248   (when (gnus-cache-fully-p gnus-newsgroup-name)
249     (let ((cache-articles gnus-newsgroup-cached))
250       (gnus-cache-change-buffer gnus-newsgroup-name)
251       (dolist (article gnus-cache-removable-articles)
252         (when (memq article cache-articles)
253           ;; The article was in the cache, so we see whether we are
254           ;; supposed to remove it from the cache.
255           (gnus-cache-possibly-remove-article
256            article (memq article gnus-newsgroup-marked)
257            (memq article gnus-newsgroup-dormant)
258            (or (memq article gnus-newsgroup-unreads)
259                (memq article gnus-newsgroup-unselected))))))
260     ;; The overview file might have been modified, save it
261     ;; safe because we're only called at group exit anyway.
262     (gnus-cache-save-buffers)))
263
264 (defun gnus-cache-request-article (article group)
265   "Retrieve ARTICLE in GROUP from the cache."
266   (let ((file (gnus-cache-file-name group article))
267         (buffer-read-only nil)
268         (file-name-coding-system nnmail-pathname-coding-system))
269     (when (file-exists-p file)
270       (erase-buffer)
271       (gnus-kill-all-overlays)
272       (let ((coding-system-for-read gnus-cache-coding-system))
273         (insert-file-contents file))
274       t)))
275
276 (defun gnus-cache-possibly-alter-active (group active)
277   "Alter the ACTIVE info for GROUP to reflect the articles in the cache."
278   (when gnus-cache-active-hashtb
279     (let ((cache-active (gnus-gethash group gnus-cache-active-hashtb)))
280       (when cache-active
281         (when (< (car cache-active) (car active))
282           (setcar active (car cache-active)))
283         (when (> (cdr cache-active) (cdr active))
284           (setcdr active (cdr cache-active)))))))
285
286 (defun gnus-cache-retrieve-headers (articles group &optional fetch-old)
287   "Retrieve the headers for ARTICLES in GROUP."
288   (let ((cached
289          (setq gnus-newsgroup-cached (gnus-cache-articles-in-group group))))
290     (if (not cached)
291         ;; No cached articles here, so we just retrieve them
292         ;; the normal way.
293         (let ((gnus-use-cache nil))
294           (gnus-retrieve-headers articles group fetch-old))
295       (let ((uncached-articles (gnus-sorted-difference articles cached))
296             (cache-file (gnus-cache-file-name group ".overview"))
297             type
298             (file-name-coding-system nnmail-pathname-coding-system))
299         ;; We first retrieve all the headers that we don't have in
300         ;; the cache.
301         (let ((gnus-use-cache nil))
302           (when uncached-articles
303             (setq type (and articles
304                             (gnus-retrieve-headers
305                              uncached-articles group fetch-old)))))
306         (gnus-cache-save-buffers)
307         ;; Then we insert the cached headers.
308         (save-excursion
309           (cond
310            ((not (file-exists-p cache-file))
311             ;; There are no cached headers.
312             type)
313            ((null type)
314             ;; There were no uncached headers (or retrieval was
315             ;; unsuccessful), so we use the cached headers exclusively.
316             (set-buffer nntp-server-buffer)
317             (erase-buffer)
318             (let ((coding-system-for-read
319                    gnus-cache-overview-coding-system))
320               (insert-file-contents cache-file))
321             'nov)
322            ((eq type 'nov)
323             ;; We have both cached and uncached NOV headers, so we
324             ;; braid them.
325             (gnus-cache-braid-nov group cached)
326             type)
327            (t
328             ;; We braid HEADs.
329             (gnus-cache-braid-heads group (gnus-sorted-intersection
330                                            cached articles))
331             type)))))))
332
333 (defun gnus-cache-enter-article (&optional n)
334   "Enter the next N articles into the cache.
335 If not given a prefix, use the process marked articles instead.
336 Returns the list of articles entered."
337   (interactive "P")
338   (let (out)
339     (dolist (article (gnus-summary-work-articles n))
340       (gnus-summary-remove-process-mark article)
341       (if (natnump article)
342           (when (gnus-cache-possibly-enter-article
343                  gnus-newsgroup-name article
344                  nil nil nil t)
345             (setq gnus-newsgroup-undownloaded (delq article gnus-newsgroup-undownloaded))
346             (push article out))
347         (gnus-message 2 "Can't cache article %d" article))
348       (gnus-summary-update-download-mark article)
349       (gnus-summary-update-secondary-mark article))
350     (gnus-summary-next-subject 1)
351     (gnus-summary-position-point)
352     (nreverse out)))
353
354 (defun gnus-cache-remove-article (&optional n)
355   "Remove the next N articles from the cache.
356 If not given a prefix, use the process marked articles instead.
357 Returns the list of articles removed."
358   (interactive "P")
359   (gnus-cache-change-buffer gnus-newsgroup-name)
360   (let (out)
361     (dolist (article (gnus-summary-work-articles n))
362       (gnus-summary-remove-process-mark article)
363       (when (gnus-cache-possibly-remove-article article nil nil nil t)
364         (when gnus-newsgroup-agentized
365           (let ((alist (gnus-agent-load-alist gnus-newsgroup-name)))
366             (unless (cdr (assoc article alist))
367               (setq gnus-newsgroup-undownloaded
368                     (gnus-add-to-sorted-list
369                      gnus-newsgroup-undownloaded article)))))
370         (push article out))
371       (gnus-summary-update-download-mark article)
372       (gnus-summary-update-secondary-mark article))
373     (gnus-summary-next-subject 1)
374     (gnus-summary-position-point)
375     (nreverse out)))
376
377 (defun gnus-cached-article-p (article)
378   "Say whether ARTICLE is cached in the current group."
379   (memq article gnus-newsgroup-cached))
380
381 (defun gnus-summary-insert-cached-articles ()
382   "Insert all the articles cached for this group into the current buffer."
383   (interactive)
384   (let ((gnus-verbose (max 6 gnus-verbose)))
385     (cond
386      ((not gnus-newsgroup-cached)
387       (gnus-message 3 "No cached articles for this group"))
388      ;; This is faster if there are few articles to insert.
389      ((< (length gnus-newsgroup-cached) 20)
390       (gnus-summary-goto-subjects gnus-newsgroup-cached))
391      (t
392       (gnus-summary-include-articles gnus-newsgroup-cached)))))
393
394 (defun gnus-summary-limit-include-cached ()
395   "Limit the summary buffer to articles that are cached."
396   (interactive)
397   (let ((gnus-verbose (max 6 gnus-verbose)))
398     (if gnus-newsgroup-cached
399         (progn
400           (gnus-summary-limit gnus-newsgroup-cached)
401           (gnus-summary-position-point))
402       (gnus-message 3 "No cached articles for this group"))))
403
404 ;;; Internal functions.
405
406 (defun gnus-cache-change-buffer (group)
407   (and gnus-cache-buffer
408        ;; See if the current group's overview cache has been loaded.
409        (or (string= group (car gnus-cache-buffer))
410            ;; Another overview cache is current, save it.
411            (gnus-cache-save-buffers)))
412   ;; if gnus-cache buffer is nil, create it
413   (unless gnus-cache-buffer
414     ;; Create cache buffer
415     (save-excursion
416       (setq gnus-cache-buffer
417             (cons group
418                   (set-buffer (gnus-get-buffer-create
419                                " *gnus-cache-overview*"))))
420       ;; Insert the contents of this group's cache overview.
421       (erase-buffer)
422       (let ((file (gnus-cache-file-name group ".overview"))
423             (file-name-coding-system nnmail-pathname-coding-system))
424         (when (file-exists-p file)
425           (nnheader-insert-file-contents file)))
426   &nb