backup-agoddard 3.0.27
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.
- data/.gitignore +8 -0
- data/.travis.yml +10 -0
- data/Gemfile +28 -0
- data/Guardfile +23 -0
- data/LICENSE.md +24 -0
- data/README.md +478 -0
- data/backup.gemspec +32 -0
- data/bin/backup +11 -0
- data/lib/backup.rb +133 -0
- data/lib/backup/archive.rb +117 -0
- data/lib/backup/binder.rb +22 -0
- data/lib/backup/cleaner.rb +121 -0
- data/lib/backup/cli/helpers.rb +93 -0
- data/lib/backup/cli/utility.rb +255 -0
- data/lib/backup/compressor/base.rb +35 -0
- data/lib/backup/compressor/bzip2.rb +50 -0
- data/lib/backup/compressor/custom.rb +53 -0
- data/lib/backup/compressor/gzip.rb +50 -0
- data/lib/backup/compressor/lzma.rb +52 -0
- data/lib/backup/compressor/pbzip2.rb +59 -0
- data/lib/backup/config.rb +174 -0
- data/lib/backup/configuration.rb +33 -0
- data/lib/backup/configuration/helpers.rb +130 -0
- data/lib/backup/configuration/store.rb +24 -0
- data/lib/backup/database/base.rb +53 -0
- data/lib/backup/database/mongodb.rb +230 -0
- data/lib/backup/database/mysql.rb +160 -0
- data/lib/backup/database/postgresql.rb +144 -0
- data/lib/backup/database/redis.rb +136 -0
- data/lib/backup/database/riak.rb +67 -0
- data/lib/backup/dependency.rb +108 -0
- data/lib/backup/encryptor/base.rb +29 -0
- data/lib/backup/encryptor/gpg.rb +760 -0
- data/lib/backup/encryptor/open_ssl.rb +72 -0
- data/lib/backup/errors.rb +124 -0
- data/lib/backup/hooks.rb +68 -0
- data/lib/backup/logger.rb +152 -0
- data/lib/backup/model.rb +409 -0
- data/lib/backup/notifier/base.rb +81 -0
- data/lib/backup/notifier/campfire.rb +155 -0
- data/lib/backup/notifier/hipchat.rb +93 -0
- data/lib/backup/notifier/mail.rb +206 -0
- data/lib/backup/notifier/prowl.rb +65 -0
- data/lib/backup/notifier/pushover.rb +88 -0
- data/lib/backup/notifier/twitter.rb +70 -0
- data/lib/backup/package.rb +47 -0
- data/lib/backup/packager.rb +100 -0
- data/lib/backup/pipeline.rb +110 -0
- data/lib/backup/splitter.rb +75 -0
- data/lib/backup/storage/base.rb +99 -0
- data/lib/backup/storage/cloudfiles.rb +87 -0
- data/lib/backup/storage/cycler.rb +117 -0
- data/lib/backup/storage/dropbox.rb +178 -0
- data/lib/backup/storage/ftp.rb +119 -0
- data/lib/backup/storage/local.rb +82 -0
- data/lib/backup/storage/ninefold.rb +116 -0
- data/lib/backup/storage/rsync.rb +149 -0
- data/lib/backup/storage/s3.rb +94 -0
- data/lib/backup/storage/scp.rb +99 -0
- data/lib/backup/storage/sftp.rb +108 -0
- data/lib/backup/syncer/base.rb +46 -0
- data/lib/backup/syncer/cloud/base.rb +247 -0
- data/lib/backup/syncer/cloud/cloud_files.rb +78 -0
- data/lib/backup/syncer/cloud/s3.rb +68 -0
- data/lib/backup/syncer/rsync/base.rb +49 -0
- data/lib/backup/syncer/rsync/local.rb +55 -0
- data/lib/backup/syncer/rsync/pull.rb +36 -0
- data/lib/backup/syncer/rsync/push.rb +116 -0
- data/lib/backup/template.rb +46 -0
- data/lib/backup/version.rb +43 -0
- data/spec-live/.gitignore +6 -0
- data/spec-live/README +7 -0
- data/spec-live/backups/config.rb +83 -0
- data/spec-live/backups/config.yml.template +46 -0
- data/spec-live/backups/models.rb +184 -0
- data/spec-live/compressor/custom_spec.rb +30 -0
- data/spec-live/compressor/gzip_spec.rb +30 -0
- data/spec-live/encryptor/gpg_keys.rb +239 -0
- data/spec-live/encryptor/gpg_spec.rb +287 -0
- data/spec-live/notifier/mail_spec.rb +121 -0
- data/spec-live/spec_helper.rb +151 -0
- data/spec-live/storage/dropbox_spec.rb +151 -0
- data/spec-live/storage/local_spec.rb +83 -0
- data/spec-live/storage/scp_spec.rb +193 -0
- data/spec-live/syncer/cloud/s3_spec.rb +124 -0
- data/spec/archive_spec.rb +335 -0
- data/spec/cleaner_spec.rb +312 -0
- data/spec/cli/helpers_spec.rb +301 -0
- data/spec/cli/utility_spec.rb +411 -0
- data/spec/compressor/base_spec.rb +52 -0
- data/spec/compressor/bzip2_spec.rb +217 -0
- data/spec/compressor/custom_spec.rb +106 -0
- data/spec/compressor/gzip_spec.rb +217 -0
- data/spec/compressor/lzma_spec.rb +123 -0
- data/spec/compressor/pbzip2_spec.rb +165 -0
- data/spec/config_spec.rb +321 -0
- data/spec/configuration/helpers_spec.rb +247 -0
- data/spec/configuration/store_spec.rb +39 -0
- data/spec/configuration_spec.rb +62 -0
- data/spec/database/base_spec.rb +63 -0
- data/spec/database/mongodb_spec.rb +510 -0
- data/spec/database/mysql_spec.rb +411 -0
- data/spec/database/postgresql_spec.rb +353 -0
- data/spec/database/redis_spec.rb +334 -0
- data/spec/database/riak_spec.rb +176 -0
- data/spec/dependency_spec.rb +51 -0
- data/spec/encryptor/base_spec.rb +40 -0
- data/spec/encryptor/gpg_spec.rb +909 -0
- data/spec/encryptor/open_ssl_spec.rb +148 -0
- data/spec/errors_spec.rb +306 -0
- data/spec/hooks_spec.rb +35 -0
- data/spec/logger_spec.rb +367 -0
- data/spec/model_spec.rb +694 -0
- data/spec/notifier/base_spec.rb +104 -0
- data/spec/notifier/campfire_spec.rb +217 -0
- data/spec/notifier/hipchat_spec.rb +211 -0
- data/spec/notifier/mail_spec.rb +316 -0
- data/spec/notifier/prowl_spec.rb +138 -0
- data/spec/notifier/pushover_spec.rb +123 -0
- data/spec/notifier/twitter_spec.rb +153 -0
- data/spec/package_spec.rb +61 -0
- data/spec/packager_spec.rb +213 -0
- data/spec/pipeline_spec.rb +259 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/splitter_spec.rb +120 -0
- data/spec/storage/base_spec.rb +166 -0
- data/spec/storage/cloudfiles_spec.rb +254 -0
- data/spec/storage/cycler_spec.rb +247 -0
- data/spec/storage/dropbox_spec.rb +480 -0
- data/spec/storage/ftp_spec.rb +271 -0
- data/spec/storage/local_spec.rb +259 -0
- data/spec/storage/ninefold_spec.rb +343 -0
- data/spec/storage/rsync_spec.rb +362 -0
- data/spec/storage/s3_spec.rb +245 -0
- data/spec/storage/scp_spec.rb +233 -0
- data/spec/storage/sftp_spec.rb +244 -0
- data/spec/syncer/base_spec.rb +109 -0
- data/spec/syncer/cloud/base_spec.rb +515 -0
- data/spec/syncer/cloud/cloud_files_spec.rb +181 -0
- data/spec/syncer/cloud/s3_spec.rb +174 -0
- data/spec/syncer/rsync/base_spec.rb +98 -0
- data/spec/syncer/rsync/local_spec.rb +149 -0
- data/spec/syncer/rsync/pull_spec.rb +98 -0
- data/spec/syncer/rsync/push_spec.rb +333 -0
- data/spec/version_spec.rb +21 -0
- data/templates/cli/utility/archive +25 -0
- data/templates/cli/utility/compressor/bzip2 +4 -0
- data/templates/cli/utility/compressor/custom +11 -0
- data/templates/cli/utility/compressor/gzip +4 -0
- data/templates/cli/utility/compressor/lzma +10 -0
- data/templates/cli/utility/compressor/pbzip2 +10 -0
- data/templates/cli/utility/config +32 -0
- data/templates/cli/utility/database/mongodb +18 -0
- data/templates/cli/utility/database/mysql +21 -0
- data/templates/cli/utility/database/postgresql +17 -0
- data/templates/cli/utility/database/redis +16 -0
- data/templates/cli/utility/database/riak +11 -0
- data/templates/cli/utility/encryptor/gpg +27 -0
- data/templates/cli/utility/encryptor/openssl +9 -0
- data/templates/cli/utility/model.erb +23 -0
- data/templates/cli/utility/notifier/campfire +12 -0
- data/templates/cli/utility/notifier/hipchat +15 -0
- data/templates/cli/utility/notifier/mail +22 -0
- data/templates/cli/utility/notifier/prowl +11 -0
- data/templates/cli/utility/notifier/pushover +11 -0
- data/templates/cli/utility/notifier/twitter +13 -0
- data/templates/cli/utility/splitter +7 -0
- data/templates/cli/utility/storage/cloud_files +22 -0
- data/templates/cli/utility/storage/dropbox +20 -0
- data/templates/cli/utility/storage/ftp +12 -0
- data/templates/cli/utility/storage/local +7 -0
- data/templates/cli/utility/storage/ninefold +9 -0
- data/templates/cli/utility/storage/rsync +11 -0
- data/templates/cli/utility/storage/s3 +19 -0
- data/templates/cli/utility/storage/scp +11 -0
- data/templates/cli/utility/storage/sftp +11 -0
- data/templates/cli/utility/syncer/cloud_files +46 -0
- data/templates/cli/utility/syncer/rsync_local +12 -0
- data/templates/cli/utility/syncer/rsync_pull +17 -0
- data/templates/cli/utility/syncer/rsync_push +17 -0
- data/templates/cli/utility/syncer/s3 +43 -0
- data/templates/general/links +11 -0
- data/templates/general/version.erb +2 -0
- data/templates/notifier/mail/failure.erb +9 -0
- data/templates/notifier/mail/success.erb +7 -0
- data/templates/notifier/mail/warning.erb +9 -0
- data/templates/storage/dropbox/authorization_url.erb +6 -0
- data/templates/storage/dropbox/authorized.erb +4 -0
- data/templates/storage/dropbox/cache_file_written.erb +10 -0
- metadata +277 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Backup
|
|
4
|
+
module Encryptor
|
|
5
|
+
class OpenSSL < Base
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# The password that'll be used to encrypt the backup. This
|
|
9
|
+
# password will be required to decrypt the backup later on.
|
|
10
|
+
attr_accessor :password
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# The password file to use to encrypt the backup.
|
|
14
|
+
attr_accessor :password_file
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Determines whether the 'base64' should be used or not
|
|
18
|
+
attr_accessor :base64
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Determines whether the 'salt' flag should be used
|
|
22
|
+
attr_accessor :salt
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Creates a new instance of Backup::Encryptor::OpenSSL and
|
|
26
|
+
# sets the password attribute to what was provided
|
|
27
|
+
def initialize(&block)
|
|
28
|
+
super
|
|
29
|
+
|
|
30
|
+
@base64 ||= false
|
|
31
|
+
@salt ||= true
|
|
32
|
+
@password_file ||= nil
|
|
33
|
+
|
|
34
|
+
instance_eval(&block) if block_given?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# This is called as part of the procedure run by the Packager.
|
|
39
|
+
# It sets up the needed options to pass to the openssl command,
|
|
40
|
+
# then yields the command to use as part of the packaging procedure.
|
|
41
|
+
# Once the packaging procedure is complete, it will return
|
|
42
|
+
# so that any clean-up may be performed after the yield.
|
|
43
|
+
def encrypt_with
|
|
44
|
+
log!
|
|
45
|
+
yield "#{ utility(:openssl) } #{ options }", '.enc'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Uses the 256bit AES encryption cipher, which is what the
|
|
52
|
+
# US Government uses to encrypt information at the "Top Secret" level.
|
|
53
|
+
#
|
|
54
|
+
# The -base64 option will make the encrypted output base64 encoded,
|
|
55
|
+
# this makes the encrypted file readable using text editors
|
|
56
|
+
#
|
|
57
|
+
# The -salt option adds strength to the encryption
|
|
58
|
+
#
|
|
59
|
+
# Always sets a password option, if even no password is given,
|
|
60
|
+
# but will prefer the password_file option if both are given.
|
|
61
|
+
def options
|
|
62
|
+
opts = ['aes-256-cbc']
|
|
63
|
+
opts << '-base64' if @base64
|
|
64
|
+
opts << '-salt' if @salt
|
|
65
|
+
opts << ( @password_file.to_s.empty? ?
|
|
66
|
+
"-k '#{@password}'" : "-pass file:#{@password_file}" )
|
|
67
|
+
opts.join(' ')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Backup
|
|
4
|
+
##
|
|
5
|
+
# - automatically defines module namespaces referenced under Backup::Errors
|
|
6
|
+
# - any constant name referenced that ends with 'Error' will be created
|
|
7
|
+
# as a subclass of Backup::Errors::Error
|
|
8
|
+
# e.g.
|
|
9
|
+
# err = Backup::Errors::Foo::Bar::FooError.new('error message')
|
|
10
|
+
# err.message => "Foo::Bar::FooError: error message"
|
|
11
|
+
#
|
|
12
|
+
module ErrorsHelper
|
|
13
|
+
def const_missing(const)
|
|
14
|
+
if const.to_s.end_with?('Error')
|
|
15
|
+
module_eval("class #{const} < Backup::Errors::Error; end")
|
|
16
|
+
else
|
|
17
|
+
module_eval("module #{const}; extend Backup::ErrorsHelper; end")
|
|
18
|
+
end
|
|
19
|
+
const_get(const)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# provides cascading errors with formatted messages
|
|
25
|
+
# see the specs for details
|
|
26
|
+
#
|
|
27
|
+
# e.g.
|
|
28
|
+
# module Backup
|
|
29
|
+
# begin
|
|
30
|
+
# begin
|
|
31
|
+
# begin
|
|
32
|
+
# raise Errors::ZoneAError, 'an error occurred in Zone A'
|
|
33
|
+
# rescue => err
|
|
34
|
+
# raise Errors::ZoneBError.wrap(err, <<-EOS)
|
|
35
|
+
# an error occurred in Zone B
|
|
36
|
+
#
|
|
37
|
+
# the following error should give a reason
|
|
38
|
+
# EOS
|
|
39
|
+
# end
|
|
40
|
+
# rescue => err
|
|
41
|
+
# raise Errors::ZoneCError.wrap(err)
|
|
42
|
+
# end
|
|
43
|
+
# rescue => err
|
|
44
|
+
# puts Errors::ZoneDError.wrap(err, 'an error occurred in Zone D')
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# Outputs:
|
|
49
|
+
# ZoneDError: an error occurred in Zone D
|
|
50
|
+
# Reason: ZoneCError
|
|
51
|
+
# ZoneBError: an error occurred in Zone B
|
|
52
|
+
#
|
|
53
|
+
# the following error should give a reason
|
|
54
|
+
# Reason: ZoneAError
|
|
55
|
+
# an error occurred in Zone A
|
|
56
|
+
#
|
|
57
|
+
module Errors
|
|
58
|
+
extend ErrorsHelper
|
|
59
|
+
|
|
60
|
+
class Error < StandardError
|
|
61
|
+
|
|
62
|
+
def self.wrap(orig_err, msg = nil)
|
|
63
|
+
new(msg, orig_err)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(msg = nil, orig_err = nil)
|
|
67
|
+
super(msg)
|
|
68
|
+
set_backtrace(orig_err.backtrace) if @orig_err = orig_err
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_s
|
|
72
|
+
return @to_s if @to_s
|
|
73
|
+
orig_to_s = super()
|
|
74
|
+
|
|
75
|
+
if orig_to_s == self.class.to_s
|
|
76
|
+
msg = orig_err_msg ?
|
|
77
|
+
"#{orig_err_class}: #{orig_err_msg}" : orig_err_class
|
|
78
|
+
else
|
|
79
|
+
msg = format_msg(orig_to_s)
|
|
80
|
+
msg << "\n Reason: #{orig_err_class}" + (orig_err_msg ?
|
|
81
|
+
"\n #{orig_err_msg}" : ' (no message given)') if @orig_err
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@to_s = msg ? msg_prefix + msg : class_name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def msg_prefix
|
|
90
|
+
@msg_prefix ||= class_name + ': '
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def orig_msg
|
|
94
|
+
@orig_msg ||= to_s.sub(msg_prefix, '')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def class_name
|
|
98
|
+
@class_name ||= self.class.to_s.sub('Backup::Errors::', '')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def orig_err_class
|
|
102
|
+
return unless @orig_err
|
|
103
|
+
|
|
104
|
+
@orig_err_class ||= @orig_err.is_a?(Errors::Error) ?
|
|
105
|
+
@orig_err.send(:class_name) : @orig_err.class.to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def orig_err_msg
|
|
109
|
+
return unless @orig_err
|
|
110
|
+
return @orig_err_msg unless @orig_err_msg.nil?
|
|
111
|
+
|
|
112
|
+
msg = @orig_err.is_a?(Errors::Error) ?
|
|
113
|
+
@orig_err.send(:orig_msg) : @orig_err.to_s
|
|
114
|
+
@orig_err_msg = (msg == orig_err_class) ?
|
|
115
|
+
false : format_msg(msg)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def format_msg(msg)
|
|
119
|
+
msg.gsub(/^ */, ' ').strip
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/backup/hooks.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
|
|
4
|
+
module Backup
|
|
5
|
+
class Hooks
|
|
6
|
+
include Backup::CLI::Helpers
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Stores a block to be run before the backup
|
|
10
|
+
attr_accessor :before_proc
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Stores a block to be run before the backup
|
|
14
|
+
attr_accessor :after_proc
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# The model
|
|
18
|
+
attr_reader :model
|
|
19
|
+
|
|
20
|
+
def initialize(model, &block)
|
|
21
|
+
@model = model
|
|
22
|
+
@before_proc = Proc.new { } # noop
|
|
23
|
+
@after_proc = Proc.new { } # noop
|
|
24
|
+
instance_eval(&block) if block_given?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def before(&code)
|
|
28
|
+
@before_proc = code
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def after(&code)
|
|
32
|
+
@after_proc = code
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def before!
|
|
36
|
+
begin
|
|
37
|
+
Logger.message "Performing Before Hook"
|
|
38
|
+
@before_proc.call(model)
|
|
39
|
+
Logger.message "Before Hook Completed Successfully"
|
|
40
|
+
rescue => err
|
|
41
|
+
raise Errors::Hooks::BeforeHookError.wrap(
|
|
42
|
+
err, "Before Hook Failed!"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def after!
|
|
48
|
+
begin
|
|
49
|
+
Logger.message "Performing After Hook"
|
|
50
|
+
@after_proc.call(model)
|
|
51
|
+
Logger.message "After Hook Completed Successfully"
|
|
52
|
+
rescue => err
|
|
53
|
+
raise Errors::Hooks::AfterHookError.wrap(
|
|
54
|
+
err, "After Hook Failed!"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def perform!(hook)
|
|
60
|
+
case hook
|
|
61
|
+
when :before
|
|
62
|
+
before!
|
|
63
|
+
when :after
|
|
64
|
+
after!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Backup
|
|
4
|
+
module Logger
|
|
5
|
+
class << self
|
|
6
|
+
|
|
7
|
+
attr_accessor :quiet
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Outputs a messages to the console and writes it to the backup.log
|
|
11
|
+
def message(string)
|
|
12
|
+
to_console loggify(string, :message, :green)
|
|
13
|
+
to_file loggify(string, :message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Outputs an error to the console and writes it to the backup.log
|
|
18
|
+
# Called when an Exception has caused the backup process to abort.
|
|
19
|
+
def error(string)
|
|
20
|
+
to_console loggify(string, :error, :red), true
|
|
21
|
+
to_file loggify(string, :error)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Outputs a notice to the console and writes it to the backup.log
|
|
26
|
+
# Sets #has_warnings? true so :on_warning notifications will be sent
|
|
27
|
+
def warn(string)
|
|
28
|
+
@has_warnings = true
|
|
29
|
+
to_console loggify(string, :warning, :yellow), true
|
|
30
|
+
to_file loggify(string, :warning)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Outputs the data as if it were a regular 'puts' command,
|
|
34
|
+
# but also logs it to the backup.log
|
|
35
|
+
def normal(string)
|
|
36
|
+
to_console loggify(string)
|
|
37
|
+
to_file loggify(string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Silently logs data to the log file
|
|
42
|
+
def silent(string)
|
|
43
|
+
to_file loggify(string, :silent)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# Returns an Array of all messages written to the log file for this session
|
|
48
|
+
def messages
|
|
49
|
+
@messages ||= []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Returns true if any warnings have been issued
|
|
54
|
+
def has_warnings?
|
|
55
|
+
@has_warnings ||= false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clear!
|
|
59
|
+
messages.clear
|
|
60
|
+
@has_warnings = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def truncate!(max_bytes = 500_000)
|
|
64
|
+
log_file = File.join(Config.log_path, 'backup.log')
|
|
65
|
+
return unless File.exist?(log_file)
|
|
66
|
+
|
|
67
|
+
if File.stat(log_file).size > max_bytes
|
|
68
|
+
FileUtils.mv(log_file, log_file + '~')
|
|
69
|
+
File.open(log_file + '~', 'r') do |io_in|
|
|
70
|
+
File.open(log_file, 'w') do |io_out|
|
|
71
|
+
io_in.seek(-max_bytes, IO::SEEK_END) && io_in.gets
|
|
72
|
+
while line = io_in.gets
|
|
73
|
+
io_out.puts line
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
FileUtils.rm_f(log_file + '~')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Returns the time in [YYYY/MM/DD HH:MM:SS] format
|
|
85
|
+
def time
|
|
86
|
+
Time.now.strftime("%Y/%m/%d %H:%M:%S")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Receives a String, or an Object that responds to #to_s (e.g. an
|
|
91
|
+
# Exception), from one of the messaging methods and converts it into an
|
|
92
|
+
# Array of Strings, split on newline separators. Each line is then
|
|
93
|
+
# formatted into a log format based on the given options, and the Array
|
|
94
|
+
# returned to be passed to to_console() and/or to_file().
|
|
95
|
+
def loggify(string, type = false, color = false)
|
|
96
|
+
lines = string.to_s.split("\n")
|
|
97
|
+
if type
|
|
98
|
+
type = send(color, type) if color
|
|
99
|
+
time_now = time
|
|
100
|
+
lines.map {|line| "[#{time_now}][#{type}] #{line}" }
|
|
101
|
+
else
|
|
102
|
+
lines
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# Receives an Array of Strings to be written to the console.
|
|
108
|
+
def to_console(lines, stderr = false)
|
|
109
|
+
return if quiet
|
|
110
|
+
lines.each {|line| stderr ? Kernel.warn(line) : puts(line) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
##
|
|
114
|
+
# Receives an Array of Strings to be written to the log file.
|
|
115
|
+
def to_file(lines)
|
|
116
|
+
File.open(File.join(Config.log_path, 'backup.log'), 'a') do |file|
|
|
117
|
+
lines.each {|line| file.puts line }
|
|
118
|
+
end
|
|
119
|
+
messages.push(*lines)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# Invokes the #colorize method with the provided string
|
|
124
|
+
# and the color code "32" (for green)
|
|
125
|
+
def green(string)
|
|
126
|
+
colorize(string, 32)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Invokes the #colorize method with the provided string
|
|
131
|
+
# and the color code "33" (for yellow)
|
|
132
|
+
def yellow(string)
|
|
133
|
+
colorize(string, 33)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Invokes the #colorize method the with provided string
|
|
138
|
+
# and the color code "31" (for red)
|
|
139
|
+
def red(string)
|
|
140
|
+
colorize(string, 31)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Wraps the provided string in colorizing tags to provide
|
|
145
|
+
# easier to view output to the client
|
|
146
|
+
def colorize(string, code)
|
|
147
|
+
"\e[#{code}m#{string}\e[0m"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/backup/model.rb
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Backup
|
|
4
|
+
class Model
|
|
5
|
+
include Backup::CLI::Helpers
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
##
|
|
9
|
+
# The Backup::Model.all class method keeps track of all the models
|
|
10
|
+
# that have been instantiated. It returns the @all class variable,
|
|
11
|
+
# which contains an array of all the models
|
|
12
|
+
def all
|
|
13
|
+
@all ||= []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Return the first model matching +trigger+.
|
|
18
|
+
# Raises Errors::MissingTriggerError if no matches are found.
|
|
19
|
+
def find(trigger)
|
|
20
|
+
trigger = trigger.to_s
|
|
21
|
+
all.each do |model|
|
|
22
|
+
return model if model.trigger == trigger
|
|
23
|
+
end
|
|
24
|
+
raise Errors::Model::MissingTriggerError,
|
|
25
|
+
"Could not find trigger '#{trigger}'."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Find and return an Array of all models matching +trigger+
|
|
30
|
+
# Used to match triggers using a wildcard (*)
|
|
31
|
+
def find_matching(trigger)
|
|
32
|
+
regex = /^#{ trigger.to_s.gsub('*', '(.*)') }$/
|
|
33
|
+
all.select {|model| regex =~ model.trigger }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# The trigger (stored as a String) is used as an identifier
|
|
39
|
+
# for initializing the backup process
|
|
40
|
+
attr_reader :trigger
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# The label (stored as a String) is used for a more friendly user output
|
|
44
|
+
attr_reader :label
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# The databases attribute holds an array of database objects
|
|
48
|
+
attr_reader :databases
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# The archives attr_accessor holds an array of archive objects
|
|
52
|
+
attr_reader :archives
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# The notifiers attr_accessor holds an array of notifier objects
|
|
56
|
+
attr_reader :notifiers
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# The storages attribute holds an array of storage objects
|
|
60
|
+
attr_reader :storages
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# The syncers attribute holds an array of syncer objects
|
|
64
|
+
attr_reader :syncers
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Holds the configured Compressor
|
|
68
|
+
attr_reader :compressor
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# Holds the configured Encryptor
|
|
72
|
+
attr_reader :encryptor
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Holds the configured Splitter
|
|
76
|
+
attr_reader :splitter
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# The final backup Package this model will create.
|
|
80
|
+
attr_reader :package
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# The time when the backup initiated (in format: 2011.02.20.03.29.59)
|
|
84
|
+
attr_reader :time
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Hooks which can be run before or after the backup process
|
|
88
|
+
attr_reader :hooks
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Takes a trigger, label and the configuration block.
|
|
92
|
+
# After the instance has evaluated the configuration block
|
|
93
|
+
# to configure the model, it will be appended to Model.all
|
|
94
|
+
def initialize(trigger, label, &block)
|
|
95
|
+
@trigger = trigger.to_s
|
|
96
|
+
@label = label.to_s
|
|
97
|
+
|
|
98
|
+
# default noop hooks
|
|
99
|
+
@hooks = Hooks.new(self)
|
|
100
|
+
|
|
101
|
+
procedure_instance_variables.each do |variable|
|
|
102
|
+
instance_variable_set(variable, Array.new)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
instance_eval(&block) if block_given?
|
|
106
|
+
Model.all << self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Adds an archive to the array of archives
|
|
111
|
+
# to store during the backup process
|
|
112
|
+
def archive(name, &block)
|
|
113
|
+
@archives << Archive.new(self, name, &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Adds a database to the array of databases
|
|
118
|
+
# to dump during the backup process
|
|
119
|
+
def database(name, &block)
|
|
120
|
+
@databases << get_class_from_scope(Database, name).new(self, &block)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Adds a storage method to the array of storage
|
|
125
|
+
# methods to use during the backup process
|
|
126
|
+
def store_with(name, storage_id = nil, &block)
|
|
127
|
+
@storages << get_class_from_scope(Storage, name).new(self, storage_id, &block)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
# Adds a syncer method to the array of syncer
|
|
132
|
+
# methods to use during the backup process
|
|
133
|
+
def sync_with(name, &block)
|
|
134
|
+
##
|
|
135
|
+
# Warn user of DSL changes
|
|
136
|
+
case name.to_s
|
|
137
|
+
when 'Backup::Config::RSync'
|
|
138
|
+
Logger.warn Errors::ConfigError.new(<<-EOS)
|
|
139
|
+
Configuration Update Needed for Syncer::RSync
|
|
140
|
+
The RSync Syncer has been split into three separate modules:
|
|
141
|
+
RSync::Local, RSync::Push and RSync::Pull
|
|
142
|
+
Please update your configuration.
|
|
143
|
+
i.e. 'sync_with RSync' is now 'sync_with RSync::Push'
|
|
144
|
+
EOS
|
|
145
|
+
name = 'RSync::Push'
|
|
146
|
+
when /(Backup::Config::S3|Backup::Config::CloudFiles)/
|
|
147
|
+
syncer = $1.split('::')[2]
|
|
148
|
+
Logger.warn Errors::ConfigError.new(<<-EOS)
|
|
149
|
+
Configuration Update Needed for '#{ syncer }' Syncer.
|
|
150
|
+
This Syncer is now referenced as Cloud::#{ syncer }
|
|
151
|
+
i.e. 'sync_with #{ syncer }' is now 'sync_with Cloud::#{ syncer }'
|
|
152
|
+
EOS
|
|
153
|
+
name = "Cloud::#{ syncer }"
|
|
154
|
+
end
|
|
155
|
+
@syncers << get_class_from_scope(Syncer, name).new(&block)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
# Adds a notifier to the array of notifiers
|
|
160
|
+
# to use during the backup process
|
|
161
|
+
def notify_by(name, &block)
|
|
162
|
+
@notifiers << get_class_from_scope(Notifier, name).new(self, &block)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
##
|
|
166
|
+
# Adds an encryptor to use during the backup process
|
|
167
|
+
def encrypt_with(name, &block)
|
|
168
|
+
@encryptor = get_class_from_scope(Encryptor, name).new(&block)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
# Adds a compressor to use during the backup process
|
|
173
|
+
def compress_with(name, &block)
|
|
174
|
+
@compressor = get_class_from_scope(Compressor, name).new(&block)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
##
|
|
178
|
+
# Run a block of ruby code before the backup process
|
|
179
|
+
def before(&block)
|
|
180
|
+
@hooks.before &block
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
##
|
|
184
|
+
# Run a block of ruby code after the backup process
|
|
185
|
+
def after(&block)
|
|
186
|
+
@hooks.after &block
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
##
|
|
190
|
+
# Adds a method that allows the user to configure this backup model
|
|
191
|
+
# to use a Splitter, with the given +chunk_size+
|
|
192
|
+
# The +chunk_size+ (in megabytes) will later determine
|
|
193
|
+
# in how many chunks the backup needs to be split into
|
|
194
|
+
def split_into_chunks_of(chunk_size)
|
|
195
|
+
if chunk_size.is_a?(Integer)
|
|
196
|
+
@splitter = Splitter.new(self, chunk_size)
|
|
197
|
+
else
|
|
198
|
+
raise Errors::Model::ConfigurationError, <<-EOS
|
|
199
|
+
Invalid Chunk Size for Splitter
|
|
200
|
+
Argument to #split_into_chunks_of() must be an Integer
|
|
201
|
+
EOS
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
# Ensure DATA_PATH and DATA_PATH/TRIGGER are created
|
|
207
|
+
# if they do not yet exist
|
|
208
|
+
#
|
|
209
|
+
# Clean any temporary files and/or package files left over
|
|
210
|
+
# from the last time this model/trigger was performed.
|
|
211
|
+
# Logs warnings if files exist and are cleaned.
|
|
212
|
+
def prepare!
|
|
213
|
+
FileUtils.mkdir_p(File.join(Config.data_path, trigger))
|
|
214
|
+
Cleaner.prepare(self)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
##
|
|
218
|
+
# Performs the backup process
|
|
219
|
+
##
|
|
220
|
+
# [Databases]
|
|
221
|
+
# Runs all (if any) database objects to dump the databases
|
|
222
|
+
##
|
|
223
|
+
# [Archives]
|
|
224
|
+
# Runs all (if any) archive objects to package all their
|
|
225
|
+
# paths in to a single tar file and places it in the backup folder
|
|
226
|
+
##
|
|
227
|
+
# [Packaging]
|
|
228
|
+
# After all the database dumps and archives are placed inside
|
|
229
|
+
# the folder, it'll make a single .tar package (archive) out of it
|
|
230
|
+
##
|
|
231
|
+
# [Encryption]
|
|
232
|
+
# Optionally encrypts the packaged file with the configured encryptor
|
|
233
|
+
##
|
|
234
|
+
# [Compression]
|
|
235
|
+
# Optionally compresses the each Archive and Database dump with the configured compressor
|
|
236
|
+
##
|
|
237
|
+
# [Splitting]
|
|
238
|
+
# Optionally splits the backup file in to multiple smaller chunks before transferring them
|
|
239
|
+
##
|
|
240
|
+
# [Storages]
|
|
241
|
+
# Runs all (if any) storage objects to store the backups to remote locations
|
|
242
|
+
# and (if configured) it'll cycle the files on the remote location to limit the
|
|
243
|
+
# amount of backups stored on each individual location
|
|
244
|
+
##
|
|
245
|
+
# [Syncers]
|
|
246
|
+
# Runs all (if any) sync objects to store the backups to remote locations.
|
|
247
|
+
# A Syncer does not go through the process of packaging, compressing, encrypting backups.
|
|
248
|
+
# A Syncer directly transfers data from the filesystem to the remote location
|
|
249
|
+
##
|
|
250
|
+
# [Notifiers]
|
|
251
|
+
# Runs all (if any) notifier objects when a backup proces finished with or without
|
|
252
|
+
# any errors.
|
|
253
|
+
##
|
|
254
|
+
# [Cleaning]
|
|
255
|
+
# Once the final Packaging is complete, the temporary folder used will be removed.
|
|
256
|
+
# Then, once all Storages have run, the final packaged files will be removed.
|
|
257
|
+
# If any errors occur during the backup process, all temporary files will be left in place.
|
|
258
|
+
# If the error occurs before Packaging, then the temporary folder (tmp_path/trigger)
|
|
259
|
+
# will remain and may contain all or some of the configured Archives and/or Database dumps.
|
|
260
|
+
# If the error occurs after Packaging, but before the Storages complete, then the final
|
|
261
|
+
# packaged files (located in the root of tmp_path) will remain.
|
|
262
|
+
# *** Important *** If an error occurs and any of the above mentioned temporary files remain,
|
|
263
|
+
# those files *** will be removed *** before the next scheduled backup for the same trigger.
|
|
264
|
+
#
|
|
265
|
+
def perform!
|
|
266
|
+
@started_at = Time.now
|
|
267
|
+
@time = @started_at.strftime("%Y.%m.%d.%H.%M.%S")
|
|
268
|
+
log!(:started)
|
|
269
|
+
|
|
270
|
+
@hooks.perform!(:before)
|
|
271
|
+
|
|
272
|
+
if databases.any? or archives.any?
|
|
273
|
+
procedures.each do |procedure|
|
|
274
|
+
(procedure.call; next) if procedure.is_a?(Proc)
|
|
275
|
+
procedure.each(&:perform!)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
syncers.each(&:perform!)
|
|
280
|
+
notifiers.each(&:perform!)
|
|
281
|
+
|
|
282
|
+
@hooks.perform!(:after)
|
|
283
|
+
|
|
284
|
+
log!(:finished)
|
|
285
|
+
|
|
286
|
+
rescue Exception => err
|
|
287
|
+
fatal = !err.is_a?(StandardError)
|
|
288
|
+
|
|
289
|
+
err = Errors::ModelError.wrap(err, <<-EOS)
|
|
290
|
+
Backup for #{label} (#{trigger}) Failed!
|
|
291
|
+
An Error occured which has caused this Backup to abort before completion.
|
|
292
|
+
EOS
|
|
293
|
+
Logger.error err
|
|
294
|
+
Logger.error "\nBacktrace:\n\s\s" + err.backtrace.join("\n\s\s") + "\n\n"
|
|
295
|
+
|
|
296
|
+
Cleaner.warnings(self)
|
|
297
|
+
|
|
298
|
+
if fatal
|
|
299
|
+
Logger.error Errors::ModelError.new(<<-EOS)
|
|
300
|
+
This Error was Fatal and Backup will now exit.
|
|
301
|
+
If you have other Backup jobs (triggers) configured to run,
|
|
302
|
+
they will not be processed.
|
|
303
|
+
EOS
|
|
304
|
+
else
|
|
305
|
+
Logger.message Errors::ModelError.new(<<-EOS)
|
|
306
|
+
If you have other Backup jobs (triggers) configured to run,
|
|
307
|
+
Backup will now attempt to continue...
|
|
308
|
+
EOS
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
notifiers.each do |n|
|
|
312
|
+
begin
|
|
313
|
+
n.perform!(true)
|
|
314
|
+
rescue Exception; end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
exit(1) if fatal
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
##
|
|
323
|
+
# After all the databases and archives have been dumped and sorted,
|
|
324
|
+
# these files will be bundled in to a .tar archive (uncompressed),
|
|
325
|
+
# which may be optionally Encrypted and/or Split into multiple "chunks".
|
|
326
|
+
# All information about this final archive is stored in the @package.
|
|
327
|
+
# Once complete, the temporary folder used during packaging is removed.
|
|
328
|
+
def package!
|
|
329
|
+
@package = Package.new(self)
|
|
330
|
+
Packager.package!(self)
|
|
331
|
+
Cleaner.remove_packaging(self)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
##
|
|
335
|
+
# Removes the final package file(s) once all configured Storages have run.
|
|
336
|
+
def clean!
|
|
337
|
+
Cleaner.remove_package(@package)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
##
|
|
341
|
+
# Returns an array of procedures
|
|
342
|
+
def procedures
|
|
343
|
+
[databases, archives, lambda { package! }, storages, lambda { clean! }]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
##
|
|
347
|
+
# Returns an Array of the names (String) of the procedure instance variables
|
|
348
|
+
def procedure_instance_variables
|
|
349
|
+
[:@databases, :@archives, :@storages, :@notifiers, :@syncers]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
##
|
|
353
|
+
# Returns the class/model specified by +name+ inside of +scope+.
|
|
354
|
+
# +scope+ should be a Class/Module.
|
|
355
|
+
# +name+ may be Class/Module or String representation
|
|
356
|
+
# of any namespace which exists under +scope+.
|
|
357
|
+
#
|
|
358
|
+
# The 'Backup::Config::' namespace is stripped from +name+,
|
|
359
|
+
# since this is the namespace where we define module namespaces
|
|
360
|
+
# for use with Model's DSL methods.
|
|
361
|
+
#
|
|
362
|
+
# Examples:
|
|
363
|
+
# get_class_from_scope(Backup::Database, 'MySQL')
|
|
364
|
+
# returns the class Backup::Database::MySQL
|
|
365
|
+
#
|
|
366
|
+
# get_class_from_scope(Backup::Syncer, Backup::Config::RSync::Local)
|
|
367
|
+
# returns the class Backup::Syncer::RSync::Local
|
|
368
|
+
#
|
|
369
|
+
def get_class_from_scope(scope, name)
|
|
370
|
+
klass = scope
|
|
371
|
+
name = name.to_s.sub(/^Backup::Config::/, '')
|
|
372
|
+
name.split('::').each do |chunk|
|
|
373
|
+
klass = klass.const_get(chunk)
|
|
374
|
+
end
|
|
375
|
+
klass
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
##
|
|
379
|
+
# Logs messages when the backup starts and finishes
|
|
380
|
+
def log!(action)
|
|
381
|
+
case action
|
|
382
|
+
when :started
|
|
383
|
+
Logger.message "Performing Backup for '#{label} (#{trigger})'!\n" +
|
|
384
|
+
"[ backup #{ Version.current } : #{ RUBY_DESCRIPTION } ]"
|
|
385
|
+
|
|
386
|
+
when :finished
|
|
387
|
+
msg = "Backup for '#{ label } (#{ trigger })' " +
|
|
388
|
+
"Completed %s in #{ elapsed_time }"
|
|
389
|
+
if Logger.has_warnings?
|
|
390
|
+
Logger.warn msg % 'Successfully (with Warnings)'
|
|
391
|
+
else
|
|
392
|
+
Logger.message msg % 'Successfully'
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
##
|
|
398
|
+
# Returns a string representing the elapsed time since the backup started.
|
|
399
|
+
def elapsed_time
|
|
400
|
+
duration = Time.now.to_i - @started_at.to_i
|
|
401
|
+
hours = duration / 3600
|
|
402
|
+
remainder = duration - (hours * 3600)
|
|
403
|
+
minutes = remainder / 60
|
|
404
|
+
seconds = remainder - (minutes * 60)
|
|
405
|
+
'%02d:%02d:%02d' % [hours, minutes, seconds]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
end
|
|
409
|
+
end
|