]> git.cworth.org Git - sup/blob - bin/sup-sync-back
refactor index locking interaction and replace suicidemanager
[sup] / bin / sup-sync-back
1 #!/usr/bin/env ruby
2
3 require 'rubygems'
4 require 'uri'
5 require 'tempfile'
6 require 'trollop'
7 require 'enumerator'
8 require "sup"
9
10 ## save a message 'm' to an open file pointer 'fp'
11 def save m, fp
12   m.source.each_raw_message_line(m.source_info) { |l| fp.print l }
13 end
14 def die msg
15   $stderr.puts "Error: #{msg}"
16   exit(-1)
17 end
18 def has_any_from_source_with_label? index, source, label
19   query = { :source_id => source.id, :label => label, :limit => 1, :load_spam => true, :load_deleted => true, :load_killed => true }
20   not Enumerable::Enumerator.new(index, :each_id, query).map.empty?
21 end
22
23 opts = Trollop::options do
24   version "sup-sync-back (sup #{Redwood::VERSION})"
25   banner <<EOS
26 Drop or move messages from Sup sources that are marked as deleted or
27 spam in the Sup index.
28
29 Currently only works with mbox sources.
30
31 Usage:
32   sup-sync-back [options] <source>*
33
34 where <source>* is zero or more source URIs. If no sources are given,
35 sync back all usual sources.
36
37 You almost certainly want to run sup-sync --changed after this command.
38 Running this does not change the index.
39
40 Options include:
41 EOS
42   opt :drop_deleted, "Drop deleted messages.", :default => false, :short => "d"
43   opt :move_deleted, "Move deleted messages to a local mbox file.", :type => String, :short => :none
44   opt :drop_spam, "Drop spam messages.", :default => false, :short => "s"
45   opt :move_spam, "Move spam messages to a local mbox file.", :type => String, :short => :none
46
47   opt :with_dotlockfile, "Specific dotlockfile location (mbox files only).", :default => "/usr/bin/dotlockfile", :short => :none
48   opt :dont_use_dotlockfile, "Don't use dotlockfile to lock mbox files. Dangerous if other processes modify them concurrently.", :default => false, :short => :none
49
50   opt :verbose, "Print message ids as they're processed."
51   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
52   opt :version, "Show version information", :short => :none
53
54   conflicts :drop_deleted, :move_deleted
55   conflicts :drop_spam, :move_spam
56 end
57
58 unless opts[:drop_deleted] || opts[:move_deleted] || opts[:drop_spam] || opts[:move_spam]
59   puts <<EOS
60 Nothing to do. Please specify at least one of --drop-deleted, --move-deleted,
61 --drop-spam, or --move-spam.
62 EOS
63
64   exit
65 end
66
67 Redwood::start
68 index = Redwood::Index.new
69 index.lock_interactively or exit
70
71 deleted_fp, spam_fp = nil
72 unless opts[:dry_run]
73   deleted_fp = File.open(opts[:move_deleted], "a") if opts[:move_deleted] 
74   spam_fp = File.open(opts[:move_spam], "a") if opts[:move_spam]
75 end
76
77 dotlockfile = opts[:with_dotlockfile] || "/usr/bin/dotlockfile"
78
79 begin
80   index.load
81
82   sources = ARGV.map do |uri|
83     s = Redwood::SourceManager.source_for(uri) or die "unknown source: #{uri}. Did you add it with sup-add first?"
84     s.is_a?(Redwood::MBox::Loader) or die "#{uri} is not an mbox source."
85     s
86   end
87
88   if sources.empty?
89     sources = Redwood::SourceManager.usual_sources.select { |s| s.is_a? Redwood::MBox::Loader }
90   end
91
92   unless sources.all? { |s| s.file_path.nil? } || File.executable?(dotlockfile) || opts[:dont_use_dotlockfile]
93     die <<EOS
94 can't execute dotlockfile binary: #{dotlockfile}. Specify --with-dotlockfile
95 if it's in a nonstandard location, or, if you want to live dangerously, try
96 --dont-use-dotlockfile
97 EOS
98   end
99
100   modified_sources = []
101   sources.each do |source|
102     $stderr.puts "Scanning #{source}..."
103
104     unless ((opts[:drop_deleted] || opts[:move_deleted]) && has_any_from_source_with_label?(index, source, :deleted)) || ((opts[:drop_spam] || opts[:move_spam]) && has_any_from_source_with_label?(index, source, :spam))
105       $stderr.puts "Nothing to do from this source; skipping"
106       next
107     end
108
109     source.reset!
110     num_dropped = num_moved = num_scanned = 0
111     
112     out_fp = Tempfile.new "sup-sync-back-#{source.id}"
113     Redwood::PollManager.add_messages_from source do |m_old, m, offset|
114       num_scanned += 1
115
116       if m_old
117         labels = m_old.labels
118
119         if labels.member? :deleted
120           if opts[:drop_deleted]
121             puts "Dropping deleted message #{source}##{offset}" if opts[:verbose]
122             num_dropped += 1
123           elsif opts[:move_deleted] && labels.member?(:deleted)
124             puts "Moving deleted message #{source}##{offset}" if opts[:verbose]
125             save m, deleted_fp unless opts[:dry_run]
126             num_moved += 1
127           end
128
129         elsif labels.member? :spam
130           if opts[:drop_spam]
131             puts "Dropping spam message #{source}##{offset}" if opts[:verbose]
132             num_dropped += 1
133           elsif opts[:move_spam] && labels.member?(:spam)
134             puts "Moving spam message #{source}##{offset}" if opts[:verbose]
135             save m, spam_fp unless opts[:dry_run]
136             num_moved += 1
137           end
138         else
139           save m, out_fp unless opts[:dry_run]
140         end
141       else
142         save m, out_fp unless opts[:dry_run]
143       end
144
145       nil # don't actually add anything!
146     end
147     $stderr.puts "Scanned #{num_scanned}, dropped #{num_dropped}, moved #{num_moved} messages from #{source}."
148     modified_sources << source if num_dropped > 0 || num_moved > 0
149     out_fp.close unless opts[:dry_run]
150
151     unless opts[:dry_run] || (num_dropped == 0 && num_moved == 0)
152       deleted_fp.flush if deleted_fp
153       spam_fp.flush if spam_fp
154       unless opts[:dont_use_dotlockfile]
155         puts "Locking #{source.file_path}..."
156         system "#{opts[:dotlockfile]} -l #{source.file_path}"
157         puts "Writing #{source.file_path}..."
158         FileUtils.cp out_fp.path, source.file_path
159         puts "Unlocking #{source.file_path}..."
160         system "#{opts[:dotlockfile]} -u #{source.file_path}"
161       end
162     end
163   end
164
165   unless opts[:dry_run]
166     deleted_fp.close if deleted_fp
167     spam_fp.close if spam_fp
168   end
169
170   $stderr.puts "Done."
171   unless modified_sources.empty?
172     $stderr.puts "You should now run: sup-sync --changed #{modified_sources.join(' ')}"
173   end
174 rescue Exception => e
175   File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
176   raise
177 ensure
178   Redwood::finish
179   index.unlock
180 end