diff options
| author | Ted Zlatanov | 2016-07-11 11:01:26 -0400 |
|---|---|---|
| committer | Ted Zlatanov | 2016-07-20 08:52:11 -0400 |
| commit | 30b3a842ec87d27cfe003b6d4323689d48b3fcd2 (patch) | |
| tree | 6588aff454fd55b05d5c08ae0c7d40b209dae831 | |
| parent | 60dd094a8c7bdbbff121c99f56f42910534e7cc1 (diff) | |
| download | emacs-30b3a842ec87d27cfe003b6d4323689d48b3fcd2.tar.gz emacs-30b3a842ec87d27cfe003b6d4323689d48b3fcd2.zip | |
Bring the Gnus Cloud package into working order.
* lisp/gnus/gnus-sync.el: Removed in favor of gnus-cloud.el.
* lisp/gnus/gnus-cloud.el: Autoload EPG functions. Change storage format to
simplify non-file data.
(gnus-cloud-storage-method): New defcustom to support nil, Base64,
Base64+gzip, or EPG encoding on the Gnus Cloud IMAP server. Defaults to
EPG if that's available, Base64+gzip otherwise.
(gnus-cloud-interactive): New defcustom to make Gnus Cloud operations
interactive, defaults to enabled.
(gnus-cloud-group-name): New variable for the Gnus Cloud group name.
(gnus-cloud-make-chunk): Tag with "Gnus-Cloud-Version" instead of just
"Version".
(gnus-cloud-insert-data): Simplify and support :newsrc-data entries.
(gnus-cloud-encode-data, gnus-cloud-decode-data): Support various
storage methods as per gnus-cloud-storage-method.
(gnus-cloud-parse-chunk): Look for "Gnus-Cloud-Version" marker.
(gnus-cloud-parse-version-1): Fix parsing loop bug. Handle :newsrc-data
entries.
(gnus-cloud-update-all): Handle :newsrc-data entries and dispatch to
file and data handlers.
(gnus-cloud-update-newsrc-data): New function to handle :newrsc-data
entries.
(gnus-cloud-update-file): Rework to support gnus-cloud-interactive and
be more careful.
(gnus-cloud-delete-file): Remove; merged into gnus-cloud-update-file.
(gnus-cloud-file-covered-p, gnus-cloud-all-files)
(gnus-cloud-files-to-upload, gnus-cloud-ensure-cloud-group)
(gnus-cloud-add-timestamps, gnus-cloud-available-chunks)
(gnus-cloud-prune-old-chunks): Fix indentation.
(gnus-cloud-timestamp): New function to make a standard Gnus Cloud
timestamp.
(gnus-cloud-file-new-p): Use it.
(gnus-cloud-upload-all-data): Add interactive convenience function to
upload all data.
(gnus-cloud-upload-data): Make interactive; collect files and newsrc
data separately; refresh Gnus Cloud group after insert.
(gnus-cloud-download-all-data): Add interactive convenience function to
download all data.
(gnus-cloud-download-data): Rework to support "Gnus-Cloud-Version"
marker and different storage methods.
(gnus-cloud-host-server-p): New function to check if a server is the
Gnus Cloud host.
(gnus-cloud-collect-full-newsrc): Tag entries with :newsrc-data.
(gnus-cloud-host-acceptable-method-p): New function so
other code can check if a server method can host the Gnus cloud.
(gnus-cloud-storage-method): Use 'radio instead of 'choice for better UI.
(gnus-cloud-method): Make this a defcustom and note how to set it.
* lisp/gnus/gnus-group.el (gnus-group-cloud-map): Add Gnus Cloud autoloaded
keybindings under the `~' prefix.
* lisp/gnus/gnus-srvr.el (gnus-server-mode-map, gnus-server-make-menu-bar)
(gnus-server-cloud, gnus-server-cloud-host)
(gnus-server-font-lock-keywords, gnus-server-insert-server-line)
(gnus-server-toggle-cloud-method-server): Support Gnus Cloud
synchronized servers and synchronization host server toggling (`i' and
`I') and visual display.
(gnus-server-toggle-cloud-method-server): Use
gnus-cloud-host-acceptable-method-p.
(gnus-server-toggle-cloud-method-server): Use custom-set-variables to
set the gnus-cloud-method. Ask the user if it's OK to upload the data
right now.
* doc/misc/gnus.texi: Document Gnus Cloud package.
| -rw-r--r-- | doc/misc/gnus.texi | 96 | ||||
| -rw-r--r-- | lisp/gnus/gnus-cloud.el | 462 | ||||
| -rw-r--r-- | lisp/gnus/gnus-group.el | 9 | ||||
| -rw-r--r-- | lisp/gnus/gnus-srvr.el | 41 | ||||
| -rw-r--r-- | lisp/gnus/gnus-sync.el | 896 |
5 files changed, 446 insertions, 1058 deletions
diff --git a/doc/misc/gnus.texi b/doc/misc/gnus.texi index df673fc099f..2473d26cc15 100644 --- a/doc/misc/gnus.texi +++ b/doc/misc/gnus.texi | |||
| @@ -828,6 +828,7 @@ Various | |||
| 828 | * Thwarting Email Spam:: Simple ways to avoid unsolicited commercial email. | 828 | * Thwarting Email Spam:: Simple ways to avoid unsolicited commercial email. |
| 829 | * Spam Package:: A package for filtering and processing spam. | 829 | * Spam Package:: A package for filtering and processing spam. |
| 830 | * The Gnus Registry:: A package for tracking messages by Message-ID. | 830 | * The Gnus Registry:: A package for tracking messages by Message-ID. |
| 831 | * The Gnus Cloud:: A package for synchronizing Gnus marks. | ||
| 831 | * Other modes:: Interaction with other modes. | 832 | * Other modes:: Interaction with other modes. |
| 832 | * Various Various:: Things that are really various. | 833 | * Various Various:: Things that are really various. |
| 833 | 834 | ||
| @@ -22208,6 +22209,7 @@ to you, using @kbd{G b u} and updating the group will usually fix this. | |||
| 22208 | * Thwarting Email Spam:: Simple ways to avoid unsolicited commercial email. | 22209 | * Thwarting Email Spam:: Simple ways to avoid unsolicited commercial email. |
| 22209 | * Spam Package:: A package for filtering and processing spam. | 22210 | * Spam Package:: A package for filtering and processing spam. |
| 22210 | * The Gnus Registry:: A package for tracking messages by Message-ID. | 22211 | * The Gnus Registry:: A package for tracking messages by Message-ID. |
| 22212 | * The Gnus Cloud:: A package for synchronizing Gnus marks. | ||
| 22211 | * Other modes:: Interaction with other modes. | 22213 | * Other modes:: Interaction with other modes. |
| 22212 | * Various Various:: Things that are really various. | 22214 | * Various Various:: Things that are really various. |
| 22213 | @end menu | 22215 | @end menu |
| @@ -26166,6 +26168,100 @@ default this is just @code{(marks)} so the custom registry marks are | |||
| 26166 | precious. | 26168 | precious. |
| 26167 | @end defvar | 26169 | @end defvar |
| 26168 | 26170 | ||
| 26171 | @node The Gnus Cloud | ||
| 26172 | @section The Gnus Cloud | ||
| 26173 | @cindex cloud | ||
| 26174 | @cindex gnus-cloud | ||
| 26175 | @cindex synchronization | ||
| 26176 | @cindex sync | ||
| 26177 | @cindex synch | ||
| 26178 | |||
| 26179 | The Gnus Cloud is a way to synchronize marks and general files and | ||
| 26180 | data across multiple machines. | ||
| 26181 | |||
| 26182 | Very often, you want all your marks (what articles you've read, which | ||
| 26183 | ones were important, and so on) to be synchronized between several | ||
| 26184 | machines. With IMAP, that's built into the protocol, so you can read | ||
| 26185 | nnimap groups from many machines and they are automatically | ||
| 26186 | synchronized. But NNTP, nnrss, and many other backends do not store | ||
| 26187 | marks, so you have to do it locally. | ||
| 26188 | |||
| 26189 | The Gnus Cloud package stores the marks, plus any files you choose, on | ||
| 26190 | an IMAP server in a special folder. It's like a | ||
| 26191 | DropTorrentSyncBoxOakTree(TM). | ||
| 26192 | |||
| 26193 | @menu | ||
| 26194 | * Gnus Cloud Setup:: | ||
| 26195 | * Gnus Cloud Usage:: | ||
| 26196 | @end menu | ||
| 26197 | |||
| 26198 | @node Gnus Cloud Setup | ||
| 26199 | @subsection Gnus Cloud Setup | ||
| 26200 | |||
| 26201 | Setting up the Gnus Cloud takes less than a minute. From the Group | ||
| 26202 | buffer: | ||
| 26203 | |||
| 26204 | Press @kbd{^} to go to the Server buffer. Here you'll see all the | ||
| 26205 | servers that Gnus knows. @xref{Server Buffer}. | ||
| 26206 | |||
| 26207 | Then press @kbd{i} to mark any servers as cloud-synchronized (their marks are synchronized). | ||
| 26208 | |||
| 26209 | Then press @kbd{I} to mark a single server as the cloud host (it must | ||
| 26210 | be an IMAP server, and will host a special IMAP folder with all the | ||
| 26211 | synchronization data). This will set the variable | ||
| 26212 | @code{gnus-cloud-method} (using the Customize facilities), then ask | ||
| 26213 | you to optionally upload your first CloudSynchronizationDataPack(TM). | ||
| 26214 | |||
| 26215 | @node Gnus Cloud Usage | ||
| 26216 | @subsection Gnus Cloud Usage | ||
| 26217 | |||
| 26218 | After setting up, you can use these shortcuts from the Group buffer: | ||
| 26219 | |||
| 26220 | @table @kbd | ||
| 26221 | @item ~ RET | ||
| 26222 | @item ~ d | ||
| 26223 | @findex gnus-cloud-download-all-data | ||
| 26224 | @cindex cloud, download | ||
| 26225 | Download the latest Gnus Cloud data. | ||
| 26226 | |||
| 26227 | @item ~ u | ||
| 26228 | @item ~ ~ | ||
| 26229 | @findex gnus-cloud-upload-all-data | ||
| 26230 | @cindex cloud, download | ||
| 26231 | Upload the local Gnus Cloud data. Creates a new | ||
| 26232 | CloudSynchronizationDataPack(TM). | ||
| 26233 | |||
| 26234 | @end table | ||
| 26235 | |||
| 26236 | But wait, there's more. Of course there's more. So much more. You can | ||
| 26237 | customize all of the following. | ||
| 26238 | |||
| 26239 | @defvar gnus-cloud-synced-files | ||
| 26240 | These are the files that will be part of every | ||
| 26241 | CloudSynchronizationDataPack(TM). They are included in every upload, | ||
| 26242 | so don't synchronize a lot of large files. Files under 100Kb are best. | ||
| 26243 | @end defvar | ||
| 26244 | |||
| 26245 | @defvar gnus-cloud-storage-method | ||
| 26246 | This is a choice from several storage methods. It's highly recommended | ||
| 26247 | to use the EPG facilities. It will be automatic if have GnuPG | ||
| 26248 | installed and EPG loaded. Otherwise, you could use Base64+gzip, | ||
| 26249 | Base64, or no encoding. | ||
| 26250 | @end defvar | ||
| 26251 | |||
| 26252 | @defvar gnus-cloud-interactive | ||
| 26253 | When this is set, and by default it is, the Gnus Cloud package will | ||
| 26254 | ask you for confirmation here and there. Leave it on until you're | ||
| 26255 | comfortable with the package. | ||
| 26256 | @end defvar | ||
| 26257 | |||
| 26258 | |||
| 26259 | @defvar gnus-cloud-method | ||
| 26260 | The name of the IMAP server to store the | ||
| 26261 | CloudSynchronizationDataPack(TM)s. It's easiest to set this from the | ||
| 26262 | Server buffer (@pxref{Gnus Cloud Setup}). | ||
| 26263 | @end defvar | ||
| 26264 | |||
| 26169 | @node Other modes | 26265 | @node Other modes |
| 26170 | @section Interaction with other modes | 26266 | @section Interaction with other modes |
| 26171 | 26267 | ||
diff --git a/lisp/gnus/gnus-cloud.el b/lisp/gnus/gnus-cloud.el index a6a0f64603d..22086b1f36e 100644 --- a/lisp/gnus/gnus-cloud.el +++ b/lisp/gnus/gnus-cloud.el | |||
| @@ -28,6 +28,12 @@ | |||
| 28 | (require 'parse-time) | 28 | (require 'parse-time) |
| 29 | (require 'nnimap) | 29 | (require 'nnimap) |
| 30 | 30 | ||
| 31 | (eval-when-compile (require 'epg)) ;; setf-method for `epg-context-armor' | ||
| 32 | (autoload 'epg-make-context "epg") | ||
| 33 | (autoload 'epg-context-set-passphrase-callback "epg") | ||
| 34 | (autoload 'epg-decrypt-string "epg") | ||
| 35 | (autoload 'epg-encrypt-string "epg") | ||
| 36 | |||
| 31 | (defgroup gnus-cloud nil | 37 | (defgroup gnus-cloud nil |
| 32 | "Syncing Gnus data via IMAP." | 38 | "Syncing Gnus data via IMAP." |
| 33 | :version "25.1" | 39 | :version "25.1" |
| @@ -43,18 +49,36 @@ | |||
| 43 | ;; FIXME this type does not match the default. Nor does the documentation. | 49 | ;; FIXME this type does not match the default. Nor does the documentation. |
| 44 | :type '(repeat regexp)) | 50 | :type '(repeat regexp)) |
| 45 | 51 | ||
| 46 | (defvar gnus-cloud-group-name "*Emacs Cloud*") | 52 | (defcustom gnus-cloud-storage-method (if (featurep 'epg) 'epg 'base64-gzip) |
| 53 | "Storage method for cloud data, defaults to EPG if that's available." | ||
| 54 | :group 'gnus-cloud | ||
| 55 | :type '(radio (const :tag "No encoding" nil) | ||
| 56 | (const :tag "Base64" base64) | ||
| 57 | (const :tag "Base64+gzip" base64-gzip) | ||
| 58 | (const :tag "EPG" epg))) | ||
| 59 | |||
| 60 | (defcustom gnus-cloud-interactive t | ||
| 61 | "Whether Gnus Cloud changes should be confirmed." | ||
| 62 | :group 'gnus-cloud | ||
| 63 | :type 'boolean) | ||
| 64 | |||
| 65 | (defvar gnus-cloud-group-name "Emacs-Cloud") | ||
| 47 | (defvar gnus-cloud-covered-servers nil) | 66 | (defvar gnus-cloud-covered-servers nil) |
| 48 | 67 | ||
| 49 | (defvar gnus-cloud-version 1) | 68 | (defvar gnus-cloud-version 1) |
| 50 | (defvar gnus-cloud-sequence 1) | 69 | (defvar gnus-cloud-sequence 1) |
| 51 | 70 | ||
| 52 | (defvar gnus-cloud-method nil | 71 | (defcustom gnus-cloud-method nil |
| 53 | "The IMAP select method used to store the cloud data.") | 72 | "The IMAP select method used to store the cloud data. |
| 73 | See also `gnus-server-toggle-cloud-method-server' for an | ||
| 74 | easy interactive way to set this from the Server buffer." | ||
| 75 | :group 'gnus-cloud | ||
| 76 | :type '(radio (const :tag "Not set" nil) | ||
| 77 | (string :tag "A Gnus server name as a string"))) | ||
| 54 | 78 | ||
| 55 | (defun gnus-cloud-make-chunk (elems) | 79 | (defun gnus-cloud-make-chunk (elems) |
| 56 | (with-temp-buffer | 80 | (with-temp-buffer |
| 57 | (insert (format "Version %s\n" gnus-cloud-version)) | 81 | (insert (format "Gnus-Cloud-Version %s\n" gnus-cloud-version)) |
| 58 | (insert (gnus-cloud-insert-data elems)) | 82 | (insert (gnus-cloud-insert-data elems)) |
| 59 | (buffer-string))) | 83 | (buffer-string))) |
| 60 | 84 | ||
| @@ -63,106 +87,187 @@ | |||
| 63 | (dolist (elem elems) | 87 | (dolist (elem elems) |
| 64 | (cond | 88 | (cond |
| 65 | ((eq (plist-get elem :type) :file) | 89 | ((eq (plist-get elem :type) :file) |
| 66 | (let (length data) | 90 | (let (length data) |
| 67 | (mm-with-unibyte-buffer | 91 | (mm-with-unibyte-buffer |
| 68 | (insert-file-contents-literally (plist-get elem :file-name)) | 92 | (insert-file-contents-literally (plist-get elem :file-name)) |
| 69 | (setq length (buffer-size) | 93 | (setq length (buffer-size) |
| 70 | data (buffer-string))) | 94 | data (buffer-string))) |
| 71 | (insert (format "(:type :file :file-name %S :timestamp %S :length %d)\n" | 95 | (insert (format "(:type :file :file-name %S :timestamp %S :length %d)\n" |
| 72 | (plist-get elem :file-name) | 96 | (plist-get elem :file-name) |
| 73 | (plist-get elem :timestamp) | 97 | (plist-get elem :timestamp) |
| 74 | length)) | 98 | length)) |
| 75 | (insert data) | 99 | (insert data) |
| 76 | (insert "\n"))) | 100 | (insert "\n"))) |
| 77 | ((eq (plist-get elem :type) :data) | 101 | ((eq (plist-get elem :type) :newsrc-data) |
| 78 | (insert (format "(:type :data :name %S :length %d)\n" | 102 | (let ((print-level nil) |
| 79 | (plist-get elem :name) | 103 | (print-length nil)) |
| 80 | (with-current-buffer (plist-get elem :buffer) | 104 | (print elem (current-buffer))) |
| 81 | (buffer-size)))) | 105 | (insert "\n")) |
| 82 | (insert-buffer-substring (plist-get elem :buffer)) | ||
| 83 | (insert "\n")) | ||
| 84 | ((eq (plist-get elem :type) :delete) | 106 | ((eq (plist-get elem :type) :delete) |
| 85 | (insert (format "(:type :delete :file-name %S)\n" | 107 | (insert (format "(:type :delete :file-name %S)\n" |
| 86 | (plist-get elem :file-name)))))) | 108 | (plist-get elem :file-name)))))) |
| 87 | (gnus-cloud-encode-data) | 109 | (gnus-cloud-encode-data) |
| 88 | (buffer-string))) | 110 | (buffer-string))) |
| 89 | 111 | ||
| 90 | (defun gnus-cloud-encode-data () | 112 | (defun gnus-cloud-encode-data () |
| 91 | (call-process-region (point-min) (point-max) "gzip" | 113 | (cond |
| 92 | t (current-buffer) nil | 114 | ((eq gnus-cloud-storage-method 'base64-gzip) |
| 93 | "-c") | 115 | (call-process-region (point-min) (point-max) "gzip" |
| 94 | (base64-encode-region (point-min) (point-max))) | 116 | t (current-buffer) nil |
| 117 | "-c")) | ||
| 118 | |||
| 119 | ((memq gnus-cloud-storage-method '(base64 base64-gzip)) | ||
| 120 | (base64-encode-region (point-min) (point-max))) | ||
| 121 | |||
| 122 | ((eq gnus-cloud-storage-method 'epg) | ||
| 123 | (let ((context (epg-make-context 'OpenPGP)) | ||
| 124 | cipher) | ||
| 125 | (setf (epg-context-armor context) t) | ||
| 126 | (setf (epg-context-textmode context) t) | ||
| 127 | (let ((data (epg-encrypt-string context | ||
| 128 | (buffer-substring-no-properties | ||
| 129 | (point-min) | ||
| 130 | (point-max)) | ||
| 131 | nil))) | ||
| 132 | (delete-region (point-min) (point-max)) | ||
| 133 | (insert data)))) | ||
| 134 | |||
| 135 | ((null gnus-cloud-storage-method) | ||
| 136 | (gnus-message 5 "Leaving cloud data plaintext")) | ||
| 137 | (t (gnus-error 1 "Invalid cloud storage method %S" | ||
| 138 | gnus-cloud-storage-method)))) | ||
| 95 | 139 | ||
| 96 | (defun gnus-cloud-decode-data () | 140 | (defun gnus-cloud-decode-data () |
| 97 | (base64-decode-region (point-min) (point-max)) | 141 | (cond |
| 98 | (call-process-region (point-min) (point-max) "gunzip" | 142 | ((memq gnus-cloud-storage-method '(base64 base64-gzip)) |
| 99 | t (current-buffer) nil | 143 | (base64-decode-region (point-min) (point-max))) |
| 100 | "-c")) | 144 | |
| 145 | ((eq gnus-cloud-storage-method 'base64-gzip) | ||
| 146 | (call-process-region (point-min) (point-max) "gunzip" | ||
| 147 | t (current-buffer) nil | ||
| 148 | "-c")) | ||
| 149 | |||
| 150 | ((eq gnus-cloud-storage-method 'epg) | ||
| 151 | (let* ((context (epg-make-context 'OpenPGP)) | ||
| 152 | (data (epg-decrypt-string context (buffer-substring-no-properties | ||
| 153 | (point-min) | ||
| 154 | (point-max))))) | ||
| 155 | (delete-region (point-min) (point-max)) | ||
| 156 | (insert data))) | ||
| 157 | |||
| 158 | ((null gnus-cloud-storage-method) | ||
| 159 | (gnus-message 5 "Reading cloud data as plaintext")) | ||
| 160 | |||
| 161 | (t (gnus-error 1 "Invalid cloud storage method %S" | ||
| 162 | gnus-cloud-storage-method)))) | ||
| 101 | 163 | ||
| 102 | (defun gnus-cloud-parse-chunk () | 164 | (defun gnus-cloud-parse-chunk () |
| 103 | (save-excursion | 165 | (save-excursion |
| 104 | (goto-char (point-min)) | 166 | (unless (looking-at "Gnus-Cloud-Version \\([0-9]+\\)") |
| 105 | (unless (looking-at "Version \\([0-9]+\\)") | ||
| 106 | (error "Not a valid Cloud chunk in the current buffer")) | 167 | (error "Not a valid Cloud chunk in the current buffer")) |
| 107 | (forward-line 1) | 168 | (forward-line 1) |
| 108 | (let ((version (string-to-number (match-string 1))) | 169 | (let ((version (string-to-number (match-string 1))) |
| 109 | (data (buffer-substring (point) (point-max)))) | 170 | (data (buffer-substring (point) (point-max)))) |
| 110 | (mm-with-unibyte-buffer | 171 | (mm-with-unibyte-buffer |
| 111 | (insert data) | 172 | (insert data) |
| 112 | (cond | 173 | (cond |
| 113 | ((= version 1) | 174 | ((= version 1) |
| 114 | (gnus-cloud-decode-data) | 175 | (gnus-cloud-decode-data) |
| 115 | (goto-char (point-min)) | 176 | (goto-char (point-min)) |
| 116 | (gnus-cloud-parse-version-1)) | 177 | (gnus-cloud-parse-version-1)) |
| 117 | (t | 178 | (t |
| 118 | (error "Unsupported Cloud chunk version %s" version))))))) | 179 | (error "Unsupported Cloud chunk version %s" version))))))) |
| 119 | 180 | ||
| 120 | (defun gnus-cloud-parse-version-1 () | 181 | (defun gnus-cloud-parse-version-1 () |
| 121 | (let ((elems nil)) | 182 | (let ((elems nil)) |
| 122 | (while (not (eobp)) | 183 | (while (not (eobp)) |
| 123 | (while (and (not (eobp)) | 184 | (while (and (not (eobp)) |
| 124 | (not (looking-at "(:type"))) | 185 | (not (looking-at "(:type"))) |
| 125 | (forward-line 1)) | 186 | (forward-line 1)) |
| 126 | (unless (eobp) | 187 | (unless (eobp) |
| 127 | (let ((spec (ignore-errors (read (current-buffer)))) | 188 | (let ((spec (ignore-errors (read (current-buffer)))) |
| 128 | length) | 189 | length) |
| 129 | (when (and (consp spec) | 190 | (when (consp spec) |
| 130 | (memq (plist-get spec :type) '(:file :data :delete))) | 191 | (cond |
| 131 | (setq length (plist-get spec :length)) | 192 | ((memq (plist-get spec :type) '(:file :delete)) |
| 132 | (push (append spec | 193 | (setq length (plist-get spec :length)) |
| 133 | (list | 194 | (push (append spec |
| 134 | :contents (buffer-substring (1+ (point)) | 195 | (list |
| 135 | (+ (point) 1 length)))) | 196 | :contents (buffer-substring (1+ (point)) |
| 136 | elems) | 197 | (+ (point) 1 length)))) |
| 137 | (goto-char (+ (point) 1 length)))))) | 198 | elems) |
| 199 | (goto-char (+ (point) 1 length))) | ||
| 200 | ((memq (plist-get spec :type) '(:newsrc-data)) | ||
| 201 | (push spec elems))))))) | ||
| 138 | (nreverse elems))) | 202 | (nreverse elems))) |
| 139 | 203 | ||
| 140 | (defun gnus-cloud-update-data (elems) | 204 | (defun gnus-cloud-update-all (elems) |
| 141 | (dolist (elem elems) | 205 | (dolist (elem elems) |
| 142 | (let ((type (plist-get elem :type))) | 206 | (let ((type (plist-get elem :type))) |
| 143 | (cond | 207 | (cond |
| 144 | ((eq type :data) | 208 | ((eq type :newsrc-data) |
| 145 | ) | 209 | (gnus-cloud-update-newsrc-data (plist-get elem :name) elem)) |
| 146 | ((eq type :delete) | 210 | ((memq type '(:delete :file)) |
| 147 | (gnus-cloud-delete-file (plist-get elem :file-name)) | 211 | (gnus-cloud-update-file elem type)) |
| 148 | ) | ||
| 149 | ((eq type :file) | ||
| 150 | (gnus-cloud-update-file elem)) | ||
| 151 | (t | 212 | (t |
| 152 | (message "Unknown type %s; ignoring" type)))))) | 213 | (gnus-message 1 "Unknown type %s; ignoring" type)))))) |
| 153 | 214 | ||
| 154 | (defun gnus-cloud-update-file (elem) | 215 | (defun gnus-cloud-update-newsrc-data (group elem &optional force-older) |
| 155 | (let ((file-name (plist-get elem :file-name)) | 216 | "Update the newsrc data for GROUP from ELEM. |
| 156 | (date (plist-get elem :timestamp)) | 217 | Use old data if FORCE-OLDER is not nil." |
| 157 | (contents (plist-get elem :contents))) | 218 | (let* ((contents (plist-get elem :contents)) |
| 158 | (unless (gnus-cloud-file-covered-p file-name) | 219 | (date (or (plist-get elem :timestamp) "0")) |
| 159 | (message "%s isn't covered by the cloud; ignoring" file-name)) | 220 | (now (gnus-cloud-timestamp (current-time))) |
| 160 | (when (or (not (file-exists-p file-name)) | 221 | (newer (string-lessp date now)) |
| 161 | (and (file-exists-p file-name) | 222 | (group-info (gnus-get-info group))) |
| 162 | (mm-with-unibyte-buffer | 223 | (if (and contents |
| 163 | (insert-file-contents-literally file-name) | 224 | (stringp (nth 0 contents)) |
| 164 | (not (equal (buffer-string) contents))))) | 225 | (integerp (nth 1 contents))) |
| 165 | (gnus-cloud-replace-file file-name date contents)))) | 226 | (if group-info |
| 227 | (if (equal (format "%S" group-info) | ||
| 228 | (format "%S" contents)) | ||
| 229 | (gnus-message 3 "Skipping cloud update of group %s, the info is the same" group) | ||
| 230 | (if (and newer (not force-older)) | ||
| 231 | (gnus-message 3 "Skipping outdated cloud info for group %s, the info is from %s (now is %s)" group date now) | ||
| 232 | (when (or (not gnus-cloud-interactive) | ||
| 233 | (gnus-y-or-n-p | ||
| 234 | (format "%s has older different info in the cloud as of %s, update it here? " | ||
| 235 | group date)))) | ||
| 236 | (gnus-message 2 "Installing cloud update of group %s" group) | ||
| 237 | (gnus-set-info group contents) | ||
| 238 | (gnus-group-update-group group))) | ||
| 239 | (gnus-error 1 "Sorry, group %s is not subscribed" group)) | ||
| 240 | (gnus-error 1 "Sorry, could not update newsrc for group %s (invalid data %S)" | ||
| 241 | group elem)))) | ||
| 242 | |||
| 243 | (defun gnus-cloud-update-file (elem op) | ||
| 244 | "Apply Gnus Cloud data ELEM and operation OP to a file." | ||
| 245 | (let* ((file-name (plist-get elem :file-name)) | ||
| 246 | (date (plist-get elem :timestamp)) | ||
| 247 | (contents (plist-get elem :contents)) | ||
| 248 | (exists (file-exists-p file-name))) | ||
| 249 | (if (gnus-cloud-file-covered-p file-name) | ||
| 250 | (cond | ||
| 251 | ((eq op :delete) | ||
| 252 | (if (and exists | ||
| 253 | ;; prompt only if the file exists already | ||
| 254 | (or (not gnus-cloud-interactive) | ||
| 255 | (gnus-y-or-n-p (format "%s has been deleted as of %s, delete it locally? " | ||
| 256 | file-name date)))) | ||
| 257 | (rename-file file-name (car (find-backup-file-name file-name))) | ||
| 258 | (gnus-message 3 "%s was already deleted before the cloud got it" file-name))) | ||
| 259 | ((eq op :file) | ||
| 260 | (when (or (not exists) | ||
| 261 | (and exists | ||
| 262 | (mm-with-unibyte-buffer | ||
| 263 | (insert-file-contents-literally file-name) | ||
| 264 | (not (equal (buffer-string) contents))) | ||
| 265 | ;; prompt only if the file exists already | ||
| 266 | (or (not gnus-cloud-interactive) | ||
| 267 | (gnus-y-or-n-p (format "%s has updated contents as of %s, update it? " | ||
| 268 | file-name date))))) | ||
| 269 | (gnus-cloud-replace-file file-name date contents)))) | ||
| 270 | (gnus-message 2 "%s isn't covered by the cloud; ignoring" file-name)))) | ||
| 166 | 271 | ||
| 167 | (defun gnus-cloud-replace-file (file-name date new-contents) | 272 | (defun gnus-cloud-replace-file (file-name date new-contents) |
| 168 | (mm-with-unibyte-buffer | 273 | (mm-with-unibyte-buffer |
| @@ -172,25 +277,19 @@ | |||
| 172 | (write-region (point-min) (point-max) file-name) | 277 | (write-region (point-min) (point-max) file-name) |
| 173 | (set-file-times file-name (parse-iso8601-time-string date)))) | 278 | (set-file-times file-name (parse-iso8601-time-string date)))) |
| 174 | 279 | ||
| 175 | (defun gnus-cloud-delete-file (file-name) | ||
| 176 | (unless (gnus-cloud-file-covered-p file-name) | ||
| 177 | (message "%s isn't covered by the cloud; ignoring" file-name)) | ||
| 178 | (when (file-exists-p file-name) | ||
| 179 | (rename-file file-name (car (find-backup-file-name file-name))))) | ||
| 180 | |||
| 181 | (defun gnus-cloud-file-covered-p (file-name) | 280 | (defun gnus-cloud-file-covered-p (file-name) |
| 182 | (let ((matched nil)) | 281 | (let ((matched nil)) |
| 183 | (dolist (elem gnus-cloud-synced-files) | 282 | (dolist (elem gnus-cloud-synced-files) |
| 184 | (cond | 283 | (cond |
| 185 | ((stringp elem) | 284 | ((stringp elem) |
| 186 | (when (equal elem file-name) | 285 | (when (equal elem file-name) |
| 187 | (setq matched t))) | 286 | (setq matched t))) |
| 188 | ((consp elem) | 287 | ((consp elem) |
| 189 | (when (and (equal (directory-file-name (plist-get elem :directory)) | 288 | (when (and (equal (directory-file-name (plist-get elem :directory)) |
| 190 | (directory-file-name (file-name-directory file-name))) | 289 | (directory-file-name (file-name-directory file-name))) |
| 191 | (string-match (plist-get elem :match) | 290 | (string-match (plist-get elem :match) |
| 192 | (file-name-nondirectory file-name))) | 291 | (file-name-nondirectory file-name))) |
| 193 | (setq matched t))))) | 292 | (setq matched t))))) |
| 194 | matched)) | 293 | matched)) |
| 195 | 294 | ||
| 196 | (defun gnus-cloud-all-files () | 295 | (defun gnus-cloud-all-files () |
| @@ -198,106 +297,126 @@ | |||
| 198 | (dolist (elem gnus-cloud-synced-files) | 297 | (dolist (elem gnus-cloud-synced-files) |
| 199 | (cond | 298 | (cond |
| 200 | ((stringp elem) | 299 | ((stringp elem) |
| 201 | (push elem files)) | 300 | (push elem files)) |
| 202 | ((consp elem) | 301 | ((consp elem) |
| 203 | (dolist (file (directory-files (plist-get elem :directory) | 302 | (dolist (file (directory-files (plist-get elem :directory) |
| 204 | nil | 303 | nil |
| 205 | (plist-get elem :match))) | 304 | (plist-get elem :match))) |
| 206 | (push (format "%s/%s" | 305 | (push (format "%s/%s" |
| 207 | (directory-file-name (plist-get elem :directory)) | 306 | (directory-file-name (plist-get elem :directory)) |
| 208 | file) | 307 | file) |
| 209 | files))))) | 308 | files))))) |
| 210 | (nreverse files))) | 309 | (nreverse files))) |
| 211 | 310 | ||
| 212 | (defvar gnus-cloud-file-timestamps nil) | 311 | (defvar gnus-cloud-file-timestamps nil) |
| 213 | 312 | ||
| 214 | (defun gnus-cloud-files-to-upload (&optional full) | 313 | (defun gnus-cloud-files-to-upload (&optional full) |
| 215 | (let ((files nil) | 314 | (let ((files nil) |
| 216 | timestamp) | 315 | timestamp) |
| 217 | (dolist (file (gnus-cloud-all-files)) | 316 | (dolist (file (gnus-cloud-all-files)) |
| 218 | (if (file-exists-p file) | 317 | (if (file-exists-p file) |
| 219 | (when (setq timestamp (gnus-cloud-file-new-p file full)) | 318 | (when (setq timestamp (gnus-cloud-file-new-p file full)) |
| 220 | (push `(:type :file :file-name ,file :timestamp ,timestamp) files)) | 319 | (push `(:type :file :file-name ,file :timestamp ,timestamp) files)) |
| 221 | (when (assoc file gnus-cloud-file-timestamps) | 320 | (when (assoc file gnus-cloud-file-timestamps) |
| 222 | (push `(:type :delete :file-name ,file) files)))) | 321 | (push `(:type :delete :file-name ,file) files)))) |
| 223 | (nreverse files))) | 322 | (nreverse files))) |
| 224 | 323 | ||
| 324 | (defun gnus-cloud-timestamp (time) | ||
| 325 | "Return a general timestamp string for TIME." | ||
| 326 | (format-time-string "%FT%T%z" time)) | ||
| 327 | |||
| 225 | (defun gnus-cloud-file-new-p (file full) | 328 | (defun gnus-cloud-file-new-p (file full) |
| 226 | (let ((timestamp (format-time-string | 329 | (let ((timestamp (gnus-cloud-timestamp (nth 5 (file-attributes file)))) |
| 227 | "%FT%T%z" (nth 5 (file-attributes file)))) | 330 | (old (cadr (assoc file gnus-cloud-file-timestamps)))) |
| 228 | (old (cadr (assoc file gnus-cloud-file-timestamps)))) | ||
| 229 | (when (or full | 331 | (when (or full |
| 230 | (null old) | 332 | (null old) |
| 231 | (string< old timestamp)) | 333 | (string< old timestamp)) |
| 232 | timestamp))) | 334 | timestamp))) |
| 233 | 335 | ||
| 234 | (declare-function gnus-activate-group "gnus-start" | 336 | (declare-function gnus-activate-group "gnus-start" |
| 235 | (group &optional scan dont-check method dont-sub-check)) | 337 | (group &optional scan dont-check method dont-sub-check)) |
| 236 | (declare-function gnus-subscribe-group "gnus-start" | 338 | (declare-function gnus-subscribe-group "gnus-start" |
| 237 | (group &optional previous method)) | 339 | (group &optional previous method)) |
| 238 | 340 | ||
| 239 | (defun gnus-cloud-ensure-cloud-group () | 341 | (defun gnus-cloud-ensure-cloud-group () |
| 240 | (let ((method (if (stringp gnus-cloud-method) | 342 | (let ((method (if (stringp gnus-cloud-method) |
| 241 | (gnus-server-to-method gnus-cloud-method) | 343 | (gnus-server-to-method gnus-cloud-method) |
| 242 | gnus-cloud-method))) | 344 | gnus-cloud-method))) |
| 243 | (unless (or (gnus-active gnus-cloud-group-name) | 345 | (unless (or (gnus-active gnus-cloud-group-name) |
| 244 | (gnus-activate-group gnus-cloud-group-name nil nil | 346 | (gnus-activate-group gnus-cloud-group-name nil nil |
| 245 | gnus-cloud-method)) | 347 | gnus-cloud-method)) |
| 246 | (and (gnus-request-create-group gnus-cloud-group-name gnus-cloud-method) | 348 | (and (gnus-request-create-group gnus-cloud-group-name gnus-cloud-method) |
| 247 | (gnus-activate-group gnus-cloud-group-name nil nil gnus-cloud-method) | 349 | (gnus-activate-group gnus-cloud-group-name nil nil gnus-cloud-method) |
| 248 | (gnus-subscribe-group gnus-cloud-group-name))))) | 350 | (gnus-subscribe-group gnus-cloud-group-name))))) |
| 351 | |||
| 352 | (defun gnus-cloud-upload-all-data () | ||
| 353 | "Upload all data (newsrc and files) to the Gnus Cloud." | ||
| 354 | (interactive) | ||
| 355 | (gnus-cloud-upload-data t)) | ||
| 249 | 356 | ||
| 250 | (defun gnus-cloud-upload-data (&optional full) | 357 | (defun gnus-cloud-upload-data (&optional full) |
| 358 | "Upload data (newsrc and files) to the Gnus Cloud. | ||
| 359 | When FULL is t, upload everything, not just a difference from the last full." | ||
| 360 | (interactive) | ||
| 251 | (gnus-cloud-ensure-cloud-group) | 361 | (gnus-cloud-ensure-cloud-group) |
| 252 | (with-temp-buffer | 362 | (with-temp-buffer |
| 253 | (let ((elems (gnus-cloud-files-to-upload full))) | 363 | (let ((elems (append |
| 254 | (insert (format "Subject: (sequence: %d type: %s)\n" | 364 | (gnus-cloud-files-to-upload full) |
| 255 | gnus-cloud-sequence | 365 | (gnus-cloud-collect-full-newsrc))) |
| 256 | (if full :full :partial))) | 366 | (group (gnus-group-full-name gnus-cloud-group-name gnus-cloud-method))) |
| 257 | (insert "From: nobody@invalid.com\n") | 367 | (insert (format "Subject: (sequence: %s type: %s storage-method: %s)\n" |
| 368 | (or gnus-cloud-sequence "UNKNOWN") | ||
| 369 | (if full :full :partial) | ||
| 370 | gnus-cloud-storage-method)) | ||
| 371 | (insert "From: nobody@gnus.cloud.invalid\n") | ||
| 258 | (insert "\n") | 372 | (insert "\n") |
| 259 | (insert (gnus-cloud-make-chunk elems)) | 373 | (insert (gnus-cloud-make-chunk elems)) |
| 260 | (when (gnus-request-accept-article gnus-cloud-group-name gnus-cloud-method | 374 | (if (gnus-request-accept-article gnus-cloud-group-name gnus-cloud-method |
| 261 | t t) | 375 | t t) |
| 262 | (setq gnus-cloud-sequence (1+ gnus-cloud-sequence)) | 376 | (progn |
| 263 | (gnus-cloud-add-timestamps elems))))) | 377 | (setq gnus-cloud-sequence (1+ (or gnus-cloud-sequence 0))) |
| 378 | (gnus-cloud-add-timestamps elems) | ||
| 379 | (gnus-message 3 "Uploaded Gnus Cloud data successfully to %s" group) | ||
| 380 | (gnus-group-refresh-group group)) | ||
| 381 | (gnus-error 2 "Failed to upload Gnus Cloud data to %s" group))))) | ||
| 264 | 382 | ||
| 265 | (defun gnus-cloud-add-timestamps (elems) | 383 | (defun gnus-cloud-add-timestamps (elems) |
| 266 | (dolist (elem elems) | 384 | (dolist (elem elems) |
| 267 | (let* ((file-name (plist-get elem :file-name)) | 385 | (let* ((file-name (plist-get elem :file-name)) |
| 268 | (old (assoc file-name gnus-cloud-file-timestamps))) | 386 | (old (assoc file-name gnus-cloud-file-timestamps))) |
| 269 | (when old | 387 | (when old |
| 270 | (setq gnus-cloud-file-timestamps | 388 | (setq gnus-cloud-file-timestamps |
| 271 | (delq old gnus-cloud-file-timestamps))) | 389 | (delq old gnus-cloud-file-timestamps))) |
| 272 | (push (list file-name (plist-get elem :timestamp)) | 390 | (push (list file-name (plist-get elem :timestamp)) |
| 273 | gnus-cloud-file-timestamps)))) | 391 | gnus-cloud-file-timestamps)))) |
| 274 | 392 | ||
| 275 | (defun gnus-cloud-available-chunks () | 393 | (defun gnus-cloud-available-chunks () |
| 276 | (gnus-activate-group gnus-cloud-group-name nil nil gnus-cloud-method) | 394 | (gnus-activate-group gnus-cloud-group-name nil nil gnus-cloud-method) |
| 277 | (let* ((group (gnus-group-full-name gnus-cloud-group-name gnus-cloud-method)) | 395 | (let* ((group (gnus-group-full-name gnus-cloud-group-name gnus-cloud-method)) |
| 278 | (active (gnus-active group)) | 396 | (active (gnus-active group)) |
| 279 | headers head) | 397 | headers head) |
| 280 | (when (gnus-retrieve-headers (gnus-uncompress-range active) group) | 398 | (when (gnus-retrieve-headers (gnus-uncompress-range active) group) |
| 281 | (with-current-buffer nntp-server-buffer | 399 | (with-current-buffer nntp-server-buffer |
| 282 | (goto-char (point-min)) | 400 | (goto-char (point-min)) |
| 283 | (while (and (not (eobp)) | 401 | (while (and (not (eobp)) |
| 284 | (setq head (nnheader-parse-head))) | 402 | (setq head (nnheader-parse-head))) |
| 285 | (push head headers)))) | 403 | (push head headers)))) |
| 286 | (sort (nreverse headers) | 404 | (sort (nreverse headers) |
| 287 | (lambda (h1 h2) | 405 | (lambda (h1 h2) |
| 288 | (> (gnus-cloud-chunk-sequence (mail-header-subject h1)) | 406 | (> (gnus-cloud-chunk-sequence (mail-header-subject h1)) |
| 289 | (gnus-cloud-chunk-sequence (mail-header-subject h2))))))) | 407 | (gnus-cloud-chunk-sequence (mail-header-subject h2))))))) |
| 290 | 408 | ||
| 291 | (defun gnus-cloud-chunk-sequence (string) | 409 | (defun gnus-cloud-chunk-sequence (string) |
| 292 | (if (string-match "sequence: \\([0-9]+\\)" string) | 410 | (if (string-match "sequence: \\([0-9]+\\)" string) |
| 293 | (string-to-number (match-string 1 string)) | 411 | (string-to-number (match-string 1 string)) |
| 294 | 0)) | 412 | 0)) |
| 295 | 413 | ||
| 414 | ;; TODO: use this | ||
| 296 | (defun gnus-cloud-prune-old-chunks (headers) | 415 | (defun gnus-cloud-prune-old-chunks (headers) |
| 297 | (let ((headers (reverse headers)) | 416 | (let ((headers (reverse headers)) |
| 298 | (found nil)) | 417 | (found nil)) |
| 299 | (while (and headers | 418 | (while (and headers |
| 300 | (not found)) | 419 | (not found)) |
| 301 | (when (string-match "type: :full" (mail-header-subject (car headers))) | 420 | (when (string-match "type: :full" (mail-header-subject (car headers))) |
| 302 | (setq found t)) | 421 | (setq found t)) |
| 303 | (pop headers)) | 422 | (pop headers)) |
| @@ -306,37 +425,68 @@ | |||
| 306 | (when headers | 425 | (when headers |
| 307 | (gnus-request-expire-articles | 426 | (gnus-request-expire-articles |
| 308 | (mapcar (lambda (h) | 427 | (mapcar (lambda (h) |
| 309 | (mail-header-number h)) | 428 | (mail-header-number h)) |
| 310 | (nreverse headers)) | 429 | (nreverse headers)) |
| 311 | (gnus-group-full-name gnus-cloud-group-name gnus-cloud-method))))) | 430 | (gnus-group-full-name gnus-cloud-group-name gnus-cloud-method))))) |
| 312 | 431 | ||
| 313 | (defun gnus-cloud-download-data () | 432 | (defun gnus-cloud-download-all-data () |
| 433 | "Download the Gnus Cloud data and install it. | ||
| 434 | Starts at `gnus-cloud-sequence' in the sequence." | ||
| 435 | (interactive) | ||
| 436 | (gnus-cloud-download-data t)) | ||
| 437 | |||
| 438 | (defun gnus-cloud-download-data (&optional update sequence-override) | ||
| 439 | "Download the Gnus Cloud data and install it if UPDATE is t. | ||
| 440 | When SEQUENCE-OVERRIDE is given, start at that sequence number | ||
| 441 | instead of `gnus-cloud-sequence'. | ||
| 442 | |||
| 443 | When UPDATE is t, returns the result of calling `gnus-cloud-update-all'. | ||
| 444 | Otherwise, returns the Gnus Cloud data chunks." | ||
| 314 | (let ((articles nil) | 445 | (let ((articles nil) |
| 315 | chunks) | 446 | chunks) |
| 316 | (dolist (header (gnus-cloud-available-chunks)) | 447 | (dolist (header (gnus-cloud-available-chunks)) |
| 317 | (when (> (gnus-cloud-chunk-sequence (mail-header-subject header)) | 448 | (when (> (gnus-cloud-chunk-sequence (mail-header-subject header)) |
| 318 | gnus-cloud-sequence) | 449 | (or sequence-override gnus-cloud-sequence -1)) |
| 319 | (push (mail-header-number header) articles))) | 450 | |
| 451 | (if (string-match (format "storage-method: %s" gnus-cloud-storage-method) | ||
| 452 | (mail-header-subject header)) | ||
| 453 | (push (mail-header-number header) articles) | ||
| 454 | (gnus-message 1 "Skipping article %s because it didn't match the Gnus Cloud method %s: %s" | ||
| 455 | (mail-header-number header) | ||
| 456 | gnus-cloud-storage-method | ||
| 457 | (mail-header-subject header))))) | ||
| 320 | (when articles | 458 | (when articles |
| 321 | (nnimap-request-articles (nreverse articles) gnus-cloud-group-name) | 459 | (nnimap-request-articles (nreverse articles) gnus-cloud-group-name) |
| 322 | (with-current-buffer nntp-server-buffer | 460 | (with-current-buffer nntp-server-buffer |
| 323 | (goto-char (point-min)) | 461 | (goto-char (point-min)) |
| 324 | (while (re-search-forward "^Version " nil t) | 462 | (while (re-search-forward "^Gnus-Cloud-Version " nil t) |
| 325 | (beginning-of-line) | 463 | (beginning-of-line) |
| 326 | (push (gnus-cloud-parse-chunk) chunks) | 464 | (push (gnus-cloud-parse-chunk) chunks) |
| 327 | (forward-line 1)))))) | 465 | (forward-line 1)))) |
| 466 | (if update | ||
| 467 | (mapcar #'gnus-cloud-update-all chunks) | ||
| 468 | chunks))) | ||
| 328 | 469 | ||
| 329 | (defun gnus-cloud-server-p (server) | 470 | (defun gnus-cloud-server-p (server) |
| 330 | (member server gnus-cloud-covered-servers)) | 471 | (member server gnus-cloud-covered-servers)) |
| 331 | 472 | ||
| 473 | (defun gnus-cloud-host-server-p (server) | ||
| 474 | (equal gnus-cloud-method server)) | ||
| 475 | |||
| 476 | (defun gnus-cloud-host-acceptable-method-p (server) | ||
| 477 | (eq (car-safe (gnus-server-to-method server)) 'nnimap)) | ||
| 478 | |||
| 332 | (defun gnus-cloud-collect-full-newsrc () | 479 | (defun gnus-cloud-collect-full-newsrc () |
| 480 | "Collect all the Gnus newsrc data in a portable format." | ||
| 333 | (let ((infos nil)) | 481 | (let ((infos nil)) |
| 334 | (dolist (info (cdr gnus-newsrc-alist)) | 482 | (dolist (info (cdr gnus-newsrc-alist)) |
| 335 | (when (gnus-cloud-server-p | 483 | (when (gnus-cloud-server-p |
| 336 | (gnus-method-to-server | 484 | (gnus-method-to-server |
| 337 | (gnus-find-method-for-group (gnus-info-group info)))) | 485 | (gnus-find-method-for-group (gnus-info-group info)))) |
| 338 | (push info infos))) | 486 | |
| 339 | )) | 487 | (push `(:type :newsrc-data :name ,(gnus-info-group info) :contents ,info :timestamp ,(gnus-cloud-timestamp (current-time))) |
| 488 | infos))) | ||
| 489 | infos)) | ||
| 340 | 490 | ||
| 341 | (provide 'gnus-cloud) | 491 | (provide 'gnus-cloud) |
| 342 | 492 | ||
diff --git a/lisp/gnus/gnus-group.el b/lisp/gnus/gnus-group.el index 813d9b6ced5..828805384ca 100644 --- a/lisp/gnus/gnus-group.el +++ b/lisp/gnus/gnus-group.el | |||
| @@ -51,6 +51,9 @@ | |||
| 51 | 51 | ||
| 52 | (autoload 'gnus-group-make-nnir-group "nnir") | 52 | (autoload 'gnus-group-make-nnir-group "nnir") |
| 53 | 53 | ||
| 54 | (autoload 'gnus-cloud-upload-all-data "gnus-cloud") | ||
| 55 | (autoload 'gnus-cloud-download-all-data "gnus-cloud") | ||
| 56 | |||
| 54 | (defcustom gnus-no-groups-message "No news is good news" | 57 | (defcustom gnus-no-groups-message "No news is good news" |
| 55 | "Message displayed by Gnus when no groups are available." | 58 | "Message displayed by Gnus when no groups are available." |
| 56 | :group 'gnus-start | 59 | :group 'gnus-start |
| @@ -636,6 +639,12 @@ simple manner." | |||
| 636 | "#" gnus-group-mark-group | 639 | "#" gnus-group-mark-group |
| 637 | "\M-#" gnus-group-unmark-group) | 640 | "\M-#" gnus-group-unmark-group) |
| 638 | 641 | ||
| 642 | (gnus-define-keys (gnus-group-cloud-map "~" gnus-group-mode-map) | ||
| 643 | "u" gnus-cloud-upload-all-data | ||
| 644 | "~" gnus-cloud-upload-all-data | ||
| 645 | "d" gnus-cloud-download-all-data | ||
| 646 | "\r" gnus-cloud-download-all-data) | ||
| 647 | |||
| 639 | (gnus-define-keys (gnus-group-mark-map "M" gnus-group-mode-map) | 648 | (gnus-define-keys (gnus-group-mark-map "M" gnus-group-mode-map) |
| 640 | "m" gnus-group-mark-group | 649 | "m" gnus-group-mark-group |
| 641 | "u" gnus-group-unmark-group | 650 | "u" gnus-group-unmark-group |
diff --git a/lisp/gnus/gnus-srvr.el b/lisp/gnus/gnus-srvr.el index aa76a5f35f5..66fb9ee1b59 100644 --- a/lisp/gnus/gnus-srvr.el +++ b/lisp/gnus/gnus-srvr.el | |||
| @@ -32,6 +32,7 @@ | |||
| 32 | (require 'gnus-group) | 32 | (require 'gnus-group) |
| 33 | (require 'gnus-int) | 33 | (require 'gnus-int) |
| 34 | (require 'gnus-range) | 34 | (require 'gnus-range) |
| 35 | (require 'gnus-cloud) | ||
| 35 | 36 | ||
| 36 | (autoload 'gnus-group-make-nnir-group "nnir") | 37 | (autoload 'gnus-group-make-nnir-group "nnir") |
| 37 | 38 | ||
| @@ -140,7 +141,8 @@ If nil, a faster, but more primitive, buffer is used instead." | |||
| 140 | ["Close" gnus-server-close-server t] | 141 | ["Close" gnus-server-close-server t] |
| 141 | ["Offline" gnus-server-offline-server t] | 142 | ["Offline" gnus-server-offline-server t] |
| 142 | ["Deny" gnus-server-deny-server t] | 143 | ["Deny" gnus-server-deny-server t] |
| 143 | ["Toggle Cloud" gnus-server-toggle-cloud-server t] | 144 | ["Toggle Cloud Sync for this server" gnus-server-toggle-cloud-server t] |
| 145 | ["Toggle Cloud Sync Host" gnus-server-toggle-cloud-method-server t] | ||
| 144 | "---" | 146 | "---" |
| 145 | ["Open All" gnus-server-open-all-servers t] | 147 | ["Open All" gnus-server-open-all-servers t] |
| 146 | ["Close All" gnus-server-close-all-servers t] | 148 | ["Close All" gnus-server-close-all-servers t] |
| @@ -187,6 +189,7 @@ If nil, a faster, but more primitive, buffer is used instead." | |||
| 187 | "z" gnus-server-compact-server | 189 | "z" gnus-server-compact-server |
| 188 | 190 | ||
| 189 | "i" gnus-server-toggle-cloud-server | 191 | "i" gnus-server-toggle-cloud-server |
| 192 | "I" gnus-server-toggle-cloud-method-server | ||
| 190 | 193 | ||
| 191 | "\C-c\C-i" gnus-info-find-node | 194 | "\C-c\C-i" gnus-info-find-node |
| 192 | "\C-c\C-b" gnus-bug)) | 195 | "\C-c\C-b" gnus-bug)) |
| @@ -205,7 +208,14 @@ If nil, a faster, but more primitive, buffer is used instead." | |||
| 205 | '((((class color) (background light)) (:foreground "ForestGreen" :bold t)) | 208 | '((((class color) (background light)) (:foreground "ForestGreen" :bold t)) |
| 206 | (((class color) (background dark)) (:foreground "PaleGreen" :bold t)) | 209 | (((class color) (background dark)) (:foreground "PaleGreen" :bold t)) |
| 207 | (t (:bold t))) | 210 | (t (:bold t))) |
| 208 | "Face used for displaying AGENTIZED servers" | 211 | "Face used for displaying Cloud-synced servers" |
| 212 | :group 'gnus-server-visual) | ||
| 213 | |||
| 214 | (defface gnus-server-cloud-host | ||
| 215 | '((((class color) (background light)) (:foreground "ForestGreen" :inverse-video t :italic t)) | ||
| 216 | (((class color) (background dark)) (:foreground "PaleGreen" :inverse-video t :italic t)) | ||
| 217 | (t (:inverse-video t :italic t))) | ||
| 218 | "Face used for displaying the Cloud Host" | ||
| 209 | :group 'gnus-server-visual) | 219 | :group 'gnus-server-visual) |
| 210 | 220 | ||
| 211 | (defface gnus-server-opened | 221 | (defface gnus-server-opened |
| @@ -251,7 +261,8 @@ If nil, a faster, but more primitive, buffer is used instead." | |||
| 251 | 261 | ||
| 252 | (defvar gnus-server-font-lock-keywords | 262 | (defvar gnus-server-font-lock-keywords |
| 253 | '(("(\\(agent\\))" 1 'gnus-server-agent) | 263 | '(("(\\(agent\\))" 1 'gnus-server-agent) |
| 254 | ("(\\(cloud\\))" 1 'gnus-server-cloud) | 264 | ("(\\(cloud[-]sync\\))" 1 'gnus-server-cloud) |
| 265 | ("(\\(CLOUD[-]HOST\\))" 1 'gnus-server-cloud-host) | ||
| 255 | ("(\\(opened\\))" 1 'gnus-server-opened) | 266 | ("(\\(opened\\))" 1 'gnus-server-opened) |
| 256 | ("(\\(closed\\))" 1 'gnus-server-closed) | 267 | ("(\\(closed\\))" 1 'gnus-server-closed) |
| 257 | ("(\\(offline\\))" 1 'gnus-server-offline) | 268 | ("(\\(offline\\))" 1 'gnus-server-offline) |
| @@ -306,9 +317,13 @@ The following commands are available: | |||
| 306 | (gnus-agent-method-p method)) | 317 | (gnus-agent-method-p method)) |
| 307 | " (agent)" | 318 | " (agent)" |
| 308 | "")) | 319 | "")) |
| 309 | (gnus-tmp-cloud (if (gnus-cloud-server-p gnus-tmp-name) | 320 | (gnus-tmp-cloud (concat |
| 310 | " (cloud)" | 321 | (if (gnus-cloud-host-server-p gnus-tmp-name) |
| 311 | ""))) | 322 | " (CLOUD-HOST)" |
| 323 | "") | ||
| 324 | (if (gnus-cloud-server-p gnus-tmp-name) | ||
| 325 | " (cloud-sync)" | ||
| 326 | "")))) | ||
| 312 | (beginning-of-line) | 327 | (beginning-of-line) |
| 313 | (add-text-properties | 328 | (add-text-properties |
| 314 | (point) | 329 | (point) |
| @@ -1132,6 +1147,20 @@ Requesting compaction of %s... (this may take a long time)" | |||
| 1132 | "Replication of %s in the cloud will stop") | 1147 | "Replication of %s in the cloud will stop") |
| 1133 | server))) | 1148 | server))) |
| 1134 | 1149 | ||
| 1150 | (defun gnus-server-toggle-cloud-method-server () | ||
| 1151 | "Set the server under point to host the Emacs Cloud." | ||
| 1152 | (interactive) | ||
| 1153 | (let ((server (gnus-server-server-name))) | ||
| 1154 | (unless server | ||
| 1155 | (error "No server on the current line")) | ||
| 1156 | (unless (gnus-cloud-host-acceptable-method-p server) | ||
| 1157 | (error "The server under point can't host the Emacs Cloud")) | ||
| 1158 | |||
| 1159 | (custom-set-variables '(gnus-cloud-method server)) | ||
| 1160 | (when (gnus-yes-or-no-p (format "Upload Cloud data to %S now? " server)) | ||
| 1161 | (gnus-message 1 "Uploading all data to Emacs Cloud server %S" server) | ||
| 1162 | (gnus-cloud-upload-data t)))) | ||
| 1163 | |||
| 1135 | (provide 'gnus-srvr) | 1164 | (provide 'gnus-srvr) |
| 1136 | 1165 | ||
| 1137 | ;;; gnus-srvr.el ends here | 1166 | ;;; gnus-srvr.el ends here |
diff --git a/lisp/gnus/gnus-sync.el b/lisp/gnus/gnus-sync.el deleted file mode 100644 index 249eb087b0b..00000000000 --- a/lisp/gnus/gnus-sync.el +++ /dev/null | |||
| @@ -1,896 +0,0 @@ | |||
| 1 | ;;; gnus-sync.el --- synchronization facility for Gnus | ||
| 2 | |||
| 3 | ;; Copyright (C) 2010-2016 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: Ted Zlatanov <tzz@lifelogs.com> | ||
| 6 | ;; Keywords: news synchronization nntp nnrss | ||
| 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 | ;; This is the gnus-sync.el package. | ||
| 26 | |||
| 27 | ;; Put this in your startup file (~/.gnus.el for instance) | ||
| 28 | |||
| 29 | ;; possibilities for gnus-sync-backend: | ||
| 30 | ;; Tramp over SSH: /ssh:user@host:/path/to/filename | ||
| 31 | ;; ...or any other file Tramp and Emacs can handle... | ||
| 32 | |||
| 33 | ;; (setq gnus-sync-backend "/remote:/path.gpg" ; will use Tramp+EPA if loaded | ||
| 34 | ;; gnus-sync-global-vars '(gnus-newsrc-last-checked-date) | ||
| 35 | ;; gnus-sync-newsrc-groups '("nntp" "nnrss")) | ||
| 36 | ;; gnus-sync-newsrc-offsets '(2 3)) | ||
| 37 | ;; against a LeSync server (beware the vampire LeSync, who knows your newsrc) | ||
| 38 | |||
| 39 | ;; (setq gnus-sync-backend '(lesync "http://lesync.info:5984/tzz") | ||
| 40 | ;; gnus-sync-newsrc-groups '("nntp" "nnrss")) | ||
| 41 | |||
| 42 | ;; What's a LeSync server? | ||
| 43 | |||
| 44 | ;; 1. install CouchDB, set up a real server admin user, and create a | ||
| 45 | ;; database, e.g. "tzz" and save the URL, | ||
| 46 | ;; e.g. http://lesync.info:5984/tzz | ||
| 47 | |||
| 48 | ;; 2. run `M-: (gnus-sync-lesync-setup "http://lesync.info:5984/tzz" "tzzadmin" "mypassword" "mysalt" t t)' | ||
| 49 | |||
| 50 | ;; (If you run it more than once, you have to remove the entry from | ||
| 51 | ;; _users yourself. This is intentional. This sets up a database | ||
| 52 | ;; admin for the "tzz" database, distinct from the server admin | ||
| 53 | ;; user in (1) above.) | ||
| 54 | |||
| 55 | ;; That's it, you can start using http://lesync.info:5984/tzz in your | ||
| 56 | ;; gnus-sync-backend as a LeSync backend. Fan fiction about the | ||
| 57 | ;; vampire LeSync is welcome. | ||
| 58 | |||
| 59 | ;; You may not want to expose a CouchDB install to the Big Bad | ||
| 60 | ;; Internet, especially if your love of all things furry would be thus | ||
| 61 | ;; revealed. Make sure it's not accessible by unauthorized users and | ||
| 62 | ;; guests, at least. | ||
| 63 | |||
| 64 | ;; If you want to try it out, I will create a test DB for you under | ||
| 65 | ;; http://lesync.info:5984/yourfavoritedbname | ||
| 66 | |||
| 67 | ;; TODO: | ||
| 68 | |||
| 69 | ;; - after gnus-sync-read, the message counts look wrong until you do | ||
| 70 | ;; `g'. So it's not run automatically, you have to call it with M-x | ||
| 71 | ;; gnus-sync-read | ||
| 72 | |||
| 73 | ;; - use gnus-after-set-mark-hook and gnus-before-update-mark-hook to | ||
| 74 | ;; catch the mark updates | ||
| 75 | |||
| 76 | ;; - repositioning of groups within topic after a LeSync sync is a | ||
| 77 | ;; weird sort of bubble sort ("buttle" sort: the old entry ends up | ||
| 78 | ;; at the rear of the list); you will eventually end up with the | ||
| 79 | ;; right order after calling `gnus-sync-read' a bunch of times. | ||
| 80 | |||
| 81 | ;; - installing topics and groups is inefficient and annoying, lots of | ||
| 82 | ;; prompts could be avoided | ||
| 83 | |||
| 84 | ;;; Code: | ||
| 85 | |||
| 86 | (eval-when-compile (require 'cl)) | ||
| 87 | (require 'json) | ||
| 88 | (require 'gnus) | ||
| 89 | (require 'gnus-start) | ||
| 90 | (require 'gnus-util) | ||
| 91 | |||
| 92 | (defvar gnus-topic-alist) ;; gnus-group.el | ||
| 93 | (autoload 'gnus-group-topic "gnus-topic") | ||
| 94 | |||
| 95 | (defgroup gnus-sync nil | ||
| 96 | "The Gnus synchronization facility." | ||
| 97 | :version "24.1" | ||
| 98 | :group 'gnus) | ||
| 99 | |||
| 100 | (defcustom gnus-sync-newsrc-groups '("nntp" "nnrss") | ||
| 101 | "List of groups to be synchronized in the gnus-newsrc-alist. | ||
| 102 | The group names are matched, they don't have to be fully | ||
| 103 | qualified. Typically you would choose all of these. That's the | ||
| 104 | default because there is no active sync backend by default, so | ||
| 105 | this setting is harmless until the user chooses a sync backend." | ||
| 106 | :group 'gnus-sync | ||
| 107 | :type '(repeat regexp)) | ||
| 108 | |||
| 109 | (defcustom gnus-sync-newsrc-offsets '(2 3) | ||
| 110 | "List of per-group data to be synchronized." | ||
| 111 | :group 'gnus-sync | ||
| 112 | :version "24.4" | ||
| 113 | :type '(set (const :tag "Read ranges" 2) | ||
| 114 | (const :tag "Marks" 3))) | ||
| 115 | |||
| 116 | (defcustom gnus-sync-global-vars nil | ||
| 117 | "List of global variables to be synchronized. | ||
| 118 | You may want to sync `gnus-newsrc-last-checked-date' but pretty | ||
| 119 | much any symbol is fair game. You could additionally sync | ||
| 120 | `gnus-newsrc-alist', `gnus-server-alist', `gnus-topic-topology', | ||
| 121 | and `gnus-topic-alist'. Also see `gnus-variable-list'." | ||
| 122 | :group 'gnus-sync | ||
| 123 | :type '(repeat (choice (variable :tag "A known variable") | ||
| 124 | (symbol :tag "Any symbol")))) | ||
| 125 | |||
| 126 | (defcustom gnus-sync-backend nil | ||
| 127 | "The synchronization backend." | ||
| 128 | :group 'gnus-sync | ||
| 129 | :type '(radio (const :format "None" nil) | ||
| 130 | (list :tag "Sync server" | ||
| 131 | (const :format "LeSync Server API" lesync) | ||
| 132 | (string :tag "URL of a CouchDB database for API access")) | ||
| 133 | (string :tag "Sync to a file"))) | ||
| 134 | |||
| 135 | (defvar gnus-sync-newsrc-loader nil | ||
| 136 | "Carrier for newsrc data") | ||
| 137 | |||
| 138 | (defcustom gnus-sync-file-encrypt-to nil | ||
| 139 | "If non-nil, set `epa-file-encrypt-to' from this for encrypting the Sync file." | ||
| 140 | :version "24.4" | ||
| 141 | :type '(choice string (repeat string)) | ||
| 142 | :group 'gnus-sync) | ||
| 143 | |||
| 144 | (defcustom gnus-sync-lesync-name (system-name) | ||
| 145 | "The LeSync name for this machine." | ||
| 146 | :group 'gnus-sync | ||
| 147 | :version "24.3" | ||
| 148 | :type 'string) | ||
| 149 | |||
| 150 | (defcustom gnus-sync-lesync-install-topics 'ask | ||
| 151 | "Should LeSync install the recorded topics?" | ||
| 152 | :group 'gnus-sync | ||
| 153 | :version "24.3" | ||
| 154 | :type '(choice (const :tag "Never Install" nil) | ||
| 155 | (const :tag "Always Install" t) | ||
| 156 | (const :tag "Ask Me Once" ask))) | ||
| 157 | |||
| 158 | (defvar gnus-sync-lesync-props-hash (make-hash-table :test 'equal) | ||
| 159 | "LeSync props, keyed by group name") | ||
| 160 | |||
| 161 | (defvar gnus-sync-lesync-design-prefix "/_design/lesync" | ||
| 162 | "The LeSync design prefix for CouchDB") | ||
| 163 | |||
| 164 | (defvar gnus-sync-lesync-security-object "/_security" | ||
| 165 | "The LeSync security object for CouchDB") | ||
| 166 | |||
| 167 | (defun gnus-sync-lesync-parse () | ||
| 168 | "Parse the result of a LeSync request." | ||
| 169 | (goto-char (point-min)) | ||
| 170 | (condition-case nil | ||
| 171 | (when (search-forward-regexp "^$" nil t) | ||
| 172 | (json-read)) | ||
| 173 | (error | ||
| 174 | (gnus-message | ||
| 175 | 1 | ||
| 176 | "gnus-sync-lesync-parse: Could not read the LeSync response!") | ||
| 177 | nil))) | ||
| 178 | |||
| 179 | (defun gnus-sync-lesync-call (url method headers &optional kvdata) | ||
| 180 | "Make an access request to URL using KVDATA and METHOD. | ||
| 181 | KVDATA must be an alist." | ||
| 182 | (let ((url-request-method method) | ||
| 183 | (url-request-extra-headers headers) | ||
| 184 | (url-request-data (if kvdata (json-encode kvdata) nil))) | ||
| 185 | (with-current-buffer (url-retrieve-synchronously url) | ||
| 186 | (let ((data (gnus-sync-lesync-parse))) | ||
| 187 | (gnus-message 12 "gnus-sync-lesync-call: %s URL %s sent %S got %S" | ||
| 188 | method url `((headers . ,headers) (data ,kvdata)) data) | ||
| 189 | (kill-buffer (current-buffer)) | ||
| 190 | data)))) | ||
| 191 | |||
| 192 | (defun gnus-sync-lesync-PUT (url headers &optional data) | ||
| 193 | (gnus-sync-lesync-call url "PUT" headers data)) | ||
| 194 | |||
| 195 | (defun gnus-sync-lesync-POST (url headers &optional data) | ||
| 196 | (gnus-sync-lesync-call url "POST" headers data)) | ||
| 197 | |||
| 198 | (defun gnus-sync-lesync-GET (url headers &optional data) | ||
| 199 | (gnus-sync-lesync-call url "GET" headers data)) | ||
| 200 | |||
| 201 | (defun gnus-sync-lesync-DELETE (url headers &optional data) | ||
| 202 | (gnus-sync-lesync-call url "DELETE" headers data)) | ||
| 203 | |||
| 204 | ; (gnus-sync-lesync-setup "http://lesync.info:5984/tzz" "tzzadmin" "mypassword" "mysalt" t t) | ||
| 205 | ; (gnus-sync-lesync-setup "http://lesync.info:5984/tzz") | ||
| 206 | |||
| 207 | (defun gnus-sync-lesync-setup (url &optional user password salt reader admin) | ||
| 208 | (interactive "sEnter URL to set up: ") | ||
| 209 | "Set up the LeSync database at URL. | ||
| 210 | Install USER as a READER and/or an ADMIN in the security object | ||
| 211 | under \"_security\", and in the CouchDB \"_users\" table using | ||
| 212 | PASSWORD and SALT. Only one USER is thus supported for now. | ||
| 213 | When SALT is nil, a random one will be generated using `random'." | ||
| 214 | (let* ((design-url (concat url gnus-sync-lesync-design-prefix)) | ||
| 215 | (security-object (concat url "/_security")) | ||
| 216 | (user-record `((names . [,user]) (roles . []))) | ||
| 217 | (couch-user-name (format "org.couchdb.user:%s" user)) | ||
| 218 | (salt (or salt (sha1 (format "%s" (random))))) | ||
| 219 | (couch-user-record | ||
| 220 | `((_id . ,couch-user-name) | ||
| 221 | (type . user) | ||
| 222 | (name . ,(format "%s" user)) | ||
| 223 | (roles . []) | ||
| 224 | (salt . ,salt) | ||
| 225 | (password_sha . ,(when password | ||
| 226 | (sha1 | ||
| 227 | (format "%s%s" password salt)))))) | ||
| 228 | (rev (progn | ||
| 229 | (gnus-sync-lesync-find-prop 'rev design-url design-url) | ||
| 230 | (gnus-sync-lesync-get-prop 'rev design-url))) | ||
| 231 | (latest-func "function(head,req) | ||
| 232 | { | ||
| 233 | var tosend = []; | ||
| 234 | var row; | ||
| 235 | var ftime = (req.query['ftime'] || 0); | ||
| 236 | while (row = getRow()) | ||
| 237 | { | ||
| 238 | if (row.value['float-time'] > ftime) | ||
| 239 | { | ||
| 240 | var s = row.value['_id']; | ||
| 241 | if (s) tosend.push('\"'+s.replace('\"', '\\\"')+'\"'); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | send('['+tosend.join(',') + ']'); | ||
| 245 | }") | ||
| 246 | ;; <key>read</key> | ||
| 247 | ;; <dict> | ||
| 248 | ;; <key>de.alt.fan.ipod</key> | ||
| 249 | ;; <array> | ||
| 250 | ;; <integer>1</integer> | ||
| 251 | ;; <integer>2</integer> | ||
| 252 | ;; <dict> | ||
| 253 | ;; <key>start</key> | ||
| 254 | ;; <integer>100</integer> | ||
| 255 | ;; <key>length</key> | ||
| 256 | ;; <integer>100</integer> | ||
| 257 | ;; </dict> | ||
| 258 | ;; </array> | ||
| 259 | ;; </dict> | ||
| 260 | (xmlplistread-func "function(head, req) { | ||
| 261 | var row; | ||
| 262 | start({ 'headers': { 'Content-Type': 'text/xml' } }); | ||
| 263 | |||
| 264 | send('<dict>'); | ||
| 265 | send('<key>read</key>'); | ||
| 266 | send('<dict>'); | ||
| 267 | while(row = getRow()) | ||
| 268 | { | ||
| 269 | var read = row.value.read; | ||
| 270 | if (read && read[0] && read[0] == 'invlist') | ||
| 271 | { | ||
| 272 | send('<key>'+row.key+'</key>'); | ||
| 273 | //send('<invlist>'+read+'</invlist>'); | ||
| 274 | send('<array>'); | ||
| 275 | |||
| 276 | var from = 0; | ||
| 277 | var flip = false; | ||
| 278 | |||
| 279 | for (var i = 1; i < read.length && read[i]; i++) | ||
| 280 | { | ||
| 281 | var cur = read[i]; | ||
| 282 | if (flip) | ||
| 283 | { | ||
| 284 | if (from == cur-1) | ||
| 285 | { | ||
| 286 | send('<integer>'+read[i]+'</integer>'); | ||
| 287 | } | ||
| 288 | else | ||
| 289 | { | ||
| 290 | send('<dict>'); | ||
| 291 | send('<key>start</key>'); | ||
| 292 | send('<integer>'+from+'</integer>'); | ||
| 293 | send('<key>end</key>'); | ||
| 294 | send('<integer>'+(cur-1)+'</integer>'); | ||
| 295 | send('</dict>'); | ||
| 296 | } | ||
| 297 | |||
| 298 | } | ||
| 299 | flip = ! flip; | ||
| 300 | from = cur; | ||
| 301 | } | ||
| 302 | send('</array>'); | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | send('</dict>'); | ||
| 307 | send('</dict>'); | ||
| 308 | } | ||
| 309 | ") | ||
| 310 | (subs-func "function(doc){emit([doc._id, doc.source], doc._rev);}") | ||
| 311 | (revs-func "function(doc){emit(doc._id, doc._rev);}") | ||
| 312 | (bytimesubs-func "function(doc) | ||
| 313 | {emit([(doc['float-time']||0), doc._id], doc._rev);}") | ||
| 314 | (bytime-func "function(doc) | ||
| 315 | {emit([(doc['float-time']||0), doc._id], doc);}") | ||
| 316 | (groups-func "function(doc){emit(doc._id, doc);}")) | ||
| 317 | (and (if user | ||
| 318 | (and (assq 'ok (gnus-sync-lesync-PUT | ||
| 319 | security-object | ||
| 320 | nil | ||
| 321 | (append (and reader | ||
| 322 | (list `(readers . ,user-record))) | ||
| 323 | (and admin | ||
| 324 | (list `(admins . ,user-record)))))) | ||
| 325 | (assq 'ok (gnus-sync-lesync-PUT | ||
| 326 | (concat (file-name-directory url) | ||
| 327 | "_users/" | ||
| 328 | couch-user-name) | ||
| 329 | nil | ||
| 330 | couch-user-record))) | ||
| 331 | t) | ||
| 332 | (assq 'ok (gnus-sync-lesync-PUT | ||
| 333 | design-url | ||
| 334 | nil | ||
| 335 | `(,@(when rev (list (cons '_rev rev))) | ||
| 336 | (lists . ((latest . ,latest-func) | ||
| 337 | (xmlplistread . ,xmlplistread-func))) | ||
| 338 | (views . ((subs . ((map . ,subs-func))) | ||
| 339 | (revs . ((map . ,revs-func))) | ||
| 340 | (bytimesubs . ((map . ,bytimesubs-func))) | ||
| 341 | (bytime . ((map . ,bytime-func))) | ||
| 342 | (groups . ((map . ,groups-func))))))))))) | ||
| 343 | |||
| 344 | (defun gnus-sync-lesync-find-prop (prop url key) | ||
| 345 | "Retrieve a PROPerty of a document KEY at URL. | ||
| 346 | Calls `gnus-sync-lesync-set-prop'. | ||
| 347 | For the 'rev PROP, uses '_rev against the document." | ||
| 348 | (gnus-sync-lesync-set-prop | ||
| 349 | prop key (cdr (assq (if (eq prop 'rev) '_rev prop) | ||
| 350 | (gnus-sync-lesync-GET url nil))))) | ||
| 351 | |||
| 352 | (defun gnus-sync-lesync-set-prop (prop key val) | ||
| 353 | "Update the PROPerty of document KEY at URL to VAL. | ||
| 354 | Updates `gnus-sync-lesync-props-hash'." | ||
| 355 | (puthash (format "%s.%s" key prop) val gnus-sync-lesync-props-hash)) | ||
| 356 | |||
| 357 | (defun gnus-sync-lesync-get-prop (prop key) | ||
| 358 | "Get the PROPerty of KEY from `gnus-sync-lesync-props-hash'." | ||
| 359 | (gethash (format "%s.%s" key prop) gnus-sync-lesync-props-hash)) | ||
| 360 | |||
| 361 | (defun gnus-sync-deep-print (data) | ||
| 362 | (let* ((print-quoted t) | ||
| 363 | (print-readably t) | ||
| 364 | (print-escape-multibyte nil) | ||
| 365 | (print-escape-nonascii t) | ||
| 366 | (print-length nil) | ||
| 367 | (print-level nil) | ||
| 368 | (print-circle nil) | ||
| 369 | (print-escape-newlines t)) | ||
| 370 | (format "%S" data))) | ||
| 371 | |||
| 372 | (defun gnus-sync-newsrc-loader-builder (&optional only-modified) | ||
| 373 | (let* ((entries (cdr gnus-newsrc-alist)) | ||
| 374 | entry name ret) | ||
| 375 | (while entries | ||
| 376 | (setq entry (pop entries) | ||
| 377 | name (car entry)) | ||
| 378 | (when (gnus-grep-in-list name gnus-sync-newsrc-groups) | ||
| 379 | (if only-modified | ||
| 380 | (when (not (equal (gnus-sync-deep-print entry) | ||
| 381 | (gnus-sync-lesync-get-prop 'checksum name))) | ||
| 382 | (gnus-message 9 "%s: add %s, it's modified" | ||
| 383 | "gnus-sync-newsrc-loader-builder" name) | ||
| 384 | (push entry ret)) | ||
| 385 | (push entry ret)))) | ||
| 386 | ret)) | ||
| 387 | |||
| 388 | ; (json-encode (gnus-sync-range2invlist '((1 . 47137) (47139 . 47714) 48129 48211 49231 49281 49342 49473 49475 49502))) | ||
| 389 | (defun gnus-sync-range2invlist (ranges) | ||
| 390 | (append '(invlist) | ||
| 391 | (let ((ranges (delq nil ranges)) | ||
| 392 | ret range from to) | ||
| 393 | (while ranges | ||
| 394 | (setq range (pop ranges)) | ||
| 395 | (if (atom range) | ||
| 396 | (setq from range | ||
| 397 | to range) | ||
| 398 | (setq from (car range) | ||
| 399 | to (cdr range))) | ||
| 400 | (push from ret) | ||
| 401 | (push (1+ to) ret)) | ||
| 402 | (reverse ret)))) | ||
| 403 | |||
| 404 | ; (let* ((d '((1 . 47137) (47139 . 47714) 48129 48211 49231 49281 49342 49473 49475 49502)) (j (format "%S" (gnus-sync-invlist2range (gnus-sync-range2invlist d))))) (or (equal (format "%S" d) j) j)) | ||
| 405 | (defun gnus-sync-invlist2range (inv) | ||
| 406 | (setq inv (append inv nil)) | ||
| 407 | (if (equal (format "%s" (car inv)) "invlist") | ||
| 408 | (let ((i (cdr inv)) | ||
| 409 | (start 0) | ||
| 410 | ret cur top flip) | ||
| 411 | (while i | ||
| 412 | (setq cur (pop i)) | ||
| 413 | (when flip | ||
| 414 | (setq top (1- cur)) | ||
| 415 | (if (= start top) | ||
| 416 | (push start ret) | ||
| 417 | (push (cons start top) ret))) | ||
| 418 | (setq flip (not flip)) | ||
| 419 | (setq start cur)) | ||
| 420 | (reverse ret)) | ||
| 421 | inv)) | ||
| 422 | |||
| 423 | (defun gnus-sync-position (search list &optional test) | ||
| 424 | "Find the position of SEARCH in LIST using TEST, defaulting to `eq'." | ||
| 425 | (let ((pos 0) | ||
| 426 | (test (or test 'eq))) | ||
| 427 | (while (and list (not (funcall test (car list) search))) | ||
| 428 | (pop list) | ||
| 429 | (incf pos)) | ||
| 430 | (if (funcall test (car list) search) pos nil))) | ||
| 431 | |||
| 432 | (defun gnus-sync-topic-group-position (group topic-name) | ||
| 433 | (gnus-sync-position | ||
| 434 | group (cdr (assoc topic-name gnus-topic-alist)) 'equal)) | ||
| 435 | |||
| 436 | (defun gnus-sync-fix-topic-group-position (group topic-name position) | ||
| 437 | (unless (equal position (gnus-sync-topic-group-position group topic-name)) | ||
| 438 | (let* ((loc "gnus-sync-fix-topic-group-position") | ||
| 439 | (groups (delete group (cdr (assoc topic-name gnus-topic-alist)))) | ||
| 440 | (position (min position (1- (length groups)))) | ||
| 441 | (old (nth position groups))) | ||
| 442 | (when (and old (not (equal old group))) | ||
| 443 | (setf (nth position groups) group) | ||
| 444 | (setcdr (assoc topic-name gnus-topic-alist) | ||
| 445 | (append groups (list old))) | ||
| 446 | (gnus-message 9 "%s: %s moved to %d, swap with %s" | ||
| 447 | loc group position old))))) | ||
| 448 | |||
| 449 | (defun gnus-sync-lesync-pre-save-group-entry (url nentry &rest passed-props) | ||
| 450 | (let* ((loc "gnus-sync-lesync-save-group-entry") | ||
| 451 | (k (car nentry)) | ||
| 452 | (revision (gnus-sync-lesync-get-prop 'rev k)) | ||
| 453 | (sname gnus-sync-lesync-name) | ||
| 454 | (topic (gnus-group-topic k)) | ||
| 455 | (topic-offset (gnus-sync-topic-group-position k topic)) | ||
| 456 | (sources (gnus-sync-lesync-get-prop 'source k))) | ||
| 457 | ;; set the revision so we don't have a conflict | ||
| 458 | `(,@(when revision | ||
| 459 | (list (cons '_rev revision))) | ||
| 460 | (_id . ,k) | ||
| 461 | ;; the time we saved | ||
| 462 | ,@passed-props | ||
| 463 | ;; add our name to the sources list for this key | ||
| 464 | (source ,@(if (member gnus-sync-lesync-name sources) | ||
| 465 | sources | ||
| 466 | (cons gnus-sync-lesync-name sources))) | ||
| 467 | ,(cons 'level (nth 1 nentry)) | ||
| 468 | ,@(if topic (list (cons 'topic topic)) nil) | ||
| 469 | ,@(if topic-offset (list (cons 'topic-offset topic-offset)) nil) | ||
| 470 | ;; the read marks | ||
| 471 | ,(cons 'read (gnus-sync-range2invlist (nth 2 nentry))) | ||
| 472 | ;; the other marks | ||
| 473 | ,@(delq nil (mapcar (lambda (mark-entry) | ||
| 474 | (gnus-message 12 "%s: prep param %s in %s" | ||
| 475 | loc | ||
| 476 | (car mark-entry) | ||
| 477 | (nth 3 nentry)) | ||
| 478 | (if (listp (cdr mark-entry)) | ||
| 479 | (cons (car mark-entry) | ||
| 480 | (gnus-sync-range2invlist | ||
| 481 | (cdr mark-entry))) | ||
| 482 | (progn ; else this is not a list | ||
| 483 | (gnus-message 9 "%s: non-list param %s in %s" | ||
| 484 | loc | ||
| 485 | (car mark-entry) | ||
| 486 | (nth 3 nentry)) | ||
| 487 | nil))) | ||
| 488 | (nth 3 nentry)))))) | ||
| 489 | |||
| 490 | (defun gnus-sync-lesync-post-save-group-entry (url entry) | ||
| 491 | (let* ((loc "gnus-sync-lesync-post-save-group-entry") | ||
| 492 | (k (cdr (assq 'id entry)))) | ||
| 493 | (cond | ||
| 494 | ;; success! | ||
| 495 | ((and (assq 'rev entry) (assq 'id entry)) | ||
| 496 | (progn | ||
| 497 | (gnus-sync-lesync-set-prop 'rev k (cdr (assq 'rev entry))) | ||
| 498 | (gnus-sync-lesync-set-prop 'checksum | ||
| 499 | k | ||
| 500 | (gnus-sync-deep-print | ||
| 501 | (assoc k gnus-newsrc-alist))) | ||
| 502 | (gnus-message 9 "%s: successfully synced %s to %s" | ||
| 503 | loc k url))) | ||
| 504 | ;; specifically check for document conflicts | ||
| 505 | ((equal "conflict" (format "%s" (cdr-safe (assq 'error entry)))) | ||
| 506 | (gnus-error | ||
| 507 | 1 | ||
| 508 | "%s: use `%s' to resolve the conflict synchronizing %s to %s: %s" | ||
| 509 | loc "gnus-sync-read" k url (cdr (assq 'reason entry)))) | ||
| 510 | ;; generic errors | ||
| 511 | ((assq 'error entry) | ||
| 512 | (gnus-error 1 "%s: got error while synchronizing %s to %s: %s" | ||
| 513 | loc k url (cdr (assq 'reason entry)))) | ||
| 514 | |||
| 515 | (t | ||
| 516 | (gnus-message 2 "%s: unknown sync status after %s to %s: %S" | ||
| 517 | loc k url entry))) | ||
| 518 | (assoc 'error entry))) | ||
| 519 | |||
| 520 | (defun gnus-sync-lesync-groups-builder (url) | ||
| 521 | (let ((u (concat url gnus-sync-lesync-design-prefix "/_view/groups"))) | ||
| 522 | (cdr (assq 'rows (gnus-sync-lesync-GET u nil))))) | ||
| 523 | |||
| 524 | (defun gnus-sync-subscribe-group (name) | ||
| 525 | "Subscribe to group NAME. Returns NAME on success, nil otherwise." | ||
| 526 | (gnus-subscribe-newsgroup name)) | ||
| 527 | |||
| 528 | (defun gnus-sync-lesync-read-group-entry (url name entry &rest passed-props) | ||
| 529 | "Read ENTRY information for NAME. Returns NAME if successful. | ||
| 530 | Skips entries whose sources don't contain | ||
| 531 | `gnus-sync-lesync-name'. When the alist PASSED-PROPS has a | ||
| 532 | `subscribe-all' element that evaluates to true, we attempt to | ||
| 533 | subscribe to unknown groups. The user is also allowed to delete | ||
| 534 | unwanted groups via the LeSync URL." | ||
| 535 | (let* ((loc "gnus-sync-lesync-read-group-entry") | ||
| 536 | (entry (gnus-sync-lesync-normalize-group-entry entry passed-props)) | ||
| 537 | (subscribe-all (cdr (assq 'subscribe-all passed-props))) | ||
| 538 | (sources (cdr (assq 'source entry))) | ||
| 539 | (rev (cdr (assq 'rev entry))) | ||
| 540 | (in-sources (member gnus-sync-lesync-name sources)) | ||
| 541 | (known (assoc name gnus-newsrc-alist)) | ||
| 542 | cell) | ||
| 543 | (unless known | ||
| 544 | (if (and subscribe-all | ||
| 545 | (y-or-n-p (format "Subscribe to group %s?" name))) | ||
| 546 | (setq known (gnus-sync-subscribe-group name) | ||
| 547 | in-sources t) | ||
| 548 | ;; else... | ||
| 549 | (when (y-or-n-p (format "Delete group %s from server?" name)) | ||
| 550 | (if (equal name (gnus-sync-lesync-delete-group url name)) | ||
| 551 | (gnus-message 1 "%s: removed group %s from server %s" | ||
| 552 | loc name url) | ||
| 553 | (gnus-error 1 "%s: could not remove group %s from server %s" | ||
| 554 | loc name url))))) | ||
| 555 | (when known | ||
| 556 | (unless in-sources | ||
| 557 | (setq in-sources | ||
| 558 | (y-or-n-p | ||
| 559 | (format "Read group %s even though %s is not in sources %S?" | ||
| 560 | name gnus-sync-lesync-name (or sources "")))))) | ||
| 561 | (when rev | ||
| 562 | (gnus-sync-lesync-set-prop 'rev name rev)) | ||
| 563 | |||
| 564 | ;; if the source matches AND we have this group | ||
| 565 | (if (and known in-sources) | ||
| 566 | (progn | ||
| 567 | (gnus-message 10 "%s: reading LeSync entry %s, sources %S" | ||
| 568 | loc name sources) | ||
| 569 | (while entry | ||
| 570 | (setq cell (pop entry)) | ||
| 571 | (let ((k (car cell)) | ||
| 572 | (val (cdr cell))) | ||
| 573 | (gnus-sync-lesync-set-prop k name val))) | ||
| 574 | name) | ||
| 575 | ;; else... | ||
| 576 | (unless known | ||
| 577 | (gnus-message 5 "%s: ignoring entry %s, it wasn't subscribed. %s" | ||
| 578 | loc name "Call `gnus-sync-read' with C-u to force it.")) | ||
| 579 | (unless in-sources | ||
| 580 | (gnus-message 5 "%s: ignoring entry %s, %s not in sources %S" | ||
| 581 | loc name gnus-sync-lesync-name (or sources ""))) | ||
| 582 | nil))) | ||
| 583 | |||
| 584 | (declare-function gnus-topic-create-topic "gnus-topic" | ||
| 585 | (topic parent &optional previous full-topic)) | ||
| 586 | (declare-function gnus-topic-enter-dribble "gnus-topic" ()) | ||
| 587 | |||
| 588 | (defun gnus-sync-lesync-install-group-entry (name) | ||
| 589 | (let* ((master (assoc name gnus-newsrc-alist)) | ||
| 590 | (old-topic-name (gnus-group-topic name)) | ||
| 591 | (old-topic (assoc old-topic-name gnus-topic-alist)) | ||
| 592 | (target-topic-name (gnus-sync-lesync-get-prop 'topic name)) | ||
| 593 | (target-topic-offset (gnus-sync-lesync-get-prop 'topic-offset name)) | ||
| 594 | (target-topic (assoc target-topic-name gnus-topic-alist)) | ||
| 595 | (loc "gnus-sync-lesync-install-group-entry")) | ||
| 596 | (if master | ||
| 597 | (progn | ||
| 598 | (when (eq 'ask gnus-sync-lesync-install-topics) | ||
| 599 | (setq gnus-sync-lesync-install-topics | ||
| 600 | (y-or-n-p "Install topics from LeSync?"))) | ||
| 601 | (when (and (eq t gnus-sync-lesync-install-topics) | ||
| 602 | target-topic-name) | ||
| 603 | (if (equal old-topic-name target-topic-name) | ||
| 604 | (gnus-message 12 "%s: %s is already in topic %s" | ||
| 605 | loc name target-topic-name) | ||
| 606 | ;; see `gnus-topic-move-group' | ||
| 607 | (when (and old-topic target-topic) | ||
| 608 | (setcdr old-topic (gnus-delete-first name (cdr old-topic))) | ||
| 609 | (gnus-message 5 "%s: removing %s from topic %s" | ||
| 610 | loc name old-topic-name)) | ||
| 611 | (unless target-topic | ||
| 612 | (when (y-or-n-p (format "Create missing topic %s?" | ||
| 613 | target-topic-name)) | ||
| 614 | (gnus-topic-create-topic target-topic-name nil) | ||
| 615 | (setq target-topic (assoc target-topic-name | ||
| 616 | gnus-topic-alist)))) | ||
| 617 | (if target-topic | ||
| 618 | (prog1 | ||
| 619 | (nconc target-topic (list name)) | ||
| 620 | (gnus-message 5 "%s: adding %s to topic %s" | ||
| 621 | loc name (car target-topic)) | ||
| 622 | (gnus-topic-enter-dribble)) | ||
| 623 | (gnus-error 2 "%s: LeSync group %s can't go in missing topic %s" | ||
| 624 | loc name target-topic-name))) | ||
| 625 | (when (and target-topic-offset target-topic) | ||
| 626 | (gnus-sync-fix-topic-group-position | ||
| 627 | name target-topic-name target-topic-offset))) | ||
| 628 | ;; install the subscription level | ||
| 629 | (when (gnus-sync-lesync-get-prop 'level name) | ||
| 630 | (setf (nth 1 master) (gnus-sync-lesync-get-prop 'level name))) | ||
| 631 | ;; install the read and other marks | ||
| 632 | (setf (nth 2 master) (gnus-sync-lesync-get-prop 'read name)) | ||
| 633 | (setf (nth 3 master) (gnus-sync-lesync-get-prop 'marks name)) | ||
| 634 | (gnus-sync-lesync-set-prop 'checksum | ||
| 635 | name | ||
| 636 | (gnus-sync-deep-print master)) | ||
| 637 | nil) | ||
| 638 | (gnus-error 1 "%s: invalid LeSync group %s" loc name) | ||
| 639 | 'invalid-name))) | ||
| 640 | |||
| 641 | ; (gnus-sync-lesync-delete-group (cdr gnus-sync-backend) "nntp+Gmane:gwene.org.slashdot") | ||
| 642 | |||
| 643 | (defun gnus-sync-lesync-delete-group (url name) | ||
| 644 | "Returns NAME if successful deleting it from URL, an error otherwise." | ||
| 645 | (interactive "sEnter URL to set up: \rsEnter group name: ") | ||
| 646 | (let* ((u (concat (cadr gnus-sync-backend) "/" (url-hexify-string name))) | ||
| 647 | (del (gnus-sync-lesync-DELETE | ||
| 648 | u | ||
| 649 | `(,@(when (gnus-sync-lesync-get-prop 'rev name) | ||
| 650 | (list (cons "If-Match" | ||
| 651 | (gnus-sync-lesync-get-prop 'rev name)))))))) | ||
| 652 | (or (cdr (assq 'id del)) del))) | ||
| 653 | |||
| 654 | ;;; (gnus-sync-lesync-normalize-group-entry '((subscribe . ["invlist"]) (read . ["invlist"]) (topic-offset . 20) (topic . "news") (level . 6) (source . ["a" "b"]) (float-time . 1319671237.099285) (_rev . "10-edf5107f41e5e6f7f6629d1c0ee172f7") (_id . "nntp+news.net:alt.movies")) '((read-time 1319672156.486414) (subscribe-all nil))) | ||
| 655 | |||
| 656 | (defun gnus-sync-lesync-normalize-group-entry (entry &optional passed-props) | ||
| 657 | (let (ret | ||
| 658 | marks | ||
| 659 | cell) | ||
| 660 | (setq entry (append passed-props entry)) | ||
| 661 | (while (setq cell (pop entry)) | ||
| 662 | (let ((k (car cell)) | ||
| 663 | (val (cdr cell))) | ||
| 664 | (cond | ||
| 665 | ((eq k 'read) | ||
| 666 | (push (cons k (gnus-sync-invlist2range val)) ret)) | ||
| 667 | ;; we ignore these parameters | ||
| 668 | ((member k '(_id subscribe-all _deleted_conflicts)) | ||
| 669 | nil) | ||
| 670 | ((eq k '_rev) | ||
| 671 | (push (cons 'rev val) ret)) | ||
| 672 | ((eq k 'source) | ||
| 673 | (push (cons 'source (append val nil)) ret)) | ||
| 674 | ((or (eq k 'float-time) | ||
| 675 | (eq k 'level) | ||
| 676 | (eq k 'topic) | ||
| 677 | (eq k 'topic-offset) | ||
| 678 | (eq k 'read-time)) | ||
| 679 | (push (cons k val) ret)) | ||
| 680 | ;;; "How often have I said to you that when you have eliminated the | ||
| 681 | ;;; impossible, whatever remains, however improbable, must be the | ||
| 682 | ;;; truth?" --Sherlock Holmes | ||
| 683 | ;; everything remaining must be a mark | ||
| 684 | (t (push (cons k (gnus-sync-invlist2range val)) marks))))) | ||
| 685 | (cons (cons 'marks marks) ret))) | ||
| 686 | |||
| 687 | (defun gnus-sync-save (&optional force) | ||
| 688 | "Save the Gnus sync data to the backend. | ||
| 689 | With a prefix, FORCE is set and all groups will be saved." | ||
| 690 | (interactive "P") | ||
| 691 | (cond | ||
| 692 | ((and (listp gnus-sync-backend) | ||
| 693 | (eq (nth 0 gnus-sync-backend) 'lesync) | ||
| 694 | (stringp (nth 1 gnus-sync-backend))) | ||
| 695 | |||
| 696 | ;; refresh the revisions if we're forcing the save | ||
| 697 | (when force | ||
| 698 | (mapc (lambda (entry) | ||
| 699 | (when (and (assq 'key entry) | ||
| 700 | (assq 'value entry)) | ||
| 701 | (gnus-sync-lesync-set-prop | ||
| 702 | 'rev | ||
| 703 | (cdr (assq 'key entry)) | ||
| 704 | (cdr (assq 'value entry))))) | ||
| 705 | ;; the revs view is key = name, value = rev | ||
| 706 | (cdr (assq 'rows (gnus-sync-lesync-GET | ||
| 707 | (concat (nth 1 gnus-sync-backend) | ||
| 708 | gnus-sync-lesync-design-prefix | ||
| 709 | "/_view/revs") | ||
| 710 | nil))))) | ||
| 711 | |||
| 712 | (let* ((ftime (float-time)) | ||
| 713 | (url (nth 1 gnus-sync-backend)) | ||
| 714 | (entries | ||
| 715 | (mapcar (lambda (entry) | ||
| 716 | (gnus-sync-lesync-pre-save-group-entry | ||
| 717 | (cadr gnus-sync-backend) | ||
| 718 | entry | ||
| 719 | (cons 'float-time ftime))) | ||
| 720 | (gnus-sync-newsrc-loader-builder (not force)))) | ||
| 721 | ;; when there are no entries, there's nothing to save | ||
| 722 | (sync (if entries | ||
| 723 | (gnus-sync-lesync-POST | ||
| 724 | (concat url "/_bulk_docs") | ||
| 725 | '(("Content-Type" . "application/json")) | ||
| 726 | `((docs . ,(vconcat entries nil)))) | ||
| 727 | (gnus-message | ||
| 728 | 2 "gnus-sync-save: nothing to save to the LeSync backend") | ||
| 729 | nil))) | ||
| 730 | (mapcar (lambda (e) (gnus-sync-lesync-post-save-group-entry url e)) | ||
| 731 | sync))) | ||
| 732 | ((stringp gnus-sync-backend) | ||
| 733 | (gnus-message 7 "gnus-sync-save: saving to backend %s" gnus-sync-backend) | ||
| 734 | ;; populate gnus-sync-newsrc-loader from all but the first dummy | ||
| 735 | ;; entry in gnus-newsrc-alist whose group matches any of the | ||
| 736 | ;; gnus-sync-newsrc-groups | ||
| 737 | ;; TODO: keep the old contents for groups we don't have! | ||
| 738 | (let ((gnus-sync-newsrc-loader | ||
| 739 | (loop for entry in (cdr gnus-newsrc-alist) | ||
| 740 | when (gnus-grep-in-list | ||
| 741 | (car entry) ;the group name | ||
| 742 | gnus-sync-newsrc-groups) | ||
| 743 | collect (cons (car entry) | ||
| 744 | (mapcar (lambda (offset) | ||
| 745 | (cons offset (nth offset entry))) | ||
| 746 | gnus-sync-newsrc-offsets))))) | ||
| 747 | (with-temp-file gnus-sync-backend | ||
| 748 | (progn | ||
| 749 | (let ((coding-system-for-write gnus-ding-file-coding-system) | ||
| 750 | (standard-output (current-buffer))) | ||
| 751 | (when gnus-sync-file-encrypt-to | ||
| 752 | (set (make-local-variable 'epa-file-encrypt-to) | ||
| 753 | gnus-sync-file-encrypt-to)) | ||
| 754 | (princ (format ";; -*- mode:emacs-lisp; coding: %s; -*-\n" | ||
| 755 | gnus-ding-file-coding-system)) | ||
| 756 | (princ ";; Gnus sync data v. 0.0.1\n") | ||
| 757 | ;; TODO: replace with `gnus-sync-deep-print' | ||
| 758 | (let* ((print-quoted t) | ||
| 759 | (print-readably t) | ||
| 760 | (print-escape-multibyte nil) | ||
| 761 | (print-escape-nonascii t) | ||
| 762 | (print-length nil) | ||
| 763 | (print-level nil) | ||
| 764 | (print-circle nil) | ||
| 765 | (print-escape-newlines t) | ||
| 766 | (variables (cons 'gnus-sync-newsrc-loader | ||
| 767 | gnus-sync-global-vars)) | ||
| 768 | variable) | ||
| 769 | (while variables | ||
| 770 | (if (and (boundp (setq variable (pop variables))) | ||
| 771 | (symbol-value variable)) | ||
| 772 | (progn | ||
| 773 | (princ "\n(setq ") | ||
| 774 | (princ (symbol-name variable)) | ||
| 775 | (princ " '") | ||
| 776 | (prin1 (symbol-value variable)) | ||
| 777 | (princ ")\n")) | ||
| 778 | (princ "\n;;; skipping empty variable ") | ||
| 779 | (princ (symbol-name variable))))) | ||
| 780 | (gnus-message | ||
| 781 | 7 | ||
| 782 | "gnus-sync-save: stored variables %s and %d groups in %s" | ||
| 783 | gnus-sync-global-vars | ||
| 784 | (length gnus-sync-newsrc-loader) | ||
| 785 | gnus-sync-backend) | ||
| 786 | |||
| 787 | ;; Idea from Dan Christensen <jdc@chow.mat.jhu.edu> | ||
| 788 | ;; Save the .eld file with extra line breaks. | ||
| 789 | (gnus-message 8 "gnus-sync-save: adding whitespace to %s" | ||
| 790 | gnus-sync-backend) | ||
| 791 | (save-excursion | ||
| 792 | (goto-char (point-min)) | ||
| 793 | (while (re-search-forward "^(\\|(\\\"" nil t) | ||
| 794 | (replace-match "\n\\&" t)) | ||
| 795 | (goto-char (point-min)) | ||
| 796 | (while (re-search-forward " $" nil t) | ||
| 797 | (replace-match "" t t)))))))) | ||
| 798 | ;; the pass-through case: gnus-sync-backend is not a known choice | ||
| 799 | (nil))) | ||
| 800 | |||
| 801 | (defun gnus-sync-read (&optional subscribe-all) | ||
| 802 | "Load the Gnus sync data from the backend. | ||
| 803 | With a prefix, SUBSCRIBE-ALL is set and unknown groups will be subscribed." | ||
| 804 | (interactive "P") | ||
| 805 | (when gnus-sync-backend | ||
| 806 | (gnus-message 7 "gnus-sync-read: loading from backend %s" gnus-sync-backend) | ||
| 807 | (cond | ||
| 808 | ((and (listp gnus-sync-backend) | ||
| 809 | (eq (nth 0 gnus-sync-backend) 'lesync) | ||
| 810 | (stringp (nth 1 gnus-sync-backend))) | ||
| 811 | (let ((errored nil) | ||
| 812 | name ftime) | ||
| 813 | (mapc (lambda (entry) | ||
| 814 | (setq name (cdr (assq 'id entry))) | ||
| 815 | ;; set ftime the FIRST time through this loop, that | ||
| 816 | ;; way it reflects the time we FINISHED reading | ||
| 817 | (unless ftime (setq ftime (float-time))) | ||
| 818 | |||
| 819 | (unless errored | ||
| 820 | (setq errored | ||
| 821 | (when (equal name | ||
| 822 | (gnus-sync-lesync-read-group-entry | ||
| 823 | (nth 1 gnus-sync-backend) | ||
| 824 | name | ||
| 825 | (cdr (assq 'value entry)) | ||
| 826 | `(read-time ,ftime) | ||
| 827 | `(subscribe-all ,subscribe-all))) | ||
| 828 | (gnus-sync-lesync-install-group-entry | ||
| 829 | (cdr (assq 'id entry))))))) | ||
| 830 | (gnus-sync-lesync-groups-builder (nth 1 gnus-sync-backend))))) | ||
| 831 | |||
| 832 | ((stringp gnus-sync-backend) | ||
| 833 | ;; read data here... | ||
| 834 | (if (or debug-on-error debug-on-quit) | ||
| 835 | (load gnus-sync-backend nil t) | ||
| 836 | (condition-case var | ||
| 837 | (load gnus-sync-backend nil t) | ||
| 838 | (error | ||
| 839 | (error "Error in %s: %s" gnus-sync-backend (cadr var))))) | ||
| 840 | (let ((valid-count 0) | ||
| 841 | invalid-groups) | ||
| 842 | (dolist (node gnus-sync-newsrc-loader) | ||
| 843 | (if (gnus-gethash (car node) gnus-newsrc-hashtb) | ||
| 844 | (progn | ||
| 845 | (incf valid-count) | ||
| 846 | (loop for store in (cdr node) | ||
| 847 | do (setf (nth (car store) | ||
| 848 | (assoc (car node) gnus-newsrc-alist)) | ||
| 849 | (cdr store)))) | ||
| 850 | (push (car node) invalid-groups))) | ||
| 851 | (gnus-message | ||
| 852 | 7 | ||
| 853 | "gnus-sync-read: loaded %d groups (out of %d) from %s" | ||
| 854 | valid-count (length gnus-sync-newsrc-loader) | ||
| 855 | gnus-sync-backend) | ||
| 856 | (when invalid-groups | ||
| 857 | (gnus-message | ||
| 858 | 7 | ||
| 859 | "gnus-sync-read: skipped %d groups (out of %d) from %s" | ||
| 860 | (length invalid-groups) | ||
| 861 | (length gnus-sync-newsrc-loader) | ||
| 862 | gnus-sync-backend) | ||
| 863 | (gnus-message 9 "gnus-sync-read: skipped groups: %s" | ||
| 864 | (mapconcat 'identity invalid-groups ", "))))) | ||
| 865 | (nil)) | ||
| 866 | |||
| 867 | (gnus-message 9 "gnus-sync-read: remaking the newsrc hashtable") | ||
| 868 | (gnus-make-hashtable-from-newsrc-alist))) | ||
| 869 | |||
| 870 | ;;;###autoload | ||
| 871 | (defun gnus-sync-initialize () | ||
| 872 | "Initialize the Gnus sync facility." | ||
| 873 | (interactive) | ||
| 874 | (gnus-message 5 "Initializing the sync facility") | ||
| 875 | (gnus-sync-install-hooks)) | ||
| 876 | |||
| 877 | ;;;###autoload | ||
| 878 | (defun gnus-sync-install-hooks () | ||
| 879 | "Install the sync hooks." | ||
| 880 | (interactive) | ||
| 881 | ;; (add-hook 'gnus-get-new-news-hook 'gnus-sync-read) | ||
| 882 | ;; (add-hook 'gnus-read-newsrc-el-hook 'gnus-sync-read) | ||
| 883 | (add-hook 'gnus-save-newsrc-hook 'gnus-sync-save)) | ||
| 884 | |||
| 885 | (defun gnus-sync-unload-hook () | ||
| 886 | "Uninstall the sync hooks." | ||
| 887 | (interactive) | ||
| 888 | (remove-hook 'gnus-save-newsrc-hook 'gnus-sync-save)) | ||
| 889 | |||
| 890 | (add-hook 'gnus-sync-unload-hook 'gnus-sync-unload-hook) | ||
| 891 | |||
| 892 | (when gnus-sync-backend (gnus-sync-initialize)) | ||
| 893 | |||
| 894 | (provide 'gnus-sync) | ||
| 895 | |||
| 896 | ;;; gnus-sync.el ends here | ||