]> git.cworth.org Git - sup/blob - lib/sup/util.rb
improved mime headers, yet again
[sup] / lib / sup / util.rb
1 require 'lockfile'
2 require 'mime/types'
3 require 'pathname'
4
5 ## time for some monkeypatching!
6 class Lockfile
7   def gen_lock_id
8     Hash[
9          'host' => "#{ Socket.gethostname }",
10          'pid' => "#{ Process.pid }",
11          'ppid' => "#{ Process.ppid }",
12          'time' => timestamp,
13          'pname' => $0,
14          'user' => ENV["USER"]
15         ]
16   end
17
18   def dump_lock_id lock_id = @lock_id
19       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
20         lock_id.values_at('host','pid','ppid','time','user', 'pname')
21     end
22
23   def lockinfo_on_disk
24     h = load_lock_id IO.read(path)
25     h['mtime'] = File.mtime path
26     h
27   end
28
29   def touch_yourself; touch path end
30 end
31
32 class Pathname
33   def human_size
34     s =
35       begin
36         size
37       rescue SystemCallError
38         return "?"
39       end
40
41     if s < 1024
42       s.to_s + "b"
43     elsif s < (1024 * 1024)
44       (s / 1024).to_s + "k"
45     elsif s < (1024 * 1024 * 1024)
46       (s / 1024 / 1024).to_s + "m"
47     else
48       (s / 1024 / 1024 / 1024).to_s + "g"
49     end
50   end
51
52   def human_time
53     begin
54       ctime.strftime("%Y-%m-%d %H:%M")
55     rescue SystemCallError
56       "?"
57     end
58   end
59 end
60
61 ## more monkeypatching!
62 module RMail
63   class EncodingUnsupportedError < StandardError; end
64
65   class Message
66     def add_file_attachment fn
67       bfn = File.basename fn
68       a = Message.new
69       t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
70
71       a.header.add "Content-Disposition", "attachment; filename=#{bfn.to_s.inspect}"
72       a.header.add "Content-Type", "#{t.content_type}; name=#{bfn.to_s.inspect}"
73       a.header.add "Content-Transfer-Encoding", t.encoding
74       a.body =
75         case t.encoding
76         when "base64"
77           [IO.read(fn)].pack "m"
78         when "quoted-printable"
79           [IO.read(fn)].pack "M"
80         when "7bit", "8bit"
81           IO.read(fn)
82         else
83           raise EncodingUnsupportedError, t.encoding
84         end
85
86       add_part a
87     end
88
89     def charset
90       if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/
91         $1
92       end
93     end
94   end
95 end
96
97 class Range
98   ## only valid for integer ranges (unless I guess it's exclusive)
99   def size 
100     last - first + (exclude_end? ? 0 : 1)
101   end
102 end
103
104 class Module
105   def bool_reader *args
106     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
107   end
108   def bool_writer *args; attr_writer(*args); end
109   def bool_accessor *args
110     bool_reader(*args)
111     bool_writer(*args)
112   end
113
114   def defer_all_other_method_calls_to obj
115     class_eval %{
116       def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
117       def respond_to? meth; @#{obj}.respond_to?(meth); end
118     }
119   end
120 end
121
122 class Object
123   def ancestors
124     ret = []
125     klass = self.class
126
127     until klass == Object
128       ret << klass
129       klass = klass.superclass
130     end
131     ret
132   end
133
134   ## "k combinator"
135   def returning x; yield x; x; end
136
137   ## clone of java-style whole-method synchronization
138   ## assumes a @mutex variable
139   def synchronized *meth
140     meth.each do
141       class_eval <<-EOF
142         alias unsynchronized_#{meth} #{meth}
143         def #{meth}(*a, &b)
144           @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
145         end
146       EOF
147     end
148   end
149 end
150
151 class String
152   def camel_to_hyphy
153     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
154   end
155
156   def find_all_positions x
157     ret = []
158     start = 0
159     while start < length
160       pos = index x, start
161       break if pos.nil?
162       ret << pos
163       start = pos + 1
164     end
165     ret
166   end
167
168   def ucfirst
169     self[0 .. 0].upcase + self[1 .. -1]
170   end
171
172   ## a very complicated regex found on teh internets to split on
173   ## commas, unless they occurr within double quotes.
174   def split_on_commas
175     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
176   end
177
178   def wrap len
179     ret = []
180     s = self
181     while s.length > len
182       cut = s[0 ... len].rindex(/\s/)
183       if cut
184         ret << s[0 ... cut]
185         s = s[(cut + 1) .. -1]
186       else
187         ret << s[0 ... len]
188         s = s[len .. -1]
189       end
190     end
191     ret << s
192   end
193
194   def normalize_whitespace
195     gsub(/\t/, "    ").gsub(/\r/, "")
196   end
197 end
198
199 class Numeric
200   def clamp min, max
201     if self < min
202       min
203     elsif self > max
204       max
205     else
206       self
207     end
208   end
209
210   def in? range; range.member? self; end
211 end
212
213 class Fixnum
214   def num_digits base=10
215     return 1 if self == 0
216     1 + (Math.log(self) / Math.log(10)).floor
217   end
218   
219   def to_character
220     if self < 128 && self >= 0
221       chr
222     else
223       "<#{self}>"
224     end
225   end
226 end
227
228 class Hash
229   def - o
230     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
231   end
232
233   def select_by_value v=true
234     select { |k, vv| vv == v }.map { |x| x.first }
235   end
236 end
237
238 module Enumerable
239   def map_with_index
240     ret = []
241     each_with_index { |x, i| ret << yield(x, i) }
242     ret
243   end
244
245   def sum; inject(0) { |x, y| x + y }; end
246   
247   def map_to_hash
248     ret = {}
249     each { |x| ret[x] = yield(x) }
250     ret
251   end
252
253   # like find, except returns the value of the block rather than the
254   # element itself.
255   def argfind
256     ret = nil
257     find { |e| ret ||= yield(e) }
258     ret || nil # force
259   end
260
261   def argmin
262     best, bestval = nil, nil
263     each do |e|
264       val = yield e
265       if bestval.nil? || val < bestval
266         best, bestval = e, val
267       end
268     end
269     best
270   end
271
272   ## returns the maximum shared prefix of an array of strings
273   ## optinally excluding a prefix
274   def shared_prefix caseless=false, exclude=""
275     return "" if empty?
276     prefix = ""
277     (0 ... first.length).each do |i|
278       c = (caseless ? first.downcase : first)[i]
279       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
280       next if exclude[i] == c
281       prefix += first[i].chr
282     end
283     prefix
284   end
285
286   def max_of
287     map { |e| yield e }.max
288   end
289 end
290
291 class Array
292   def flatten_one_level
293     inject([]) { |a, e| a + e }
294   end
295
296   def to_h; Hash[*flatten]; end
297   def rest; self[1..-1]; end
298
299   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
300
301   def last= e; self[-1] = e end
302 end
303
304 class Time
305   def to_indexable_s
306     sprintf "%012d", self
307   end
308
309   def nearest_hour
310     if min < 30
311       self
312     else
313       self + (60 - min) * 60
314     end
315   end
316
317   def midnight # within a second
318     self - (hour * 60 * 60) - (min * 60) - sec
319   end
320
321   def is_the_same_day? other
322     (midnight - other.midnight).abs < 1
323   end
324
325   def is_the_day_before? other
326     other.midnight - midnight <=  24 * 60 * 60 + 1
327   end
328
329   def to_nice_distance_s from=Time.now
330     later_than = (self < from)
331     diff = (self.to_i - from.to_i).abs.to_f
332     text = 
333       [ ["second", 60],
334         ["minute", 60],
335         ["hour", 24],
336         ["day", 7],
337         ["week", 4.345], # heh heh
338         ["month", 12],
339         ["year", nil],
340       ].argfind do |unit, size|
341         if diff.round <= 1
342           "one #{unit}"
343         elsif size.nil? || diff.round < size
344           "#{diff.round} #{unit}s"
345         else
346           diff /= size.to_f
347           false
348         end
349       end
350     if later_than
351       text + " ago"
352     else
353       "in " + text
354     end  
355   end
356
357   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
358   def to_nice_s from=Time.now
359     if year != from.year
360       strftime "%b %Y"
361     elsif month != from.month
362       strftime "%b %e"
363     else
364       if is_the_same_day? from
365         strftime("%l:%M%P")
366       elsif is_the_day_before? from
367         "Yest."  + nearest_hour.strftime("%l%P")
368       else
369         strftime "%b %e"
370       end
371     end
372   end
373 end
374
375 ## simple singleton module. far less complete and insane than the ruby
376 ## standard library one, but automatically forwards methods calls and
377 ## allows for constructors that take arguments.
378 ##
379 ## You must have #initialize call "self.class.i_am_the_instance self"
380 ## at some point or everything will fail horribly.
381 module Singleton
382   module ClassMethods
383     def instance; @instance; end
384     def instantiated?; defined?(@instance) && !@instance.nil?; end
385     def deinstantiate!; @instance = nil; end
386     def method_missing meth, *a, &b
387       raise "no instance defined!" unless defined? @instance
388
389       ## if we've been deinstantiated, just drop all calls. this is
390       ## useful because threads that might be active during the
391       ## cleanup process (e.g. polling) would otherwise have to
392       ## special-case every call to a Singleton object
393       return nil if @instance.nil?
394
395       @instance.send meth, *a, &b
396     end
397     def i_am_the_instance o
398       raise "there can be only one! (instance)" if defined? @instance
399       @instance = o
400     end
401   end
402
403   def self.included klass
404     klass.extend ClassMethods
405   end
406 end
407
408 ## wraps an object. if it throws an exception, keeps a copy, and
409 ## rethrows it for any further method calls.
410 class Recoverable
411   def initialize o
412     @o = o
413     @e = nil
414   end
415
416   def clear_error!; @e = nil; end
417   def has_errors?; !@e.nil?; end
418   def error; @e; end
419
420   def method_missing m, *a, &b; __pass m, *a, &b; end
421   
422   def id; __pass :id; end
423   def to_s; __pass :to_s; end
424   def to_yaml x; __pass :to_yaml, x; end
425   def is_a? c; @o.is_a? c; end
426
427   def respond_to? m; @o.respond_to? m end
428
429   def __pass m, *a, &b
430     begin
431       @o.send(m, *a, &b)
432     rescue Exception => e
433       @e = e
434       raise e
435     end
436   end
437 end
438
439 ## acts like a hash with an initialization block, but saves any
440 ## newly-created value even upon lookup.
441 ##
442 ## for example:
443 ##
444 ## class C
445 ##   attr_accessor :val
446 ##   def initialize; @val = 0 end
447 ## end
448 ## 
449 ## h = Hash.new { C.new }
450 ## h[:a].val # => 0
451 ## h[:a].val = 1
452 ## h[:a].val # => 0
453 ##
454 ## h2 = SavingHash.new { C.new }
455 ## h2[:a].val # => 0
456 ## h2[:a].val = 1
457 ## h2[:a].val # => 1
458 ##
459 ## important note: you REALLY want to use #member? to test existence,
460 ## because just checking h[anything] will always evaluate to true
461 ## (except for degenerate constructor blocks that return nil or false)
462 class SavingHash
463   def initialize &b
464     @constructor = b
465     @hash = Hash.new
466   end
467
468   def [] k
469     @hash[k] ||= @constructor.call(k)
470   end
471
472   defer_all_other_method_calls_to :hash
473 end