show progress in sup-sync
[sup:mainline.git] / bin / sup-sync
1 #!/usr/bin/env ruby
2
3 require 'uri'
4 require 'rubygems'
5 require 'trollop'
6 require "sup"; Redwood::check_library_version_against "git"
7
8 PROGRESS_UPDATE_INTERVAL = 15 # seconds
9
10 class Float
11   def to_s; sprintf '%.2f', self; end
12   def to_time_s; infinite? ? "unknown" : super end
13 end
14
15 class Numeric
16   def to_time_s
17     i = to_i
18     sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
19   end
20 end
21
22 class Set
23   def to_s; to_a * ',' end
24 end
25
26 def time
27   startt = Time.now
28   yield
29   Time.now - startt
30 end
31
32 opts = Trollop::options do
33   version "sup-sync (sup #{Redwood::VERSION})"
34   banner <<EOS
35 Synchronizes the Sup index with one or more message sources by adding
36 messages, deleting messages, or changing message state in the index as
37 appropriate.
38
39 "Message state" means read/unread, archived/inbox, starred/unstarred,
40 and all user-defined labels on each message.
41
42 "Default source state" refers to any state that a source itself has
43 keeps about a message. Sup-sync uses this information when adding a
44 new message to the index. The source state is typically limited to
45 read/unread, archived/inbox status and a single label based on the
46 source name. Messages using the default source state are placed in
47 the inbox (i.e. not archived) and unstarred.
48
49 Usage:
50   sup-sync [options] <source>*
51
52 where <source>* is zero or more source URIs. If no sources are given,
53 sync from all usual sources. Supported source URI schemes can be seen
54 by running "sup-add --help".
55
56 Options controlling HOW message state is altered:
57 EOS
58   opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
59   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
60   opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
61   opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
62   opt :read, "When using the default source state, mark messages as read."
63   opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none
64
65 text <<EOS
66
67 Other options:
68 EOS
69   opt :verbose, "Print message ids as they're processed."
70   opt :optimize, "As the final operation, optimize the index."
71   opt :all_sources, "Scan over all sources.", :short => :none
72   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
73   opt :version, "Show version information", :short => :none
74
75   conflicts :asis, :restore, :discard
76 end
77
78 op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
79
80 Redwood::start
81 index = Redwood::Index.init
82
83 restored_state = if opts[:restore]
84   dump = {}
85   puts "Loading state dump from #{opts[:restore]}..."
86   IO.foreach opts[:restore] do |l|
87     l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
88     mid, labels = $1, $2
89     dump[mid] = labels.to_set_of_symbols
90   end
91   puts "Read #{dump.size} entries from dump file."
92   dump
93 else
94   {}
95 end
96
97 seen = {}
98 index.lock_interactively or exit
99 begin
100   index.load
101
102   if(s = Redwood::SourceManager.source_for Redwood::SentManager.source_uri)
103     Redwood::SentManager.source = s
104   else
105     Redwood::SourceManager.add_source Redwood::SentManager.default_source
106   end
107
108   sources = if opts[:all_sources]
109     Redwood::SourceManager.sources
110   elsif ARGV.empty?
111     Redwood::SourceManager.usual_sources
112   else
113     ARGV.map do |uri|
114       Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
115     end
116   end
117
118   sources.each do |source|
119     puts "Scanning #{source}..."
120     num_added = num_updated = num_scanned = num_restored = 0
121     last_info_time = start_time = Time.now
122
123     Redwood::PollManager.poll_from source do |action,m,old_m,progress|
124       if action == :delete
125         puts "Deleting #{m.id}"
126       elsif action == :add
127         num_scanned += 1
128         seen[m.id] = true
129
130         ## tweak source labels according to commandline arguments if necessary
131         m.labels.delete :inbox if opts[:archive]
132         m.labels.delete :unread if opts[:read]
133         m.labels += opts[:extra_labels].to_set_of_symbols(",")
134
135         ## decide what to do based on message labels and the operation we're performing
136         dothis = case
137         when (op == :restore) && restored_state[m.id]
138           if old_m && (old_m.labels != restored_state[m.id])
139             num_restored += 1
140             m.labels = restored_state[m.id]
141             :update_message_state
142           elsif old_m.nil?
143             num_restored += 1
144             m.labels = restored_state[m.id]
145             :add_message
146           else
147             # labels are the same; don't do anything
148           end
149         when op == :discard
150           if old_m && (old_m.labels != m.labels)
151             :update_message_state
152           else
153             # labels are the same; don't do anything
154           end
155         else
156           if old_m
157             :update_message
158           else
159             :add_message
160           end
161         end
162
163         ## now, actually do the operation
164         case dothis
165         when :add_message
166           puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
167           num_added += 1
168         when :update_message
169           puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
170           num_updated += 1
171         when :update_message_state
172           puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose]
173           num_updated += 1
174         end
175
176         if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
177           last_info_time = Time.now
178           elapsed = last_info_time - start_time
179           pctdone = progress * 100.0
180           remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
181           printf "## read %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
182         end
183       else fail
184       end
185       next if opts[:dry_run]
186     end
187
188     puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
189     puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
190   end
191
192   index.save
193
194   if opts[:optimize]
195     puts "Optimizing index..."
196     optt = time { index.optimize unless opts[:dry_run] }
197     puts "Optimized index of size #{index.size} in #{optt}s."
198   end
199 rescue Redwood::FatalSourceError => e
200   $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
201 rescue Exception => e
202   File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
203   raise
204 ensure
205   Redwood::finish
206   index.unlock
207 end