Mercurial > addons > slrn-macros > slrn-persistent-tags-macro
comparison persistent-tags.sl @ 0:8eeb70d3d1ce
Initial revision
author | Guido Berhoerster <guido+slrn@berhoerster.name> |
---|---|
date | Sat, 14 Mar 2015 11:43:52 +0100 |
parents | |
children | 49f639bc9bd9 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:8eeb70d3d1ce |
---|---|
1 % persistent-tags.sl - keep persistent tags across sessions | |
2 % | |
3 % Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name> | |
4 % | |
5 % Permission is hereby granted, free of charge, to any person obtaining | |
6 % a copy of this software and associated documentation files (the | |
7 % "Software"), to deal in the Software without restriction, including | |
8 % without limitation the rights to use, copy, modify, merge, publish, | |
9 % distribute, sublicense, and/or sell copies of the Software, and to | |
10 % permit persons to whom the Software is furnished to do so, subject to | |
11 % the following conditions: | |
12 % | |
13 % The above copyright notice and this permission notice shall be included | |
14 % in all copies or substantial portions of the Software. | |
15 % | |
16 % THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 % EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
18 % MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
19 % IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
20 % CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
21 % TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
22 % SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
23 | |
24 %open_log_file(make_home_filename("slrn-debug.log")); | |
25 %_traceback = 1; | |
26 | |
27 implements("PersistentTags"); | |
28 | |
29 private variable rand_next = 1; | |
30 | |
31 % implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition | |
32 static define myrand() { | |
33 rand_next = rand_next * 1103515245 + 12345; | |
34 % RAND_MAX is hardcoded to 32767 | |
35 return ((rand_next / 65536U) mod 32768U); | |
36 } | |
37 | |
38 static define mysrand(seed) { | |
39 rand_next = seed; | |
40 } | |
41 | |
42 private variable URL_SAFE_CHARS = | |
43 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+"; | |
44 | |
45 private define urldecode(str) | |
46 { | |
47 variable decoded_str = ""B; | |
48 variable char; | |
49 variable pos = 1; | |
50 variable opos = 1; | |
51 | |
52 forever { | |
53 pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos); | |
54 if (pos == 0) | |
55 break; | |
56 | |
57 % add characters between the last match and the current match | |
58 decoded_str += substr(str, opos, pos - opos); | |
59 % convert the hex representation of a byte to a byte and append it to | |
60 % the string | |
61 char = integer("0x" + substr(str, pos + 1, 2)); | |
62 decoded_str += pack("C", char); | |
63 opos = pos + 3; | |
64 } | |
65 % add remaining charcters | |
66 decoded_str += substr(str, opos, -1); | |
67 return typecast(decoded_str, String_Type); | |
68 } | |
69 | |
70 private define urlencode(str) | |
71 { | |
72 variable char; | |
73 variable encoded_str = ""; | |
74 variable i; | |
75 variable j; | |
76 | |
77 for (i = 0; i < strlen(str); ++i) { | |
78 char = substr(str, i + 1, 1); | |
79 ifnot (is_substr(URL_SAFE_CHARS, char)) { | |
80 for (j = 0; j < strbytelen(char); ++j) { | |
81 encoded_str += sprintf("%%%02X", char[j]); | |
82 } | |
83 } else { | |
84 encoded_str += sprintf("%s", char); | |
85 } | |
86 } | |
87 return encoded_str; | |
88 } | |
89 | |
90 private variable FILENAME_CHARS = | |
91 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
92 private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS); | |
93 | |
94 private define mkstemp(template) | |
95 { | |
96 variable fd; | |
97 variable tmp_filename; | |
98 variable len_template = strlen(@template); | |
99 variable c; | |
100 | |
101 if (len_template < 6) | |
102 return NULL; | |
103 c = len_template - 6; | |
104 ifnot (substr(@template, c + 1, len_template) == "XXXXXX") | |
105 return NULL; | |
106 | |
107 loop (10000) { | |
108 tmp_filename = substr(@template, 1, c); | |
109 loop(6) { | |
110 tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]]; | |
111 } | |
112 fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600); | |
113 ifnot (fd == NULL) { | |
114 @template = tmp_filename; | |
115 break; | |
116 } | |
117 } | |
118 | |
119 return fd; | |
120 } | |
121 | |
122 private define lock_file(filename) | |
123 { | |
124 variable fd; | |
125 variable st; | |
126 variable timeout = qualifier("timeout", 30); | |
127 variable stale_timeout = qualifier("stale_timeout", 360); | |
128 variable lockfile = filename + ".lock"; | |
129 variable tmp_lockfile = lockfile + ".XXXXXX"; | |
130 variable time_timeout = _time() + timeout; | |
131 variable time_stale_timeout = _time() - stale_timeout; | |
132 | |
133 fd = mkstemp(&tmp_lockfile); | |
134 if (fd == NULL) | |
135 return NULL; | |
136 () = close(fd); | |
137 try { | |
138 % attempt to acquire a lock until time_timeout is reached | |
139 while (_time() < time_timeout) { | |
140 % try to link lockfile to the previously created temporary file, | |
141 % link(2) is atomic even on NFSv2 if the lockfile exists link(2) | |
142 % will fail, this is either detected if EEXIST is returned or the | |
143 % link count of the temporary file is not 2 in this case try to | |
144 % remove a stale lockfile, then wait and try again | |
145 ifnot ((hardlink(tmp_lockfile, lockfile) == 0) || | |
146 (errno == EEXIST)) | |
147 return NULL; | |
148 | |
149 st = stat_file(tmp_lockfile); | |
150 if (st == NULL) | |
151 return NULL; | |
152 if (st.st_nlink == 2) | |
153 return lockfile; | |
154 | |
155 st = stat_file(lockfile); | |
156 if (st == NULL) { | |
157 ifnot (errno == ENOENT) | |
158 return NULL; | |
159 else | |
160 continue; | |
161 } | |
162 | |
163 % remove a stale lockfile after stale_timeout seconds have passed | |
164 if (st.st_mtime < time_stale_timeout) | |
165 () = remove(lockfile); | |
166 | |
167 sleep(2); | |
168 } | |
169 | |
170 return NULL; | |
171 } finally { | |
172 () = remove(tmp_lockfile); | |
173 } | |
174 } | |
175 | |
176 static variable config = struct | |
177 { | |
178 tag_path = ".slrn-tags", | |
179 autosave = 1 | |
180 }; | |
181 | |
182 private variable tag_list = Assoc_Type[Null_Type]; | |
183 | |
184 private define update_tag_list(ref_tag_list) | |
185 { | |
186 variable new_tag_list = Assoc_Type[Null_Type]; | |
187 variable needs_update = 0; | |
188 variable msgid = NULL; | |
189 | |
190 call("header_bob"); | |
191 % the very first article must be treated specially so it will not be missed | |
192 % when doing next_tagged_header() | |
193 while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) || | |
194 next_tagged_header()) { | |
195 msgid = extract_article_header("Message-ID"); | |
196 if (strlen(msgid) == 0) | |
197 continue; | |
198 new_tag_list[msgid] = NULL; | |
199 | |
200 % check if a new element which will go into new_tag_list also exists | |
201 % in tag_list in order to find any difference between both lists | |
202 ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid)) | |
203 needs_update = 1; | |
204 } | |
205 | |
206 % if all elements of new_tag_list are also contained in tag_lists | |
207 % check whether all elements of tag_lists are also contained in | |
208 % new_tag_lists in order to find any difference between both lists | |
209 ifnot (needs_update) { | |
210 foreach msgid(@ref_tag_list) using("keys") { | |
211 ifnot (assoc_key_exists(new_tag_list, msgid)) { | |
212 needs_update = 1; | |
213 break; | |
214 } | |
215 } | |
216 } | |
217 | |
218 % replace tag_list with new_tag_list | |
219 @ref_tag_list = new_tag_list; | |
220 return needs_update; | |
221 } | |
222 | |
223 static define save_tags() | |
224 { | |
225 variable line, fp; | |
226 variable dir = path_concat(make_home_filename(config.tag_path), | |
227 urlencode(server_name())); | |
228 variable filename = path_concat(dir, urlencode(current_newsgroup())); | |
229 variable lockfile; | |
230 | |
231 ifnot (update_tag_list(&tag_list)) | |
232 return; | |
233 | |
234 if (mkdir(dir) == -1) { | |
235 ifnot (errno == EEXIST) | |
236 throw IOError, "mkdir $dir failed: "$ + errno_string(errno); | |
237 } | |
238 | |
239 lockfile = lock_file(filename); | |
240 if (lockfile == NULL) | |
241 throw IOError, "failed to lock $filename"$; | |
242 | |
243 try { | |
244 % if tag_list is empty remove the file | |
245 if (length(tag_list) == 0) { | |
246 () = remove(filename); | |
247 return; | |
248 } | |
249 | |
250 fp = fopen(filename, "w"); | |
251 if (fp == NULL) | |
252 throw OpenError, "opening $filename failed: "$ + | |
253 errno_string(errno); | |
254 foreach line(tag_list) using("keys") { | |
255 if (fputs(line + "\n", fp) == -1) | |
256 throw WriteError, "writing to $filename failed: "$ + | |
257 errno_string(errno); | |
258 } | |
259 () = fclose(fp); | |
260 } finally { | |
261 () = remove(lockfile); | |
262 } | |
263 return; | |
264 } | |
265 | |
266 static define load_tags() | |
267 { | |
268 variable fp, buf, msgid, pos; | |
269 variable dir = path_concat(make_home_filename(config.tag_path), | |
270 urlencode(server_name())); | |
271 variable filename = path_concat(dir, urlencode(current_newsgroup())); | |
272 | |
273 % re-ininitalize tag_list | |
274 tag_list = Assoc_Type[Null_Type]; | |
275 | |
276 fp = fopen(filename, "r"); | |
277 if (fp == NULL) { | |
278 if (errno == ENOENT) | |
279 return; | |
280 throw OpenError, "opening $filename failed: "$ + errno_string(errno); | |
281 } | |
282 | |
283 % save position to restore after applying tags | |
284 pos = extract_article_header("Message-ID"); | |
285 while (fgets(&buf, fp) != -1) { | |
286 msgid = strtrim(buf); | |
287 if (strlen(msgid)) { | |
288 tag_list[msgid] = NULL; | |
289 if (locate_header_by_msgid(msgid, 1)) | |
290 set_header_flags(get_header_flags() | HEADER_TAGGED); | |
291 } | |
292 } | |
293 () = fclose(fp); | |
294 () = locate_header_by_msgid(pos, 0); | |
295 | |
296 return; | |
297 } | |
298 | |
299 mysrand(_time() * getpid()); | |
300 | |
301 if (config.autosave) | |
302 { | |
303 () = register_hook("article_mode_hook", "PersistentTags->load_tags"); | |
304 () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags"); | |
305 } |