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.
Files changed (190) hide show
  1. data/.gitignore +8 -0
  2. data/.travis.yml +10 -0
  3. data/Gemfile +28 -0
  4. data/Guardfile +23 -0
  5. data/LICENSE.md +24 -0
  6. data/README.md +478 -0
  7. data/backup.gemspec +32 -0
  8. data/bin/backup +11 -0
  9. data/lib/backup.rb +133 -0
  10. data/lib/backup/archive.rb +117 -0
  11. data/lib/backup/binder.rb +22 -0
  12. data/lib/backup/cleaner.rb +121 -0
  13. data/lib/backup/cli/helpers.rb +93 -0
  14. data/lib/backup/cli/utility.rb +255 -0
  15. data/lib/backup/compressor/base.rb +35 -0
  16. data/lib/backup/compressor/bzip2.rb +50 -0
  17. data/lib/backup/compressor/custom.rb +53 -0
  18. data/lib/backup/compressor/gzip.rb +50 -0
  19. data/lib/backup/compressor/lzma.rb +52 -0
  20. data/lib/backup/compressor/pbzip2.rb +59 -0
  21. data/lib/backup/config.rb +174 -0
  22. data/lib/backup/configuration.rb +33 -0
  23. data/lib/backup/configuration/helpers.rb +130 -0
  24. data/lib/backup/configuration/store.rb +24 -0
  25. data/lib/backup/database/base.rb +53 -0
  26. data/lib/backup/database/mongodb.rb +230 -0
  27. data/lib/backup/database/mysql.rb +160 -0
  28. data/lib/backup/database/postgresql.rb +144 -0
  29. data/lib/backup/database/redis.rb +136 -0
  30. data/lib/backup/database/riak.rb +67 -0
  31. data/lib/backup/dependency.rb +108 -0
  32. data/lib/backup/encryptor/base.rb +29 -0
  33. data/lib/backup/encryptor/gpg.rb +760 -0
  34. data/lib/backup/encryptor/open_ssl.rb +72 -0
  35. data/lib/backup/errors.rb +124 -0
  36. data/lib/backup/hooks.rb +68 -0
  37. data/lib/backup/logger.rb +152 -0
  38. data/lib/backup/model.rb +409 -0
  39. data/lib/backup/notifier/base.rb +81 -0
  40. data/lib/backup/notifier/campfire.rb +155 -0
  41. data/lib/backup/notifier/hipchat.rb +93 -0
  42. data/lib/backup/notifier/mail.rb +206 -0
  43. data/lib/backup/notifier/prowl.rb +65 -0
  44. data/lib/backup/notifier/pushover.rb +88 -0
  45. data/lib/backup/notifier/twitter.rb +70 -0
  46. data/lib/backup/package.rb +47 -0
  47. data/lib/backup/packager.rb +100 -0
  48. data/lib/backup/pipeline.rb +110 -0
  49. data/lib/backup/splitter.rb +75 -0
  50. data/lib/backup/storage/base.rb +99 -0
  51. data/lib/backup/storage/cloudfiles.rb +87 -0
  52. data/lib/backup/storage/cycler.rb +117 -0
  53. data/lib/backup/storage/dropbox.rb +178 -0
  54. data/lib/backup/storage/ftp.rb +119 -0
  55. data/lib/backup/storage/local.rb +82 -0
  56. data/lib/backup/storage/ninefold.rb +116 -0
  57. data/lib/backup/storage/rsync.rb +149 -0
  58. data/lib/backup/storage/s3.rb +94 -0
  59. data/lib/backup/storage/scp.rb +99 -0
  60. data/lib/backup/storage/sftp.rb +108 -0
  61. data/lib/backup/syncer/base.rb +46 -0
  62. data/lib/backup/syncer/cloud/base.rb +247 -0
  63. data/lib/backup/syncer/cloud/cloud_files.rb +78 -0
  64. data/lib/backup/syncer/cloud/s3.rb +68 -0
  65. data/lib/backup/syncer/rsync/base.rb +49 -0
  66. data/lib/backup/syncer/rsync/local.rb +55 -0
  67. data/lib/backup/syncer/rsync/pull.rb +36 -0
  68. data/lib/backup/syncer/rsync/push.rb +116 -0
  69. data/lib/backup/template.rb +46 -0
  70. data/lib/backup/version.rb +43 -0
  71. data/spec-live/.gitignore +6 -0
  72. data/spec-live/README +7 -0
  73. data/spec-live/backups/config.rb +83 -0
  74. data/spec-live/backups/config.yml.template +46 -0
  75. data/spec-live/backups/models.rb +184 -0
  76. data/spec-live/compressor/custom_spec.rb +30 -0
  77. data/spec-live/compressor/gzip_spec.rb +30 -0
  78. data/spec-live/encryptor/gpg_keys.rb +239 -0
  79. data/spec-live/encryptor/gpg_spec.rb +287 -0
  80. data/spec-live/notifier/mail_spec.rb +121 -0
  81. data/spec-live/spec_helper.rb +151 -0
  82. data/spec-live/storage/dropbox_spec.rb +151 -0
  83. data/spec-live/storage/local_spec.rb +83 -0
  84. data/spec-live/storage/scp_spec.rb +193 -0
  85. data/spec-live/syncer/cloud/s3_spec.rb +124 -0
  86. data/spec/archive_spec.rb +335 -0
  87. data/spec/cleaner_spec.rb +312 -0
  88. data/spec/cli/helpers_spec.rb +301 -0
  89. data/spec/cli/utility_spec.rb +411 -0
  90. data/spec/compressor/base_spec.rb +52 -0
  91. data/spec/compressor/bzip2_spec.rb +217 -0
  92. data/spec/compressor/custom_spec.rb +106 -0
  93. data/spec/compressor/gzip_spec.rb +217 -0
  94. data/spec/compressor/lzma_spec.rb +123 -0
  95. data/spec/compressor/pbzip2_spec.rb +165 -0
  96. data/spec/config_spec.rb +321 -0
  97. data/spec/configuration/helpers_spec.rb +247 -0
  98. data/spec/configuration/store_spec.rb +39 -0
  99. data/spec/configuration_spec.rb +62 -0
  100. data/spec/database/base_spec.rb +63 -0
  101. data/spec/database/mongodb_spec.rb +510 -0
  102. data/spec/database/mysql_spec.rb +411 -0
  103. data/spec/database/postgresql_spec.rb +353 -0
  104. data/spec/database/redis_spec.rb +334 -0
  105. data/spec/database/riak_spec.rb +176 -0
  106. data/spec/dependency_spec.rb +51 -0
  107. data/spec/encryptor/base_spec.rb +40 -0
  108. data/spec/encryptor/gpg_spec.rb +909 -0
  109. data/spec/encryptor/open_ssl_spec.rb +148 -0
  110. data/spec/errors_spec.rb +306 -0
  111. data/spec/hooks_spec.rb +35 -0
  112. data/spec/logger_spec.rb +367 -0
  113. data/spec/model_spec.rb +694 -0
  114. data/spec/notifier/base_spec.rb +104 -0
  115. data/spec/notifier/campfire_spec.rb +217 -0
  116. data/spec/notifier/hipchat_spec.rb +211 -0
  117. data/spec/notifier/mail_spec.rb +316 -0
  118. data/spec/notifier/prowl_spec.rb +138 -0
  119. data/spec/notifier/pushover_spec.rb +123 -0
  120. data/spec/notifier/twitter_spec.rb +153 -0
  121. data/spec/package_spec.rb +61 -0
  122. data/spec/packager_spec.rb +213 -0
  123. data/spec/pipeline_spec.rb +259 -0
  124. data/spec/spec_helper.rb +60 -0
  125. data/spec/splitter_spec.rb +120 -0
  126. data/spec/storage/base_spec.rb +166 -0
  127. data/spec/storage/cloudfiles_spec.rb +254 -0
  128. data/spec/storage/cycler_spec.rb +247 -0
  129. data/spec/storage/dropbox_spec.rb +480 -0
  130. data/spec/storage/ftp_spec.rb +271 -0
  131. data/spec/storage/local_spec.rb +259 -0
  132. data/spec/storage/ninefold_spec.rb +343 -0
  133. data/spec/storage/rsync_spec.rb +362 -0
  134. data/spec/storage/s3_spec.rb +245 -0
  135. data/spec/storage/scp_spec.rb +233 -0
  136. data/spec/storage/sftp_spec.rb +244 -0
  137. data/spec/syncer/base_spec.rb +109 -0
  138. data/spec/syncer/cloud/base_spec.rb +515 -0
  139. data/spec/syncer/cloud/cloud_files_spec.rb +181 -0
  140. data/spec/syncer/cloud/s3_spec.rb +174 -0
  141. data/spec/syncer/rsync/base_spec.rb +98 -0
  142. data/spec/syncer/rsync/local_spec.rb +149 -0
  143. data/spec/syncer/rsync/pull_spec.rb +98 -0
  144. data/spec/syncer/rsync/push_spec.rb +333 -0
  145. data/spec/version_spec.rb +21 -0
  146. data/templates/cli/utility/archive +25 -0
  147. data/templates/cli/utility/compressor/bzip2 +4 -0
  148. data/templates/cli/utility/compressor/custom +11 -0
  149. data/templates/cli/utility/compressor/gzip +4 -0
  150. data/templates/cli/utility/compressor/lzma +10 -0
  151. data/templates/cli/utility/compressor/pbzip2 +10 -0
  152. data/templates/cli/utility/config +32 -0
  153. data/templates/cli/utility/database/mongodb +18 -0
  154. data/templates/cli/utility/database/mysql +21 -0
  155. data/templates/cli/utility/database/postgresql +17 -0
  156. data/templates/cli/utility/database/redis +16 -0
  157. data/templates/cli/utility/database/riak +11 -0
  158. data/templates/cli/utility/encryptor/gpg +27 -0
  159. data/templates/cli/utility/encryptor/openssl +9 -0
  160. data/templates/cli/utility/model.erb +23 -0
  161. data/templates/cli/utility/notifier/campfire +12 -0
  162. data/templates/cli/utility/notifier/hipchat +15 -0
  163. data/templates/cli/utility/notifier/mail +22 -0
  164. data/templates/cli/utility/notifier/prowl +11 -0
  165. data/templates/cli/utility/notifier/pushover +11 -0
  166. data/templates/cli/utility/notifier/twitter +13 -0
  167. data/templates/cli/utility/splitter +7 -0
  168. data/templates/cli/utility/storage/cloud_files +22 -0
  169. data/templates/cli/utility/storage/dropbox +20 -0
  170. data/templates/cli/utility/storage/ftp +12 -0
  171. data/templates/cli/utility/storage/local +7 -0
  172. data/templates/cli/utility/storage/ninefold +9 -0
  173. data/templates/cli/utility/storage/rsync +11 -0
  174. data/templates/cli/utility/storage/s3 +19 -0
  175. data/templates/cli/utility/storage/scp +11 -0
  176. data/templates/cli/utility/storage/sftp +11 -0
  177. data/templates/cli/utility/syncer/cloud_files +46 -0
  178. data/templates/cli/utility/syncer/rsync_local +12 -0
  179. data/templates/cli/utility/syncer/rsync_pull +17 -0
  180. data/templates/cli/utility/syncer/rsync_push +17 -0
  181. data/templates/cli/utility/syncer/s3 +43 -0
  182. data/templates/general/links +11 -0
  183. data/templates/general/version.erb +2 -0
  184. data/templates/notifier/mail/failure.erb +9 -0
  185. data/templates/notifier/mail/success.erb +7 -0
  186. data/templates/notifier/mail/warning.erb +9 -0
  187. data/templates/storage/dropbox/authorization_url.erb +6 -0
  188. data/templates/storage/dropbox/authorized.erb +4 -0
  189. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  190. 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
@@ -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
@@ -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