Mercurial > addons > slrn-macros > slrn-persistent-tags-macro
view persistent-tags.sl @ 1:49f639bc9bd9 default tip
Change license to GPL-2.0+
author | Guido Berhoerster <guido+slrn@berhoerster.name> |
---|---|
date | Sat, 25 Jul 2015 17:12:29 +0200 |
parents | 8eeb70d3d1ce |
children |
line wrap: on
line source
% persistent-tags.sl - keep persistent tags across sessions % % Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name> % % This program is free software; you can redistribute it and/or % modify it under the terms of the GNU General Public License % as published by the Free Software Foundation; either version 2 % of the License, or (at your option) any later version. % % This program is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU General Public License for more details. % % You should have received a copy of the GNU General Public License % along with this program; if not, write to the Free Software % Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. %open_log_file(make_home_filename("slrn-debug.log")); %_traceback = 1; implements("PersistentTags"); private variable rand_next = 1; % implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition static define myrand() { rand_next = rand_next * 1103515245 + 12345; % RAND_MAX is hardcoded to 32767 return ((rand_next / 65536U) mod 32768U); } static define mysrand(seed) { rand_next = seed; } private variable URL_SAFE_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+"; private define urldecode(str) { variable decoded_str = ""B; variable char; variable pos = 1; variable opos = 1; forever { pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos); if (pos == 0) break; % add characters between the last match and the current match decoded_str += substr(str, opos, pos - opos); % convert the hex representation of a byte to a byte and append it to % the string char = integer("0x" + substr(str, pos + 1, 2)); decoded_str += pack("C", char); opos = pos + 3; } % add remaining charcters decoded_str += substr(str, opos, -1); return typecast(decoded_str, String_Type); } private define urlencode(str) { variable char; variable encoded_str = ""; variable i; variable j; for (i = 0; i < strlen(str); ++i) { char = substr(str, i + 1, 1); ifnot (is_substr(URL_SAFE_CHARS, char)) { for (j = 0; j < strbytelen(char); ++j) { encoded_str += sprintf("%%%02X", char[j]); } } else { encoded_str += sprintf("%s", char); } } return encoded_str; } private variable FILENAME_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS); private define mkstemp(template) { variable fd; variable tmp_filename; variable len_template = strlen(@template); variable c; if (len_template < 6) return NULL; c = len_template - 6; ifnot (substr(@template, c + 1, len_template) == "XXXXXX") return NULL; loop (10000) { tmp_filename = substr(@template, 1, c); loop(6) { tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]]; } fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600); ifnot (fd == NULL) { @template = tmp_filename; break; } } return fd; } private define lock_file(filename) { variable fd; variable st; variable timeout = qualifier("timeout", 30); variable stale_timeout = qualifier("stale_timeout", 360); variable lockfile = filename + ".lock"; variable tmp_lockfile = lockfile + ".XXXXXX"; variable time_timeout = _time() + timeout; variable time_stale_timeout = _time() - stale_timeout; fd = mkstemp(&tmp_lockfile); if (fd == NULL) return NULL; () = close(fd); try { % attempt to acquire a lock until time_timeout is reached while (_time() < time_timeout) { % try to link lockfile to the previously created temporary file, % link(2) is atomic even on NFSv2 if the lockfile exists link(2) % will fail, this is either detected if EEXIST is returned or the % link count of the temporary file is not 2 in this case try to % remove a stale lockfile, then wait and try again ifnot ((hardlink(tmp_lockfile, lockfile) == 0) || (errno == EEXIST)) return NULL; st = stat_file(tmp_lockfile); if (st == NULL) return NULL; if (st.st_nlink == 2) return lockfile; st = stat_file(lockfile); if (st == NULL) { ifnot (errno == ENOENT) return NULL; else continue; } % remove a stale lockfile after stale_timeout seconds have passed if (st.st_mtime < time_stale_timeout) () = remove(lockfile); sleep(2); } return NULL; } finally { () = remove(tmp_lockfile); } } static variable config = struct { tag_path = ".slrn-tags", autosave = 1 }; private variable tag_list = Assoc_Type[Null_Type]; private define update_tag_list(ref_tag_list) { variable new_tag_list = Assoc_Type[Null_Type]; variable needs_update = 0; variable msgid = NULL; call("header_bob"); % the very first article must be treated specially so it will not be missed % when doing next_tagged_header() while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) || next_tagged_header()) { msgid = extract_article_header("Message-ID"); if (strlen(msgid) == 0) continue; new_tag_list[msgid] = NULL; % check if a new element which will go into new_tag_list also exists % in tag_list in order to find any difference between both lists ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid)) needs_update = 1; } % if all elements of new_tag_list are also contained in tag_lists % check whether all elements of tag_lists are also contained in % new_tag_lists in order to find any difference between both lists ifnot (needs_update) { foreach msgid(@ref_tag_list) using("keys") { ifnot (assoc_key_exists(new_tag_list, msgid)) { needs_update = 1; break; } } } % replace tag_list with new_tag_list @ref_tag_list = new_tag_list; return needs_update; } static define save_tags() { variable line, fp; variable dir = path_concat(make_home_filename(config.tag_path), urlencode(server_name())); variable filename = path_concat(dir, urlencode(current_newsgroup())); variable lockfile; ifnot (update_tag_list(&tag_list)) return; if (mkdir(dir) == -1) { ifnot (errno == EEXIST) throw IOError, "mkdir $dir failed: "$ + errno_string(errno); } lockfile = lock_file(filename); if (lockfile == NULL) throw IOError, "failed to lock $filename"$; try { % if tag_list is empty remove the file if (length(tag_list) == 0) { () = remove(filename); return; } fp = fopen(filename, "w"); if (fp == NULL) throw OpenError, "opening $filename failed: "$ + errno_string(errno); foreach line(tag_list) using("keys") { if (fputs(line + "\n", fp) == -1) throw WriteError, "writing to $filename failed: "$ + errno_string(errno); } () = fclose(fp); } finally { () = remove(lockfile); } return; } static define load_tags() { variable fp, buf, msgid, pos; variable dir = path_concat(make_home_filename(config.tag_path), urlencode(server_name())); variable filename = path_concat(dir, urlencode(current_newsgroup())); % re-ininitalize tag_list tag_list = Assoc_Type[Null_Type]; fp = fopen(filename, "r"); if (fp == NULL) { if (errno == ENOENT) return; throw OpenError, "opening $filename failed: "$ + errno_string(errno); } % save position to restore after applying tags pos = extract_article_header("Message-ID"); while (fgets(&buf, fp) != -1) { msgid = strtrim(buf); if (strlen(msgid)) { tag_list[msgid] = NULL; if (locate_header_by_msgid(msgid, 1)) set_header_flags(get_header_flags() | HEADER_TAGGED); } } () = fclose(fp); () = locate_header_by_msgid(pos, 0); return; } mysrand(_time() * getpid()); if (config.autosave) { () = register_hook("article_mode_hook", "PersistentTags->load_tags"); () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags"); }