]> git.cworth.org Git - sup/blob - bin/sup-tweak-labels
refactor index locking interaction and replace suicidemanager
[sup] / bin / sup-tweak-labels
1 #!/usr/bin/env ruby
2
3 require 'rubygems'
4 require 'trollop'
5 require 'enumerator'
6 require "sup"
7
8 class Float
9   def to_s; sprintf '%.2f', self; end
10    def to_time_s
11      infinite? ? "unknown" : super
12    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 def time
23   startt = Time.now
24   yield
25   Time.now - startt
26 end
27
28 opts = Trollop::options do
29   version "sup-tweak-labels (sup #{Redwood::VERSION})"
30   banner <<EOS
31 Batch modification of message state for messages already in the index.
32
33 Usage:
34   sup-tweak-labels [options] <source>*
35
36 where <source>* is zero or more source URIs. Supported source URI schemes can
37 be seen by running "sup-add --help".
38
39 Options:
40 EOS
41   opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :type => String
42   opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :type => String
43   opt :query, "A Sup search query", :type => String
44
45   text <<EOS
46
47 Other options:
48 EOS
49   opt :verbose, "Print message ids as they're processed."
50   opt :very_verbose, "Print message names and subjects as they're processed."
51   opt :all_sources, "Scan over all sources.", :short => :none
52   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
53   opt :version, "Show version information", :short => :none
54 end
55 opts[:verbose] = true if opts[:very_verbose]
56
57 add_labels = (opts[:add] || "").split(",").map { |l| l.intern }.uniq
58 remove_labels = (opts[:remove] || "").split(",").map { |l| l.intern }.uniq
59
60 Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
61 Trollop::die "no sources specified" if ARGV.empty?
62
63 Redwood::start
64 index = Redwood::Index.new
65 index.lock_interactively or exit
66 begin
67   index.load
68
69   source_ids = 
70     if opts[:all_sources]
71       Redwood::SourceManager.sources
72     else
73       ARGV.map do |uri|
74         Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
75       end
76   end.map { |s| s.id }
77   Trollop::die "nothing to do: no sources" if source_ids.empty?
78
79   query = "+(" + source_ids.map { |id| "source_id:#{id}" }.join(" OR ") + ")"
80   if add_labels.empty?
81     ## if all we're doing is removing labels, we can further restrict the
82     ## query to only messages with those labels
83     query += " +(" + remove_labels.map { |l| "label:#{l}" }.join(" ") + ")"
84   end
85   query += ' ' + opts[:query] if opts[:query]
86
87   parsed_query = index.parse_query query
88   parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
89   ids = Enumerable::Enumerator.new(index, :each_id, parsed_query).map
90   num_total = ids.size
91
92   $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
93
94   num_changed = num_scanned = 0
95   last_info_time = start_time = Time.now
96   ids.each do |id|
97     num_scanned += 1
98
99     m = index.build_message id
100     old_labels = m.labels.clone
101
102     m.labels += add_labels
103     m.labels -= remove_labels
104     m.labels = m.labels.uniq
105
106     unless m.labels.sort_by { |s| s.to_s } == old_labels.sort_by { |s| s.to_s }
107       num_changed += 1
108       puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
109       puts "#{m.id}: {#{old_labels.join ','}} => {#{m.labels.join ','}}" if opts[:verbose]
110       puts if opts[:very_verbose]
111       index.sync_message m unless opts[:dry_run]
112     end
113
114     if Time.now - last_info_time > 60
115       last_info_time = Time.now
116       elapsed = last_info_time - start_time
117       pctdone = 100.0 * num_scanned.to_f / num_total.to_f
118       remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
119       $stderr.puts "## #{num_scanned} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
120     end
121   end
122   $stderr.puts "Scanned #{num_scanned} / #{num_total} messages and changed #{num_changed}."
123
124   unless num_changed == 0
125     $stderr.puts "Optimizing index..."
126     index.optimize unless opts[:dry_run]
127   end
128
129 rescue Exception => e
130   File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
131   raise
132 ensure
133   index.save
134   Redwood::finish
135   index.unlock
136 end
137