#!/usr/bin/env ruby $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) require 'uri' require 'rubygems' require 'trollop' require "sup" PROGRESS_UPDATE_INTERVAL = 15 # seconds class Float def to_s; sprintf '%.2f', self; end def to_time_s; infinite? ? "unknown" : super end end class Numeric def to_time_s i = to_i sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60 end end class Set def to_s; to_a * ',' end end def time startt = Time.now yield Time.now - startt end opts = Trollop::options do version "sup-sync (sup #{Redwood::VERSION})" banner <* where * is zero or more source URIs. If no sources are given, sync from all usual sources. Supported source URI schemes can be seen by running "sup-add --help". Options controlling HOW message state is altered: EOS opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none opt :restore, "Restore message state from a dump file created with sup-dump. If a message is not in this dumpfile, act as --asis.", :type => String, :short => :none opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none opt :archive, "When using the default source state, mark messages as archived.", :short => "-x" opt :read, "When using the default source state, mark messages as read." opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none text < :none opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n" opt :version, "Show version information", :short => :none conflicts :asis, :restore, :discard end op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis Redwood::start index = Redwood::Index.init restored_state = if opts[:restore] dump = {} puts "Loading state dump from #{opts[:restore]}..." IO.foreach opts[:restore] do |l| l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}" mid, labels = $1, $2 dump[mid] = labels.to_set_of_symbols end puts "Read #{dump.size} entries from dump file." dump else {} end seen = {} index.lock_interactively or exit begin index.load if(s = Redwood::SourceManager.source_for Redwood::SentManager.source_uri) Redwood::SentManager.source = s else Redwood::SourceManager.add_source Redwood::SentManager.default_source end sources = if opts[:all_sources] Redwood::SourceManager.sources elsif ARGV.empty? Redwood::SourceManager.usual_sources else ARGV.map do |uri| Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?" end end sources.each do |source| puts "Scanning #{source}..." num_added = num_updated = num_deleted = num_scanned = num_restored = 0 last_info_time = start_time = Time.now Redwood::PollManager.poll_from source do |action,m,old_m,progress| num_scanned += 1 if action == :delete num_deleted += 1 puts "Deleting #{m.id}" if opts[:verbose] elsif action == :add seen[m.id] = true ## tweak source labels according to commandline arguments if necessary m.labels.delete :inbox if opts[:archive] m.labels.delete :unread if opts[:read] m.labels += opts[:extra_labels].to_set_of_symbols(",") ## decide what to do based on message labels and the operation we're performing dothis = case when (op == :restore) && restored_state[m.id] if old_m && (old_m.labels != restored_state[m.id]) num_restored += 1 m.labels = restored_state[m.id] :update_message_state elsif old_m.nil? num_restored += 1 m.labels = restored_state[m.id] :add_message else # labels are the same; don't do anything end when op == :discard if old_m && (old_m.labels != m.labels) :update_message_state else # labels are the same; don't do anything end else if old_m :update_message else :add_message end end ## now, actually do the operation case dothis when :add_message puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose] num_added += 1 when :update_message puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose] num_updated += 1 when :update_message_state puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose] num_updated += 1 end else fail "sup-sync cannot handle :update's" end if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL last_info_time = Time.now elapsed = last_info_time - start_time pctdone = progress * 100.0 remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone) printf "## scanned %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s end next if opts[:dry_run] end puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated}, deleted #{num_deleted} messages from #{source}." puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0 end index.save if opts[:optimize] puts "Optimizing index..." optt = time { index.optimize unless opts[:dry_run] } puts "Optimized index of size #{index.size} in #{optt}s." end rescue Redwood::FatalSourceError => e $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}" rescue Exception => e File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace } raise ensure Redwood::finish index.unlock end