]> git.cworth.org Git - sup/blob - bin/sup-import
c7fe2ad2a99e1f0ef348229266358f9644995093
[sup] / bin / sup-import
1 #!/usr/bin/env ruby
2
3 require "sup"
4
5 class Float
6   def to_s; sprintf '%.2f', self; end
7 end
8
9 class Numeric
10   def to_time_s
11     i = to_i
12     sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
13   end
14 end
15
16 def time
17   startt = Time.now
18   yield
19   Time.now - startt
20 end
21
22 def educate_user
23   $stderr.puts <<EOS
24 Loads messages into the Sup index, adding sources as needed to the
25 source list.
26
27 Usage:
28   sup-import [options] <source>*
29 where <source>* is zero or more source descriptions (e.g., mbox
30 filenames on disk). 
31
32 If the sources listed are not already in the Sup source list,
33 they will be added to it, as parameterized by the following options:
34   --archive: messages from these sources will not appear in the inbox
35   --unusual: these sources will not be polled when the flag --the-usual
36              is called
37
38 Regardless of whether the sources are new or not, they will be polled,
39 and any new messages will be added to the index, as parameterized by
40 the following options:
41   --force-archive: regardless of the source "archive" flag, any new
42                    messages found will not appear in the inbox.
43   --force-read:    any messages found will not be marked as new.
44
45 The following options can also be specified:
46   --the-usual:     import new messages from all usual sources
47   --rebuild:       rebuild the index for the specified sources rather than
48                    just adding new messages. Useful if the sources
49                    have changed in any way *other* than new messages
50                    being added.
51   --force-rebuild: force a rebuild of all messages in the inbox, not just
52                    ones that have changed. You probably won't need this
53                    unless William changes the index format.
54   --optimize:      optimize the index after adding any new messages.
55   --help:          don't do anything, just show this message.
56 EOS
57   exit
58 end
59
60 educate_user if ARGV.member? '--help'
61
62 archive = ARGV.delete "--archive"
63 unusual = ARGV.delete "--unusual"
64 force_archive = ARGV.delete "--force-archive"
65 force_read = ARGV.delete "--force-read"
66 the_usual = ARGV.delete "--the-usual"
67 rebuild = ARGV.delete "--rebuild"
68 force_rebuild = ARGV.delete "--force-rebuild"
69 optimize = ARGV.delete "--optimize"
70 start_at = # ok really need to use optparse or something now
71   if(i = ARGV.index("--start-at"))
72     raise "start-at requires a numeric argument: #{ARGV[i + 1].inspect}" unless ARGV.length > (i + 1) && ARGV[i + 1] =~ /\d/
73     ARGV.delete_at i
74     ARGV.delete_at(i).to_i # whoa!
75   end
76
77 if(o = ARGV.find { |x| x =~ /^--/ })
78   $stderr.puts "error: unknown option #{o}"
79   educate_user
80 end
81
82 puts "loading index..."
83 index = Redwood::Index.new
84 index.load
85 puts "loaded index of #{index.size} messages"
86
87 sources = ARGV.map do |fn|
88   source = index.source_for fn
89   unless source
90     source = 
91       case fn
92       when %r!^imaps?://!
93         print "Username for #{fn}: "
94         username = $stdin.gets.chomp
95         print "Password for #{fn} (warning: cleartext): "
96         password = $stdin.gets.chomp
97         Redwood::IMAP.new(fn, username, password, nil, !unusual, !!archive)
98       else
99         Redwood::MBox::Loader.new(fn, nil, !unusual, !!archive)
100       end
101     index.add_source source
102   end
103   source
104 end
105
106 sources = (sources + index.usual_sources).uniq if the_usual
107 if rebuild || force_rebuild
108   if start_at
109     sources.each { |s| s.seek_to! start_at }
110   else
111     sources.each { |s| s.reset! }
112   end
113 end
114
115 found = {}
116 start = Time.now
117 begin
118   sources.each do |source|
119     next if source.done?
120     puts "loading from #{source}... "
121     num = 0
122     start_offset = nil
123     source.each do |offset, labels|
124       start_offset ||= offset
125       labels -= [:inbox] if force_archive
126       labels -= [:unread] if force_read
127       begin
128         m = Redwood::Message.new :source => source, :source_info => offset, :labels => labels
129         if found[m.id]
130           puts "skipping duplicate message #{m.id}"
131           next
132         else
133           found[m.id] = true
134         end
135         m.remove_label :unread if m.status == "RO" unless force_read
136         puts "# message at #{offset} labels #{labels.inspect}" unless rebuild || force_rebuild
137         if (rebuild || force_rebuild) && 
138             (docid, entry = index.load_entry_for_id(m.id)) && entry
139           if force_rebuild || entry[:source_info].to_i != offset
140             puts "replacing message #{m.id} labels #{entry[:label].inspect} (offset #{entry[:source_info]} => #{offset})"
141             m.labels = entry[:label].split.map { |l| l.intern }
142             num += 1 if index.update_message m, source, offset
143           end
144         else
145           num += 1 if index.add_message m
146         end
147       rescue Redwood::MessageFormatError, Redwood::MBox::Error => e
148         $stderr.puts "ignoring erroneous message at #{source}##{offset}: #{e.message}"
149       end
150       if num % 1000 == 0 && num > 0
151         elapsed = Time.now - start
152         pctdone = (offset.to_f - start_offset) / (source.total.to_f - start_offset)
153         remaining = (source.total.to_f - offset.to_f) * (elapsed.to_f / (offset.to_f - start_offset))
154         puts "## #{num} (#{(pctdone * 100.0)}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining"
155       end
156     end
157     puts "loaded #{num} messages" unless num == 0
158   end
159 ensure
160   index.save
161 end
162
163 if rebuild || force_rebuild
164   puts "deleting missing messages from the index..."
165   numdel = num = 0
166   sources.each do |source|
167     raise "no source id for #{source}" unless source.id
168     q = "+source_id:#{source.id}"
169     q += " +source_info: >= #{start_at}" if start_at
170     #p q
171     num += index.index.search_each(q, :limit => :all) do |docid, score|
172       mid = index.index[docid][:message_id]
173       next if found[mid]
174       puts "deleting #{mid}"
175       index.index.delete docid
176       numdel += 1
177     end
178     #p num
179   end
180   puts "deleted #{numdel} / #{num} messages"
181 end
182
183 if optimize
184   puts "optimizing index..."
185   optt = time { index.index.optimize }
186   puts "optimized index of size #{index.size} in #{optt}s."
187 end