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