backup 4.3.0 → 5.0.0.beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +19 -0
  3. data/README.md +13 -9
  4. data/bin/docker_test +24 -0
  5. data/lib/backup.rb +74 -78
  6. data/lib/backup/archive.rb +31 -32
  7. data/lib/backup/binder.rb +2 -6
  8. data/lib/backup/cleaner.rb +14 -18
  9. data/lib/backup/cli.rb +104 -108
  10. data/lib/backup/cloud_io/base.rb +4 -7
  11. data/lib/backup/cloud_io/cloud_files.rb +60 -62
  12. data/lib/backup/cloud_io/s3.rb +69 -76
  13. data/lib/backup/compressor/base.rb +4 -7
  14. data/lib/backup/compressor/bzip2.rb +3 -7
  15. data/lib/backup/compressor/custom.rb +2 -6
  16. data/lib/backup/compressor/gzip.rb +16 -17
  17. data/lib/backup/config.rb +17 -18
  18. data/lib/backup/config/dsl.rb +16 -17
  19. data/lib/backup/config/helpers.rb +10 -16
  20. data/lib/backup/database/base.rb +22 -21
  21. data/lib/backup/database/mongodb.rb +36 -37
  22. data/lib/backup/database/mysql.rb +40 -41
  23. data/lib/backup/database/openldap.rb +8 -10
  24. data/lib/backup/database/postgresql.rb +29 -30
  25. data/lib/backup/database/redis.rb +27 -30
  26. data/lib/backup/database/riak.rb +15 -18
  27. data/lib/backup/database/sqlite.rb +4 -6
  28. data/lib/backup/encryptor/base.rb +2 -4
  29. data/lib/backup/encryptor/gpg.rb +49 -59
  30. data/lib/backup/encryptor/open_ssl.rb +11 -14
  31. data/lib/backup/errors.rb +7 -12
  32. data/lib/backup/logger.rb +16 -18
  33. data/lib/backup/logger/console.rb +5 -8
  34. data/lib/backup/logger/fog_adapter.rb +2 -6
  35. data/lib/backup/logger/logfile.rb +10 -12
  36. data/lib/backup/logger/syslog.rb +2 -4
  37. data/lib/backup/model.rb +33 -40
  38. data/lib/backup/notifier/base.rb +24 -26
  39. data/lib/backup/notifier/campfire.rb +9 -11
  40. data/lib/backup/notifier/command.rb +3 -3
  41. data/lib/backup/notifier/datadog.rb +9 -12
  42. data/lib/backup/notifier/flowdock.rb +13 -17
  43. data/lib/backup/notifier/hipchat.rb +18 -14
  44. data/lib/backup/notifier/http_post.rb +11 -14
  45. data/lib/backup/notifier/mail.rb +42 -54
  46. data/lib/backup/notifier/nagios.rb +5 -9
  47. data/lib/backup/notifier/pagerduty.rb +10 -12
  48. data/lib/backup/notifier/prowl.rb +15 -15
  49. data/lib/backup/notifier/pushover.rb +7 -10
  50. data/lib/backup/notifier/ses.rb +52 -17
  51. data/lib/backup/notifier/slack.rb +39 -40
  52. data/lib/backup/notifier/twitter.rb +2 -5
  53. data/lib/backup/notifier/zabbix.rb +11 -14
  54. data/lib/backup/package.rb +5 -9
  55. data/lib/backup/packager.rb +16 -17
  56. data/lib/backup/pipeline.rb +17 -21
  57. data/lib/backup/splitter.rb +8 -11
  58. data/lib/backup/storage/base.rb +5 -8
  59. data/lib/backup/storage/cloud_files.rb +21 -23
  60. data/lib/backup/storage/cycler.rb +10 -15
  61. data/lib/backup/storage/dropbox.rb +15 -21
  62. data/lib/backup/storage/ftp.rb +14 -10
  63. data/lib/backup/storage/local.rb +5 -8
  64. data/lib/backup/storage/qiniu.rb +8 -8
  65. data/lib/backup/storage/rsync.rb +24 -26
  66. data/lib/backup/storage/s3.rb +27 -28
  67. data/lib/backup/storage/scp.rb +10 -12
  68. data/lib/backup/storage/sftp.rb +10 -12
  69. data/lib/backup/syncer/base.rb +5 -8
  70. data/lib/backup/syncer/cloud/base.rb +27 -30
  71. data/lib/backup/syncer/cloud/cloud_files.rb +16 -18
  72. data/lib/backup/syncer/cloud/local_file.rb +5 -8
  73. data/lib/backup/syncer/cloud/s3.rb +23 -24
  74. data/lib/backup/syncer/rsync/base.rb +6 -10
  75. data/lib/backup/syncer/rsync/local.rb +1 -5
  76. data/lib/backup/syncer/rsync/pull.rb +6 -10
  77. data/lib/backup/syncer/rsync/push.rb +18 -22
  78. data/lib/backup/template.rb +9 -14
  79. data/lib/backup/utilities.rb +78 -69
  80. data/lib/backup/version.rb +1 -3
  81. metadata +107 -677
@@ -1,9 +1,6 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  module Encryptor
5
3
  class OpenSSL < Base
6
-
7
4
  ##
8
5
  # The password that'll be used to encrypt the backup. This
9
6
  # password will be required to decrypt the backup later on.
@@ -42,7 +39,7 @@ module Backup
42
39
  # so that any clean-up may be performed after the yield.
43
40
  def encrypt_with
44
41
  log!
45
- yield "#{ utility(:openssl) } #{ options }", '.enc'
42
+ yield "#{utility(:openssl)} #{options}", ".enc"
46
43
  end
47
44
 
48
45
  private
@@ -59,19 +56,19 @@ module Backup
59
56
  # Always sets a password option, if even no password is given,
60
57
  # but will prefer the password_file option if both are given.
61
58
  def options
62
- opts = ['aes-256-cbc']
63
- opts << '-base64' if @base64
64
- opts << '-salt' if @salt
59
+ opts = ["aes-256-cbc"]
60
+ opts << "-base64" if @base64
61
+ opts << "-salt" if @salt
65
62
 
66
- if @password_file.to_s.empty?
67
- opts << "-k #{Shellwords.escape(@password)}"
68
- else
69
- opts << "-pass file:#{@password_file}"
70
- end
63
+ opts <<
64
+ if @password_file.to_s.empty?
65
+ "-k #{Shellwords.escape(@password)}"
66
+ else
67
+ "-pass file:#{@password_file}"
68
+ end
71
69
 
72
- opts.join(' ')
70
+ opts.join(" ")
73
71
  end
74
-
75
72
  end
76
73
  end
77
74
  end
@@ -1,29 +1,25 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
-
5
2
  # Provides cascading errors with formatted messages.
6
3
  # See the specs for details.
7
4
  module NestedExceptions
8
-
9
5
  def self.included(klass)
10
- klass.extend Module.new {
6
+ klass.extend(Module.new do
11
7
  def wrap(wrapped_exception, msg = nil)
12
8
  new(msg, wrapped_exception)
13
9
  end
14
- }
10
+ end)
15
11
  end
16
12
 
17
13
  def initialize(obj = nil, wrapped_exception = nil)
18
14
  @wrapped_exception = wrapped_exception
19
- msg = (obj.respond_to?(:to_str) ? obj.to_str : obj.to_s).
20
- gsub(/^ */, ' ').strip
21
- msg = clean_name(self.class.name) + (msg.empty? ? '' : ": #{ msg }")
15
+ msg = (obj.respond_to?(:to_str) ? obj.to_str : obj.to_s)
16
+ .gsub(/^ */, " ").strip
17
+ msg = clean_name(self.class.name) + (msg.empty? ? "" : ": #{msg}")
22
18
 
23
19
  if wrapped_exception
24
20
  msg << "\n--- Wrapped Exception ---\n"
25
21
  class_name = clean_name(wrapped_exception.class.name)
26
- msg << class_name + ': ' unless
22
+ msg << class_name + ": " unless
27
23
  wrapped_exception.message.start_with? class_name
28
24
  msg << wrapped_exception.message
29
25
  end
@@ -43,9 +39,8 @@ module Backup
43
39
  private
44
40
 
45
41
  def clean_name(name)
46
- name.sub(/^Backup::/, '')
42
+ name.sub(/^Backup::/, "")
47
43
  end
48
-
49
44
  end
50
45
 
51
46
  class Error < StandardError
@@ -1,13 +1,10 @@
1
- # encoding: utf-8
2
-
3
- require 'backup/logger/console'
4
- require 'backup/logger/logfile'
5
- require 'backup/logger/syslog'
6
- require 'backup/logger/fog_adapter'
1
+ require "backup/logger/console"
2
+ require "backup/logger/logfile"
3
+ require "backup/logger/syslog"
4
+ require "backup/logger/fog_adapter"
7
5
 
8
6
  module Backup
9
7
  class Logger
10
-
11
8
  class Config
12
9
  class Logger < Struct.new(:class, :options)
13
10
  def enabled?
@@ -44,22 +41,22 @@ module Backup
44
41
  # [YYYY/MM/DD HH:MM:SS][level] message line text
45
42
  def formatted_lines
46
43
  timestamp = time.strftime("%Y/%m/%d %H:%M:%S")
47
- lines.map {|line| "[#{ timestamp }][#{ level }] #{ line }" }
44
+ lines.map { |line| "[#{timestamp}][#{level}] #{line}" }
48
45
  end
49
46
 
50
47
  def matches?(ignores)
51
48
  text = lines.join("\n")
52
- ignores.any? {|obj|
49
+ ignores.any? do |obj|
53
50
  obj.is_a?(Regexp) ? text.match(obj) : text.include?(obj)
54
- }
51
+ end
55
52
  end
56
53
  end
57
54
 
58
55
  class << self
59
56
  extend Forwardable
60
57
  def_delegators :logger,
61
- :start!, :abort!, :info, :warn, :error,
62
- :messages, :has_warnings?, :has_errors?
58
+ :start!, :abort!, :info, :warn, :error,
59
+ :messages, :has_warnings?, :has_errors?
63
60
 
64
61
  ##
65
62
  # Allows the Logger to be configured.
@@ -137,9 +134,9 @@ module Backup
137
134
  # Sends a message to the Logger using the specified log level.
138
135
  # +obj+ may be any Object that responds to #to_s (i.e. an Exception)
139
136
  [:info, :warn, :error].each do |level|
140
- define_method level, lambda {|obj|
137
+ define_method level do |obj|
141
138
  MUTEX.synchronize { log(obj, level) }
142
- }
139
+ end
143
140
  end
144
141
 
145
142
  ##
@@ -169,7 +166,7 @@ module Backup
169
166
  @loggers << logger.class.new(logger.options) if logger.enabled?
170
167
  end
171
168
  messages.each do |message|
172
- @loggers.each {|logger| logger.log(message) }
169
+ @loggers.each { |logger| logger.log(message) }
173
170
  end
174
171
  end
175
172
 
@@ -187,13 +184,14 @@ module Backup
187
184
  def log(obj, level)
188
185
  message = Message.new(Time.now.utc, level, obj.to_s.split("\n"))
189
186
 
190
- message.level = :info if message.level == :warn &&
191
- message.matches?(@config.ignores)
187
+ if message.level == :warn && message.matches?(@config.ignores)
188
+ message.level = :info
189
+ end
192
190
  @has_warnings ||= message.level == :warn
193
191
  @has_errors ||= message.level == :error
194
192
 
195
193
  messages << message
196
- @loggers.each {|logger| logger.log(message) }
194
+ @loggers.each { |logger| logger.log(message) }
197
195
  end
198
196
  end
199
197
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Logger
5
3
  class Console
@@ -30,22 +28,21 @@ module Backup
30
28
  end
31
29
 
32
30
  COLORS = {
33
- :info => "\e[32m%s\e[0m", # green
34
- :warn => "\e[33m%s\e[0m", # yellow
35
- :error => "\e[31m%s\e[0m" # red
31
+ info: "\e[32m%s\e[0m", # green
32
+ warn: "\e[33m%s\e[0m", # yellow
33
+ error: "\e[31m%s\e[0m" # red
36
34
  }
37
35
 
38
- def initialize(options = nil)
36
+ def initialize(_options = nil)
39
37
  $stdout.sync = $stderr.sync = true
40
38
  end
41
39
 
42
40
  def log(message)
43
41
  io = message.level == :info ? $stdout : $stderr
44
42
  lines = message.formatted_lines
45
- lines.map! {|line| COLORS[message.level] % line } if io.tty?
43
+ lines.map! { |line| COLORS[message.level] % line } if io.tty?
46
44
  io.puts lines
47
45
  end
48
-
49
46
  end
50
47
  end
51
48
  end
@@ -1,14 +1,11 @@
1
- # encoding: utf-8
2
-
3
1
  # require only the logger
4
- require 'formatador'
5
- require 'fog/core/logger'
2
+ require "formatador"
3
+ require "fog/core/logger"
6
4
 
7
5
  module Backup
8
6
  class Logger
9
7
  module FogAdapter
10
8
  class << self
11
-
12
9
  # Logged as :info so these won't generate warnings.
13
10
  # This is mostly to keep STDOUT clean and to provide
14
11
  # supplemental messages for our own warnings.
@@ -20,7 +17,6 @@ module Backup
20
17
  def tty?
21
18
  false
22
19
  end
23
-
24
20
  end
25
21
  end
26
22
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Logger
5
3
  class Logfile
@@ -60,7 +58,7 @@ module Backup
60
58
 
61
59
  def initialize
62
60
  @enabled = true
63
- @log_path = ''
61
+ @log_path = ""
64
62
  @max_bytes = 500_000
65
63
  end
66
64
 
@@ -84,7 +82,7 @@ module Backup
84
82
  end
85
83
 
86
84
  def log(message)
87
- File.open(@logfile, 'a') {|f| f.puts message.formatted_lines }
85
+ File.open(@logfile, "a") { |f| f.puts message.formatted_lines }
88
86
  end
89
87
 
90
88
  private
@@ -95,17 +93,17 @@ module Backup
95
93
  def setup_logfile
96
94
  # strip any trailing '/' in case the user supplied this as part of
97
95
  # an absolute path, so we can match it against File.expand_path()
98
- path = @options.log_path.chomp('/')
96
+ path = @options.log_path.chomp("/")
99
97
  if path.empty?
100
- path = File.join(Backup::Config.root_path, 'log')
98
+ path = File.join(Backup::Config.root_path, "log")
101
99
  elsif path != File.expand_path(path)
102
100
  path = File.join(Backup::Config.root_path, path)
103
101
  end
104
102
  FileUtils.mkdir_p(path)
105
- log_file = @options.log_file || 'backup.log'
103
+ log_file = @options.log_file || "backup.log"
106
104
  path = File.join(path, log_file)
107
105
  if File.exist?(path) && !File.writable?(path)
108
- raise Error, "Log File at '#{ path }' is not writable"
106
+ raise Error, "Log File at '#{path}' is not writable"
109
107
  end
110
108
  path
111
109
  end
@@ -116,16 +114,16 @@ module Backup
116
114
  return unless File.exist?(@logfile)
117
115
 
118
116
  if File.stat(@logfile).size > @options.max_bytes
119
- FileUtils.cp(@logfile, @logfile + '~')
120
- File.open(@logfile + '~', 'r') do |io_in|
121
- File.open(@logfile, 'w') do |io_out|
117
+ FileUtils.cp(@logfile, @logfile + "~")
118
+ File.open(@logfile + "~", "r") do |io_in|
119
+ File.open(@logfile, "w") do |io_out|
122
120
  io_in.seek(-@options.max_bytes, IO::SEEK_END) && io_in.gets
123
121
  while line = io_in.gets
124
122
  io_out.puts line
125
123
  end
126
124
  end
127
125
  end
128
- FileUtils.rm_f(@logfile + '~')
126
+ FileUtils.rm_f(@logfile + "~")
129
127
  end
130
128
  end
131
129
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Logger
5
3
  class Syslog
@@ -81,7 +79,7 @@ module Backup
81
79
 
82
80
  def initialize
83
81
  @enabled = false
84
- @ident = 'backup'
82
+ @ident = "backup"
85
83
  @options = ::Syslog::LOG_PID
86
84
  @facility = ::Syslog::LOG_LOCAL0
87
85
  @info = ::Syslog::LOG_INFO
@@ -108,7 +106,7 @@ module Backup
108
106
  def log(message)
109
107
  level = @options.send(message.level)
110
108
  ::Syslog.open(@options.ident, @options.options, @options.facility) do |s|
111
- message.lines.each {|line| s.log(level, '%s', line) }
109
+ message.lines.each { |line| s.log(level, "%s", line) }
112
110
  end
113
111
  end
114
112
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Model
5
3
  class Error < Backup::Error; end
@@ -18,11 +16,11 @@ module Backup
18
16
  # Return an Array of Models matching the given +trigger+.
19
17
  def find_by_trigger(trigger)
20
18
  trigger = trigger.to_s
21
- if trigger.include?('*')
19
+ if trigger.include?("*")
22
20
  regex = /^#{ trigger.gsub('*', '(.*)') }$/
23
- all.select {|model| regex =~ model.trigger }
21
+ all.select { |model| regex =~ model.trigger }
24
22
  else
25
- all.select {|model| trigger == model.trigger }
23
+ all.select { |model| trigger == model.trigger }
26
24
  end
27
25
  end
28
26
 
@@ -128,7 +126,7 @@ module Backup
128
126
 
129
127
  # trigger all defined databases to generate their #dump_filename
130
128
  # so warnings may be logged if `backup perform --check` is used
131
- databases.each {|db| db.send(:dump_filename) }
129
+ databases.each { |db| db.send(:dump_filename) }
132
130
 
133
131
  Model.all << self
134
132
  end
@@ -142,15 +140,15 @@ module Backup
142
140
  ##
143
141
  # Adds an Database. Multiple Databases may be added to the model.
144
142
  def database(name, database_id = nil, &block)
145
- @databases << get_class_from_scope(Database, name).
146
- new(self, database_id, &block)
143
+ @databases << get_class_from_scope(Database, name)
144
+ .new(self, database_id, &block)
147
145
  end
148
146
 
149
147
  ##
150
148
  # Adds an Storage. Multiple Storages may be added to the model.
151
149
  def store_with(name, storage_id = nil, &block)
152
- @storages << get_class_from_scope(Storage, name).
153
- new(self, storage_id, &block)
150
+ @storages << get_class_from_scope(Storage, name)
151
+ .new(self, storage_id, &block)
154
152
  end
155
153
 
156
154
  ##
@@ -270,14 +268,11 @@ module Backup
270
268
  end
271
269
 
272
270
  syncers.each(&:perform!)
273
-
274
271
  rescue Interrupt
275
272
  @interrupted = true
276
273
  raise
277
-
278
274
  rescue Exception => err
279
275
  @exception = err
280
-
281
276
  ensure
282
277
  unless @interrupted
283
278
  set_exit_status
@@ -302,8 +297,8 @@ module Backup
302
297
  def procedures
303
298
  return [] unless databases.any? || archives.any?
304
299
 
305
- [lambda { prepare! }, databases, archives,
306
- lambda { package! }, lambda { store! }, lambda { clean! }]
300
+ [-> { prepare! }, databases, archives,
301
+ -> { package! }, -> { store! }, -> { clean! }]
307
302
  end
308
303
 
309
304
  ##
@@ -341,8 +336,8 @@ module Backup
341
336
 
342
337
  if first_exception
343
338
  other_exceptions.each do |exception|
344
- Logger.error exception.to_s
345
- Logger.error exception.backtrace.join('\n')
339
+ Logger.error exception.to_s
340
+ Logger.error exception.backtrace.join('\n')
346
341
  end
347
342
  raise first_exception
348
343
  else
@@ -375,8 +370,8 @@ module Backup
375
370
  #
376
371
  def get_class_from_scope(scope, name)
377
372
  klass = scope
378
- name = name.to_s.sub(/^Backup::Config::DSL::/, '')
379
- name.split('::').each do |chunk|
373
+ name = name.to_s.sub(/^Backup::Config::DSL::/, "")
374
+ name.split("::").each do |chunk|
380
375
  klass = klass.const_get(chunk)
381
376
  end
382
377
  klass
@@ -385,11 +380,12 @@ module Backup
385
380
  ##
386
381
  # Sets or updates the model's #exit_status.
387
382
  def set_exit_status
388
- @exit_status = if exception
389
- exception.is_a?(StandardError) ? 2 : 3
390
- else
391
- Logger.has_warnings? ? 1 : 0
392
- end
383
+ @exit_status =
384
+ if exception
385
+ exception.is_a?(StandardError) ? 2 : 3
386
+ else
387
+ Logger.has_warnings? ? 1 : 0
388
+ end
393
389
  end
394
390
 
395
391
  ##
@@ -401,14 +397,13 @@ module Backup
401
397
  def before_hook
402
398
  return unless before
403
399
 
404
- Logger.info 'Before Hook Starting...'
400
+ Logger.info "Before Hook Starting..."
405
401
  before.call
406
- Logger.info 'Before Hook Finished.'
407
-
402
+ Logger.info "Before Hook Finished."
408
403
  rescue Exception => err
409
404
  @before_hook_failed = true
410
405
  ex = err.is_a?(StandardError) ? Error : FatalError
411
- raise ex.wrap(err, 'Before Hook Failed!')
406
+ raise ex.wrap(err, "Before Hook Failed!")
412
407
  end
413
408
 
414
409
  ##
@@ -418,16 +413,15 @@ module Backup
418
413
  def after_hook
419
414
  return unless after && !@before_hook_failed
420
415
 
421
- Logger.info 'After Hook Starting...'
416
+ Logger.info "After Hook Starting..."
422
417
  after.call(exit_status)
423
- Logger.info 'After Hook Finished.'
418
+ Logger.info "After Hook Finished."
424
419
 
425
420
  set_exit_status # in case hook logged warnings
426
-
427
421
  rescue Exception => err
428
422
  fatal = !err.is_a?(StandardError)
429
423
  ex = fatal ? FatalError : Error
430
- Logger.error ex.wrap(err, 'After Hook Failed!')
424
+ Logger.error ex.wrap(err, "After Hook Failed!")
431
425
  # upgrade exit_status if needed
432
426
  (@exit_status = fatal ? 3 : 2) unless exit_status == 3
433
427
  end
@@ -440,24 +434,24 @@ module Backup
440
434
  def log!(action)
441
435
  case action
442
436
  when :started
443
- Logger.info "Performing Backup for '#{ label } (#{ trigger })'!\n" +
444
- "[ backup #{ VERSION } : #{ RUBY_DESCRIPTION } ]"
437
+ Logger.info "Performing Backup for '#{label} (#{trigger})'!\n" \
438
+ "[ backup #{VERSION} : #{RUBY_DESCRIPTION} ]"
445
439
 
446
440
  when :finished
447
441
  if exit_status > 1
448
442
  ex = exit_status == 2 ? Error : FatalError
449
- err = ex.wrap(exception, "Backup for #{ label } (#{ trigger }) Failed!")
443
+ err = ex.wrap(exception, "Backup for #{label} (#{trigger}) Failed!")
450
444
  Logger.error err
451
445
  Logger.error "\nBacktrace:\n\s\s" + err.backtrace.join("\n\s\s") + "\n\n"
452
446
 
453
447
  Cleaner.warnings(self)
454
448
  else
455
- msg = "Backup for '#{ label } (#{ trigger })' "
449
+ msg = "Backup for '#{label} (#{trigger})' "
456
450
  if exit_status == 1
457
- msg << "Completed Successfully (with Warnings) in #{ duration }"
451
+ msg << "Completed Successfully (with Warnings) in #{duration}"
458
452
  Logger.warn msg
459
453
  else
460
- msg << "Completed Successfully in #{ duration }"
454
+ msg << "Completed Successfully in #{duration}"
461
455
  Logger.info msg
462
456
  end
463
457
  end
@@ -472,8 +466,7 @@ module Backup
472
466
  remainder = duration - (hours * 3600)
473
467
  minutes = remainder / 60
474
468
  seconds = remainder - (minutes * 60)
475
- '%02d:%02d:%02d' % [hours, minutes, seconds]
469
+ sprintf "%02d:%02d:%02d", hours, minutes, seconds
476
470
  end
477
-
478
471
  end
479
472
  end