# $Dwarf: newsrc.rb,v 1.13 2004/06/16 08:16:58 ward Exp $ # $Source$ # # Copyright (c) 2002, 2003 Ward Wouts # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # require Pathname.new(__FILE__).dirname + "../set/intspan" #require "thread" module News class Newsrc #@@save_lock = Mutex.new def initialize(file=nil) @newsrc = { "group" => Hash.new, "list" => Array.new } if file unless load(file) puts "Can't load #{file}" exit end end end def load(file=nil) file = "#{ENV['HOME']}/.newsrc" unless file @newsrc["file"] = file @newsrc["group"] = {} @newsrc["list"] = [] if FileTest.file?( "#{file}" ) and FileTest.readable?( "#{file}" ) lines = IO.readlines("#{file}") import_rc(lines) end return true end def import_rc(lines) @newsrc["group"] = {} @newsrc["list"] = [] linenumber = 1 lines.each{|line| parse(line) } end def parse(line) unless line =~ /^([^!:]+)([!:])\s(.*)$/x puts "Newsrc.parse: Bad newsrc line: #{line}" exit end name = $1 mark = $2 articles = $3 unless Set::IntSpan.valid(articles) puts "Newsrc.parse: Bad article list: #{line}" end group = { "name" => name, "subscribed" => (mark == ":"), "articles" => Set::IntSpan.new(articles)} @newsrc["group"][name] = group @newsrc["list"].push(group) end def save unless @newsrc.has_key?("file") @newsrc["file"] = "#{$ENV['HOME']}/.newsrc" end save_as(@newsrc["file"]) end # this is not thread safe! (well, it should be now) def save_as(file) # @@save_lock.synchronize{ if FileTest.exists?("#{file}") begin FileUtils.mv(file, "#{file}.bak") rescue puts "Can't rename #{file}, #{file}.bak: #{$!}" exit end end begin newsrc = File.new(file, "w") newsrc.flock(File::LOCK_EX) rescue puts "Can't open #{file}: #{$!}" exit end @newsrc["file"] = file @newsrc["list"].each{|group| newsrc.print format(group) } newsrc.sync newsrc.flock(File::LOCK_UN) # what's the right order here? newsrc.close # } end # Here 'group' is a group structure. It'd probably be much more useful if # it could just be a group_name_; which it can now. def save_group(group) unless @newsrc.has_key?("file") @newsrc["file"] = "#{$ENV['HOME']}/.newsrc" end if group.class.to_s == "String" groupname = group.dup @newsrc["list"].each{|g| if g["name"] == groupname group = g.dup break end } end save_group_as(@newsrc["file"], group) end # This should be thread safe, but may not be. It needs testing! # If not, mutexes are needed. def save_group_as(file, group) # @@save_lock.synchronize{ #p Time.now #p "copy file" if FileTest.exists?("#{file}") begin FileUtils.copy(file, "#{file}.bak") rescue puts "Can't copy #{file} to #{file}.bak: #{$!}" end end #p Time.now #p "open & lock file" begin if FileTest.exists?("#{file}") newsrc = File.new(file, "r+") else newsrc = File.new(file, "w") end newsrc.flock(File::LOCK_EX) rescue puts "Can't open #{file}: #{$!}" exit end #p Time.now #p "opened & locked" # read file lines = newsrc.readlines # pointer -> 0 newsrc.rewind group_saved = false # write read stuff & replace group lines.each{|line| # same parsing as the parse method uses unless line =~ /^([^!:]+)([!:])\s(.*)$/x puts "Newsrc.parse: Bad newsrc line: #{line}" # restore backup on failure, it'll contain the flaw too, but it'll # be complete begin FileUtils.copy("#{file}.bak", file) rescue puts "Can't copy #{file}.bak to #{file}: #{$!}" end exit end linegroup = $1 if linegroup == group["name"] newsrc.print format(group) group_saved = true else newsrc.print line end } if ! group_saved newsrc.print format(group) end #p Time.now #p "truncate, sync, unlock & close file" # sometimes the file grows and then shrinks # this is because a 'read' line van become shorter when more # articles have been read (1,3,5 vs 1-5) # when this happens the file needs to be truncated pos = newsrc.pos newsrc.truncate(pos) newsrc.sync newsrc.flock(File::LOCK_UN) # what's the right order here? newsrc.close #p Time.now #p "garbage collect" #p Time.now GC.start # } end def format(group) name = group["name"] sub = group["subscribed"] ? ':' : '!' articles = group["articles"].run_list return "#{name}#{sub} #{articles}\n" end def export_rc lines = @newsrc["list"].collect{ |group| name = group["name"] sub = group["subscribed"] ? ':' : '!' articles = group["articles"].run_list space = articles ? ' ' : '' "#{name}#{sub}#{space}#{articles}\n" } return lines end def add_group(name, options) if @newsrc["group"].has_key?(name) options.has_key?("replace") or return false del_group(name) end group = {"name" => name, "subscribed" => true, "articles" => Set::IntSpan.new } @newsrc["group"][name] = group _insert(group, options) return true end def move_group(name, options) if @newsrc["group"].has_key?(name) group = @newsrc["group"][name] else return false end @newsrc["list"] = @newsrc["list"].delete_if{|x| x["name"] == name} _insert(group, options) return true end def _insert(group, options) list = @newsrc["list"] where = "" arg = "" if options.has_key?("where") where = options["where"] end arg = where.slice!(1) if where.class.to_s == "Array" case where.to_s when "first" @newsrc["list"].unshift(group) when "last" @newsrc["list"].push(group) when "" @newsrc["list"].push(group) # default when "alpha" alpha(group) when "before" before(group, arg) when "after" after(group, arg) when "number" number(group, arg) end end def alpha (group) name = group["name"] (0...@newsrc["list"].length).each{|i| if ((name <=> @newsrc["list"][i]["name"]) == -1) upper = @newsrc["list"].slice!(i..@newsrc["list"].length) @newsrc["list"].push(group) @newsrc["list"].push(upper) return; end } @newsrc["list"].push(group) end def before(group, before) name = group["name"] (0...@newsrc["list"].length).each{|i| if (@newsrc["list"][i]["name"] == before.to_s) upper = @newsrc["list"].slice!(i..@newsrc["list"].length) @newsrc["list"].push(group) @newsrc["list"].push(upper) return; end } @newsrc["list"].push(group) end def after(group, after) name = group["name"] (0...@newsrc["list"].length).each{|i| if (@newsrc["list"][i]["name"] == after.to_s) upper = @newsrc["list"].slice!((i+1)..@newsrc["list"].length) @newsrc["list"].push(group) @newsrc["list"].push(upper) return; end } @newsrc["list"].push(group) end def number(group, offset) offset = @newsrc["list"].length if offset[0] > @newsrc["list"].length upper = @newsrc["list"].slice!(offset..@newsrc["list"].length) @newsrc["list"].push(group) @newsrc["list"].push(upper) end def del_group(name) if @newsrc["group"].has_key?(name) group = @newsrc["group"][name] else return false end @newsrc["group"].delete(name) @newsrc["list"] = @newsrc["list"].delete_if{|x| x["name"] == name} return true end def subscribe(name, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["subscribed"] = true end def unsubscribe(name, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["subscribed"] = false end def mark(name, article, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["articles"].insert!(article) end def mark_list(name, list, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end articles = @newsrc["group"][name]["articles"].union(list) @newsrc["group"][name]["articles"] = articles end def mark_range(name, from, to, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end range = Set::IntSpan.new("#{from}-#{to}") articles = @newsrc["group"][name]["articles"].union(range) @newsrc["group"][name]["articles"] = articles end def unmark(name, article, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["articles"].remove(article) end def unmark_list(name, list, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end articles = @newsrc["group"][name]["articles"].diff(list) @newsrc["group"][name]["articles"] = articles end def unmark_range(name, from, to, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end range = Set::IntSpan.new("#{from}-#{to}") articles = @newsrc["group"][name]["articles"].diff(range) @newsrc["group"][name]["articles"] = articles end def exists(name) return @newsrc["group"].has_key?(name) ? true : false end def subscribed(name) exists(name) and @newsrc["group"][name]["subscribed"] end def marked(name, article) exists(name) and @newsrc["group"][name]["articles"].member?(article) end def num_groups return @newsrc["list"].length end def groups list = @newsrc["list"].dup list.collect!{|x| x["name"]} end def sub_groups list = @newsrc["list"].dup list.collect!{|x| x["subscribed"] ? x["name"] : nil}.compact! end def unsub_groups list = @newsrc["list"].dup list.collect!{|x| x["subscribed"] ? nil : x["name"]}.compact! end def marked_articles(name, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end return @newsrc["group"][name]["articles"].elements end def unmarked_articles(name, from, to, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end range = Set::IntSpan.new("#{from}-#{to}") return range.diff(@newsrc["group"][name]["articles"]).elements end def get_articles(name, options = {"where" => ""}) unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["articles"].run_list end def set_articles(name, articles, options = {"where" => ""}) Set::IntSpan.valid(articles) or return false set = Set::IntSpan.new(articles) set.finite or return false min = set.min min != nil and min < 0 and return false unless @newsrc["group"].has_key?(name) add_group(name, options) end @newsrc["group"][name]["articles"] = set return true end end # class end # module # TODO # Do not kill an item until it's tested! # [x] new # [x] load # [ ] _scan # Initializes a Newsrc object from a string. Used for testing. # [x] import_rc # [x] parse # parses a single line from a newsrc file # [x] save # [x] save_as # [ ] save_group # [ ] save_group_as # [x] format # [x] export_rc # [ ] _dump # Formats a Newsrc object to a string. Used for testing # [x] add_group # [x] move_group # [x] Splice(\@$$@) # heet nu number en is simpeler # [x] _insert # [x] Alpha # [x] Before # [x] After # [x] del_group # [x] subscribe # [x] unsubscribe # [x] mark # [x] mark_list # [x] mark_range # [x] unmark # [x] unmark_list # [x] unmark_range # [x] exists # [x] subscribed # [x] marked # [x] num_groups # [x] groups # [x] sub_groups # [x] unsub_groups # [x] marked_articles # [x] unmarked_articles # [x] get_articles # [x] set_articles