backupii 0.1.0.pre.alpha.1

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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +37 -0
  4. data/bin/backupii +5 -0
  5. data/bin/docker_test +24 -0
  6. data/lib/backup/archive.rb +171 -0
  7. data/lib/backup/binder.rb +23 -0
  8. data/lib/backup/cleaner.rb +114 -0
  9. data/lib/backup/cli.rb +376 -0
  10. data/lib/backup/cloud_io/base.rb +40 -0
  11. data/lib/backup/cloud_io/cloud_files.rb +301 -0
  12. data/lib/backup/cloud_io/s3.rb +256 -0
  13. data/lib/backup/compressor/base.rb +34 -0
  14. data/lib/backup/compressor/bzip2.rb +37 -0
  15. data/lib/backup/compressor/custom.rb +51 -0
  16. data/lib/backup/compressor/gzip.rb +76 -0
  17. data/lib/backup/config/dsl.rb +103 -0
  18. data/lib/backup/config/helpers.rb +139 -0
  19. data/lib/backup/config.rb +122 -0
  20. data/lib/backup/database/base.rb +89 -0
  21. data/lib/backup/database/mongodb.rb +189 -0
  22. data/lib/backup/database/mysql.rb +194 -0
  23. data/lib/backup/database/openldap.rb +97 -0
  24. data/lib/backup/database/postgresql.rb +134 -0
  25. data/lib/backup/database/redis.rb +179 -0
  26. data/lib/backup/database/riak.rb +82 -0
  27. data/lib/backup/database/sqlite.rb +57 -0
  28. data/lib/backup/encryptor/base.rb +29 -0
  29. data/lib/backup/encryptor/gpg.rb +745 -0
  30. data/lib/backup/encryptor/open_ssl.rb +76 -0
  31. data/lib/backup/errors.rb +55 -0
  32. data/lib/backup/logger/console.rb +50 -0
  33. data/lib/backup/logger/fog_adapter.rb +27 -0
  34. data/lib/backup/logger/logfile.rb +134 -0
  35. data/lib/backup/logger/syslog.rb +116 -0
  36. data/lib/backup/logger.rb +199 -0
  37. data/lib/backup/model.rb +478 -0
  38. data/lib/backup/notifier/base.rb +128 -0
  39. data/lib/backup/notifier/campfire.rb +63 -0
  40. data/lib/backup/notifier/command.rb +101 -0
  41. data/lib/backup/notifier/datadog.rb +107 -0
  42. data/lib/backup/notifier/flowdock.rb +101 -0
  43. data/lib/backup/notifier/hipchat.rb +118 -0
  44. data/lib/backup/notifier/http_post.rb +116 -0
  45. data/lib/backup/notifier/mail.rb +235 -0
  46. data/lib/backup/notifier/nagios.rb +67 -0
  47. data/lib/backup/notifier/pagerduty.rb +82 -0
  48. data/lib/backup/notifier/prowl.rb +70 -0
  49. data/lib/backup/notifier/pushover.rb +73 -0
  50. data/lib/backup/notifier/ses.rb +126 -0
  51. data/lib/backup/notifier/slack.rb +149 -0
  52. data/lib/backup/notifier/twitter.rb +57 -0
  53. data/lib/backup/notifier/zabbix.rb +62 -0
  54. data/lib/backup/package.rb +53 -0
  55. data/lib/backup/packager.rb +108 -0
  56. data/lib/backup/pipeline.rb +122 -0
  57. data/lib/backup/splitter.rb +75 -0
  58. data/lib/backup/storage/base.rb +72 -0
  59. data/lib/backup/storage/cloud_files.rb +158 -0
  60. data/lib/backup/storage/cycler.rb +73 -0
  61. data/lib/backup/storage/dropbox.rb +208 -0
  62. data/lib/backup/storage/ftp.rb +118 -0
  63. data/lib/backup/storage/local.rb +63 -0
  64. data/lib/backup/storage/qiniu.rb +68 -0
  65. data/lib/backup/storage/rsync.rb +251 -0
  66. data/lib/backup/storage/s3.rb +157 -0
  67. data/lib/backup/storage/scp.rb +67 -0
  68. data/lib/backup/storage/sftp.rb +82 -0
  69. data/lib/backup/syncer/base.rb +70 -0
  70. data/lib/backup/syncer/cloud/base.rb +180 -0
  71. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  72. data/lib/backup/syncer/cloud/local_file.rb +99 -0
  73. data/lib/backup/syncer/cloud/s3.rb +118 -0
  74. data/lib/backup/syncer/rsync/base.rb +55 -0
  75. data/lib/backup/syncer/rsync/local.rb +29 -0
  76. data/lib/backup/syncer/rsync/pull.rb +49 -0
  77. data/lib/backup/syncer/rsync/push.rb +206 -0
  78. data/lib/backup/template.rb +45 -0
  79. data/lib/backup/utilities.rb +235 -0
  80. data/lib/backup/version.rb +5 -0
  81. data/lib/backup.rb +141 -0
  82. data/templates/cli/archive +28 -0
  83. data/templates/cli/compressor/bzip2 +4 -0
  84. data/templates/cli/compressor/custom +7 -0
  85. data/templates/cli/compressor/gzip +4 -0
  86. data/templates/cli/config +123 -0
  87. data/templates/cli/databases/mongodb +15 -0
  88. data/templates/cli/databases/mysql +18 -0
  89. data/templates/cli/databases/openldap +24 -0
  90. data/templates/cli/databases/postgresql +16 -0
  91. data/templates/cli/databases/redis +16 -0
  92. data/templates/cli/databases/riak +17 -0
  93. data/templates/cli/databases/sqlite +11 -0
  94. data/templates/cli/encryptor/gpg +27 -0
  95. data/templates/cli/encryptor/openssl +9 -0
  96. data/templates/cli/model +26 -0
  97. data/templates/cli/notifier/zabbix +15 -0
  98. data/templates/cli/notifiers/campfire +12 -0
  99. data/templates/cli/notifiers/command +32 -0
  100. data/templates/cli/notifiers/datadog +57 -0
  101. data/templates/cli/notifiers/flowdock +16 -0
  102. data/templates/cli/notifiers/hipchat +16 -0
  103. data/templates/cli/notifiers/http_post +32 -0
  104. data/templates/cli/notifiers/mail +24 -0
  105. data/templates/cli/notifiers/nagios +13 -0
  106. data/templates/cli/notifiers/pagerduty +12 -0
  107. data/templates/cli/notifiers/prowl +11 -0
  108. data/templates/cli/notifiers/pushover +11 -0
  109. data/templates/cli/notifiers/ses +15 -0
  110. data/templates/cli/notifiers/slack +22 -0
  111. data/templates/cli/notifiers/twitter +13 -0
  112. data/templates/cli/splitter +7 -0
  113. data/templates/cli/storages/cloud_files +11 -0
  114. data/templates/cli/storages/dropbox +20 -0
  115. data/templates/cli/storages/ftp +13 -0
  116. data/templates/cli/storages/local +8 -0
  117. data/templates/cli/storages/qiniu +12 -0
  118. data/templates/cli/storages/rsync +17 -0
  119. data/templates/cli/storages/s3 +16 -0
  120. data/templates/cli/storages/scp +15 -0
  121. data/templates/cli/storages/sftp +15 -0
  122. data/templates/cli/syncers/cloud_files +22 -0
  123. data/templates/cli/syncers/rsync_local +20 -0
  124. data/templates/cli/syncers/rsync_pull +28 -0
  125. data/templates/cli/syncers/rsync_push +28 -0
  126. data/templates/cli/syncers/s3 +27 -0
  127. data/templates/general/links +3 -0
  128. data/templates/general/version.erb +2 -0
  129. data/templates/notifier/mail/failure.erb +16 -0
  130. data/templates/notifier/mail/success.erb +16 -0
  131. data/templates/notifier/mail/warning.erb +16 -0
  132. data/templates/storage/dropbox/authorization_url.erb +6 -0
  133. data/templates/storage/dropbox/authorized.erb +4 -0
  134. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  135. metadata +507 -0
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "json"
5
+
6
+ module Backup
7
+ module Notifier
8
+ class Slack < Base
9
+ ##
10
+ # The incoming webhook url
11
+ attr_accessor :webhook_url
12
+
13
+ ##
14
+ # The channel to send messages to
15
+ attr_accessor :channel
16
+
17
+ ##
18
+ # The username to display along with the notification
19
+ attr_accessor :username
20
+
21
+ ##
22
+ # The emoji icon to display along with the notification
23
+ #
24
+ # See http://www.emoji-cheat-sheet.com for a list of icons.
25
+ #
26
+ # Default: :floppy_disk:
27
+ attr_accessor :icon_emoji
28
+
29
+ ##
30
+ # Array of statuses for which the log file should be attached.
31
+ #
32
+ # Available statuses are: `:success`, `:warning` and `:failure`.
33
+ # Default: [:warning, :failure]
34
+ attr_accessor :send_log_on
35
+
36
+ def initialize(model, &block)
37
+ super
38
+ instance_eval(&block) if block_given?
39
+
40
+ @send_log_on ||= [:warning, :failure]
41
+ @icon_emoji ||= ":floppy_disk:"
42
+ end
43
+
44
+ private
45
+
46
+ ##
47
+ # Notify the user of the backup operation results.
48
+ #
49
+ # `status` indicates one of the following:
50
+ #
51
+ # `:success`
52
+ # : The backup completed successfully.
53
+ # : Notification will be sent if `on_success` is `true`.
54
+ #
55
+ # `:warning`
56
+ # : The backup completed successfully, but warnings were logged.
57
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
58
+ #
59
+ # `:failure`
60
+ # : The backup operation failed.
61
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
62
+ #
63
+ def notify!(status)
64
+ data = {
65
+ text: message.call(model, status: status_data_for(status)),
66
+ attachments: [attachment(status)]
67
+ }
68
+ [:channel, :username, :icon_emoji].each do |param|
69
+ val = send(param)
70
+ data.merge!(param => val) if val
71
+ end
72
+
73
+ options = {
74
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" },
75
+ body: URI.encode_www_form(payload: JSON.dump(data))
76
+ }
77
+ options[:expects] = 200 # raise error if unsuccessful
78
+ Excon.post(uri, options)
79
+ end
80
+
81
+ def attachment(status)
82
+ {
83
+ fallback: "#{title(status)} - Job: #{model.label} (#{model.trigger})",
84
+ text: title(status),
85
+ color: color(status),
86
+ fields: [
87
+ {
88
+ title: "Job",
89
+ value: "#{model.label} (#{model.trigger})",
90
+ short: false
91
+ },
92
+ {
93
+ title: "Started",
94
+ value: model.started_at,
95
+ short: true
96
+ },
97
+ {
98
+ title: "Finished",
99
+ value: model.finished_at,
100
+ short: true
101
+ },
102
+ {
103
+ title: "Duration",
104
+ value: model.duration,
105
+ short: true
106
+ },
107
+ {
108
+ title: "Version",
109
+ value: "Backup v#{Backup::VERSION}\nRuby: #{RUBY_DESCRIPTION}",
110
+ short: false
111
+ },
112
+ log_field(status)
113
+ ].compact
114
+ }
115
+ end
116
+
117
+ def log_field(status)
118
+ send_log = send_log_on.include?(status)
119
+ return unless send_log
120
+
121
+ {
122
+ title: "Detailed Backup Log",
123
+ value: Logger.messages.map(&:formatted_lines).flatten.join("\n"),
124
+ short: false
125
+ }
126
+ end
127
+
128
+ def color(status)
129
+ case status
130
+ when :success then "good"
131
+ when :failure then "danger"
132
+ when :warning then "warning"
133
+ end
134
+ end
135
+
136
+ def title(status)
137
+ case status
138
+ when :success then "Backup Completed Successfully!"
139
+ when :failure then "Backup Failed!"
140
+ when :warning then "Backup Completed Successfully (with Warnings)!"
141
+ end
142
+ end
143
+
144
+ def uri
145
+ @uri ||= webhook_url
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "twitter"
4
+
5
+ module Backup
6
+ module Notifier
7
+ class Twitter < Base
8
+ ##
9
+ # Twitter consumer key credentials
10
+ attr_accessor :consumer_key, :consumer_secret
11
+
12
+ ##
13
+ # OAuth credentials
14
+ attr_accessor :oauth_token, :oauth_token_secret
15
+
16
+ def initialize(model, &block)
17
+ super
18
+ instance_eval(&block) if block_given?
19
+ end
20
+
21
+ private
22
+
23
+ ##
24
+ # Notify the user of the backup operation results.
25
+ #
26
+ # `status` indicates one of the following:
27
+ #
28
+ # `:success`
29
+ # : The backup completed successfully.
30
+ # : Notification will be sent if `on_success` is `true`.
31
+ #
32
+ # `:warning`
33
+ # : The backup completed successfully, but warnings were logged.
34
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
35
+ #
36
+ # `:failure`
37
+ # : The backup operation failed.
38
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
39
+ #
40
+ def notify!(status)
41
+ send_message(message.call(model, status: status_data_for(status)))
42
+ end
43
+
44
+ # Twitter::Client will raise an error if unsuccessful.
45
+ def send_message(message)
46
+ client = ::Twitter::REST::Client.new do |config|
47
+ config.consumer_key = @consumer_key
48
+ config.consumer_secret = @consumer_secret
49
+ config.access_token = @oauth_token
50
+ config.access_token_secret = @oauth_token_secret
51
+ end
52
+
53
+ client.update(message)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ module Notifier
5
+ class Zabbix < Base
6
+ attr_accessor :zabbix_host
7
+
8
+ attr_accessor :zabbix_port
9
+
10
+ attr_accessor :service_name
11
+
12
+ attr_accessor :service_host
13
+
14
+ attr_accessor :item_key
15
+
16
+ def initialize(model, &block)
17
+ super
18
+ instance_eval(&block) if block_given?
19
+
20
+ @zabbix_host ||= Config.hostname
21
+ @zabbix_port ||= 10_051
22
+ @service_name ||= "Backup #{model.trigger}"
23
+ @service_host ||= Config.hostname
24
+ @item_key ||= "backup_status"
25
+ end
26
+
27
+ private
28
+
29
+ ##
30
+ # Notify the user of the backup operation results.
31
+ #
32
+ # `status` indicates one of the following:
33
+ #
34
+ # `:success`
35
+ # : The backup completed successfully.
36
+ # : Notification will be sent if `on_success` is `true`.
37
+ #
38
+ # `:warning`
39
+ # : The backup completed successfully, but warnings were logged.
40
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
41
+ #
42
+ # `:failure`
43
+ # : The backup operation failed.
44
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
45
+ #
46
+ def notify!(status)
47
+ send_message(message.call(model, status: status_data_for(status)))
48
+ end
49
+
50
+ def send_message(message)
51
+ msg = [service_host, service_name, model.exit_status, message].join("\t")
52
+ cmd = utility(:zabbix_sender).to_s +
53
+ " -z '#{zabbix_host}'" \
54
+ " -p '#{zabbix_port}'" \
55
+ " -s #{service_host}" \
56
+ " -k #{item_key}" \
57
+ " -o '#{msg}'"
58
+ run("echo '#{msg}' | #{cmd}")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ class Package
5
+ ##
6
+ # The time when the backup initiated (in format: 2011.02.20.03.29.59)
7
+ attr_accessor :time
8
+
9
+ ##
10
+ # The trigger which initiated the backup process
11
+ attr_reader :trigger
12
+
13
+ ##
14
+ # Extension for the final archive file(s)
15
+ attr_accessor :extension
16
+
17
+ ##
18
+ # Set by the Splitter if the final archive was "chunked"
19
+ attr_accessor :chunk_suffixes
20
+
21
+ ##
22
+ # If true, the Cycler will not attempt to remove the package when Cycling.
23
+ attr_accessor :no_cycle
24
+
25
+ ##
26
+ # The version of Backup used to create the package
27
+ attr_reader :version
28
+
29
+ def initialize(model)
30
+ @trigger = model.trigger.dup
31
+ @extension = "tar".dup
32
+ @chunk_suffixes = []
33
+ @no_cycle = false
34
+ @version = VERSION
35
+ end
36
+
37
+ def filenames
38
+ if chunk_suffixes.empty?
39
+ [basename]
40
+ else
41
+ chunk_suffixes.map { |suffix| "#{basename}-#{suffix}" }
42
+ end
43
+ end
44
+
45
+ def basename
46
+ "#{trigger}.#{extension}"
47
+ end
48
+
49
+ def time_as_object
50
+ Time.strptime(time, "%Y.%m.%d.%H.%M.%S")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ module Packager
5
+ class Error < Backup::Error; end
6
+
7
+ class << self
8
+ include Utilities::Helpers
9
+
10
+ ##
11
+ # Build the final package for the backup model.
12
+ def package!(model)
13
+ @package = model.package
14
+ @encryptor = model.encryptor
15
+ @splitter = model.splitter
16
+ @pipeline = Pipeline.new
17
+
18
+ Logger.info "Packaging the backup files..."
19
+ procedure.call
20
+
21
+ if @pipeline.success?
22
+ Logger.info "Packaging Complete!"
23
+ else
24
+ raise Error, "Failed to Create Backup Package\n" +
25
+ @pipeline.error_messages
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ ##
32
+ # Builds a chain of nested Procs which adds each command to a Pipeline
33
+ # needed to package the final command to package the backup.
34
+ # This is done so that the Encryptor and Splitter have the ability
35
+ # to perform actions before and after the final command is executed.
36
+ # No Encryptors currently utilize this, however the Splitter does.
37
+ def procedure
38
+ stack = []
39
+
40
+ ##
41
+ # Initial `tar` command to package the temporary backup folder.
42
+ # The command's output will then be either piped to the Encryptor
43
+ # or the Splitter (if no Encryptor), or through `cat` into the final
44
+ # output file if neither are configured.
45
+ @pipeline.add(
46
+ "#{utility(:tar)} -cf - " \
47
+ "-C '#{Config.tmp_path}' '#{@package.trigger}'",
48
+ tar_success_codes
49
+ )
50
+
51
+ ##
52
+ # If an Encryptor was configured, it will be called first
53
+ # to add the encryption utility command to be piped through,
54
+ # and amend the final package extension.
55
+ # It's output will then be either piped into a Splitter,
56
+ # or through `cat` into the final output file.
57
+ if @encryptor
58
+ stack << lambda do
59
+ @encryptor.encrypt_with do |command, ext|
60
+ @pipeline << command
61
+ @package.extension << ext
62
+ stack.shift.call
63
+ end
64
+ end
65
+ end
66
+
67
+ ##
68
+ # If a Splitter was configured, the `split` utility command will be
69
+ # added to the Pipeline to split the final output into multiple files.
70
+ # Once the Proc executing the Pipeline has completed and returns back
71
+ # to the Splitter, it will check the final output files to determine
72
+ # if the backup was indeed split.
73
+ # If so, it will set the package's chunk_suffixes. If not, it will
74
+ # remove the '-aa' suffix from the only file created by `split`.
75
+ #
76
+ # If no Splitter was configured, the final file output will be
77
+ # piped through `cat` into the final output file.
78
+ stack <<
79
+ if @splitter
80
+ lambda do
81
+ @splitter.split_with do |command|
82
+ @pipeline << command
83
+ stack.shift.call
84
+ end
85
+ end
86
+ else
87
+ lambda do
88
+ outfile = File.join(Config.tmp_path, @package.basename)
89
+ @pipeline << "#{utility(:cat)} > #{outfile}"
90
+ stack.shift.call
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Last Proc to be called runs the Pipeline the procedure built.
96
+ # Once complete, the call stack will unwind back through the
97
+ # preceeding Procs in the stack (if any)
98
+ stack << -> { @pipeline.run }
99
+
100
+ stack.shift
101
+ end
102
+
103
+ def tar_success_codes
104
+ gnu_tar? ? [0, 1] : [0]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ class Pipeline
5
+ class Error < Backup::Error; end
6
+
7
+ include Utilities::Helpers
8
+
9
+ attr_reader :stderr, :errors
10
+
11
+ def initialize
12
+ @commands = []
13
+ @success_codes = []
14
+ @errors = []
15
+ @stderr = ""
16
+ end
17
+
18
+ ##
19
+ # Adds a command to be executed in the pipeline.
20
+ # Each command will be run in the order in which it was added,
21
+ # with it's output being piped to the next command.
22
+ #
23
+ # +success_codes+ must be an Array of Integer exit codes that will
24
+ # be considered successful for the +command+.
25
+ def add(command, success_codes)
26
+ @commands << command
27
+ @success_codes << success_codes
28
+ end
29
+
30
+ ##
31
+ # Commands added using this method will only be considered successful
32
+ # if their exit status is 0.
33
+ #
34
+ # Use #add if successful exit status codes need to be specified.
35
+ def <<(command)
36
+ add(command, [0])
37
+ end
38
+
39
+ ##
40
+ # Runs the command line from `#pipeline` and collects STDOUT/STDERR.
41
+ # STDOUT is then parsed to determine the exit status of each command.
42
+ # For each command with a non-zero exit status, a SystemCallError is
43
+ # created and added to @errors. All STDERR output is set in @stderr.
44
+ #
45
+ # Note that there is no accumulated STDOUT from the commands themselves.
46
+ # Also, the last command should not attempt to write to STDOUT.
47
+ # Any output on STDOUT from the final command will be sent to STDERR.
48
+ # This in itself will not cause #run to fail, but will log warnings
49
+ # when all commands exit with non-zero status.
50
+ #
51
+ # Use `#success?` to determine if all commands in the pipeline succeeded. If
52
+ # `#success?` returns `false`, use `#error_messages` to get an error report.
53
+ def run
54
+ Open4.popen4(pipeline) do |_pid, _stdin, stdout, stderr|
55
+ pipestatus = stdout.read.delete("\n").split(":").sort
56
+ pipestatus.each do |status|
57
+ index, exitstatus = status.split("|").map(&:to_i)
58
+ next if @success_codes[index].include?(exitstatus)
59
+
60
+ command = command_name(@commands[index])
61
+ @errors << SystemCallError.new(
62
+ "'#{command}' returned exit code: #{exitstatus}", exitstatus
63
+ )
64
+ end
65
+ @stderr = stderr.read.strip
66
+ end
67
+ Logger.warn(stderr_messages) if success? && stderr_messages
68
+ rescue Exception => err
69
+ raise Error.wrap(err, "Pipeline failed to execute")
70
+ end
71
+
72
+ def success?
73
+ @errors.empty?
74
+ end
75
+
76
+ ##
77
+ # Returns a multi-line String, reporting all STDERR messages received
78
+ # from the commands in the pipeline (if any), along with the SystemCallError
79
+ # (Errno) message for each command which had a non-zero exit status.
80
+ def error_messages
81
+ @error_messages ||= (stderr_messages || "") +
82
+ "The following system errors were returned:\n" +
83
+ @errors.map { |err| "#{err.class}: #{err.message}" }.join("\n")
84
+ end
85
+
86
+ private
87
+
88
+ ##
89
+ # Each command is added as part of the pipeline, grouped with an `echo`
90
+ # command to pass along the command's index in @commands and it's exit
91
+ # status. The command's STDERR is redirected to FD#4, and the `echo` command
92
+ # to report the "index|exit status" is redirected to FD#3. Each command's
93
+ # STDOUT will be connected to the STDIN of the next subshell. The entire
94
+ # pipeline is run within a container group, which redirects FD#3 to STDOUT
95
+ # and FD#4 to STDERR so these can be collected. FD#1 is redirected to STDERR
96
+ # so that any output from the final command on STDOUT will generate
97
+ # warnings, since the final command should not attempt to write to STDOUT,
98
+ # as this would interfere with collecting the exit statuses.
99
+ #
100
+ # There is no guarantee as to the order of this output, which is why the
101
+ # command's index in @commands is passed along with it's exit status. And,
102
+ # if multiple commands output messages on STDERR, those messages may be
103
+ # interleaved. Interleaving of the "index|exit status" outputs should not be
104
+ # an issue, given the small byte size of the data being written.
105
+ def pipeline
106
+ parts = []
107
+ @commands.each_with_index do |command, index|
108
+ parts << %({ #{command} 2>&4 ; echo "#{index}|$?:" >&3 ; })
109
+ end
110
+ %({ #{parts.join(" | ")} } 3>&1 1>&2 4>&2)
111
+ end
112
+
113
+ def stderr_messages
114
+ @stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(%r{^ +}, " ")
115
+ Pipeline STDERR Messages:
116
+ (Note: may be interleaved if multiple commands returned error messages)
117
+
118
+ #{@stderr}
119
+ EOS
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ class Splitter
5
+ include Utilities::Helpers
6
+
7
+ attr_reader :package, :chunk_size, :suffix_length
8
+
9
+ def initialize(model, chunk_size, suffix_length)
10
+ @package = model.package
11
+ @chunk_size = chunk_size
12
+ @suffix_length = suffix_length
13
+ end
14
+
15
+ ##
16
+ # This is called as part of the procedure used to build the final
17
+ # backup package file(s). It yields it's portion of the command line
18
+ # for this procedure, which will split the data being piped into it
19
+ # into multiple files, based on the @chunk_size, using a suffix length as
20
+ # specified by @suffix_length.
21
+ # Once the packaging procedure is complete, it will return and
22
+ # @package.chunk_suffixes will be set based on the resulting files.
23
+ def split_with
24
+ Logger.info "Splitter configured with a chunk size of #{chunk_size}MB " \
25
+ "and suffix length of #{suffix_length}."
26
+ yield split_command
27
+ after_packaging
28
+ end
29
+
30
+ private
31
+
32
+ ##
33
+ # The `split` command reads from $stdin and will store it's output in
34
+ # multiple files, based on @chunk_size and @suffix_length, using the full
35
+ # path to the final @package.basename, plus a '-' separator as the `prefix`.
36
+ def split_command
37
+ "#{utility(:split)} -a #{suffix_length} -b #{chunk_size}m - " \
38
+ "'#{File.join(Config.tmp_path, package.basename + "-")}'"
39
+ end
40
+
41
+ ##
42
+ # Finds the resulting files from the packaging procedure
43
+ # and stores an Array of suffixes used in @package.chunk_suffixes.
44
+ # If the @chunk_size was never reached and only one file
45
+ # was written, that file will be suffixed with '-aa' (or -a; -aaa; etc
46
+ # depending upon suffix_length). In which case, it will simply
47
+ # remove the suffix from the filename.
48
+ def after_packaging
49
+ suffixes = chunk_suffixes
50
+ first_suffix = "a" * suffix_length
51
+ if suffixes == [first_suffix]
52
+ FileUtils.mv(
53
+ File.join(Config.tmp_path, "#{package.basename}-#{first_suffix}"),
54
+ File.join(Config.tmp_path, package.basename)
55
+ )
56
+ else
57
+ package.chunk_suffixes = suffixes
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Returns an array of suffixes for each chunk, in alphabetical order.
63
+ # For example: [aa, ab, ac, ad, ae] or [aaa, aab, aac aad]
64
+ def chunk_suffixes
65
+ chunks.map { |chunk| File.extname(chunk).split("-").last }.sort
66
+ end
67
+
68
+ ##
69
+ # Returns an array of full paths to the backup chunks.
70
+ # Chunks are sorted in alphabetical order.
71
+ def chunks
72
+ Dir[File.join(Config.tmp_path, package.basename + "-*")].sort
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Backup
4
+ module Storage
5
+ class Base
6
+ include Config::Helpers
7
+
8
+ ##
9
+ # Base path on the remote where backup package files will be stored.
10
+ attr_reader :path
11
+
12
+ ##
13
+ # Number of backups to keep or time until which to keep.
14
+ #
15
+ # If an Integer is given it sets the limit to how many backups to keep in
16
+ # the remote location. If exceeded, the oldest will be removed to make
17
+ # room for the newest.
18
+ #
19
+ # If a Time object is given it will remove backups _older_ than the given
20
+ # date.
21
+ #
22
+ # @!attribute [rw] keep
23
+ # @param [Integer|Time]
24
+ # @return [Integer|Time]
25
+ attr_accessor :keep
26
+
27
+ attr_reader :model, :package, :storage_id
28
+
29
+ ##
30
+ # +storage_id+ is a user-defined string used to uniquely identify
31
+ # multiple storages of the same type. If multiple storages of the same
32
+ # type are added to a single backup model, this identifier must be set.
33
+ # This will be appended to the YAML storage file used for cycling backups.
34
+ def initialize(model, storage_id = nil, &block)
35
+ @model = model
36
+ @package = model.package
37
+ @storage_id = storage_id.to_s.gsub(%r{\W}, "_") if storage_id
38
+
39
+ load_defaults!
40
+ instance_eval(&block) if block_given?
41
+ end
42
+
43
+ def path=(value)
44
+ @path = value.dup
45
+ end
46
+
47
+ def perform!
48
+ Logger.info "#{storage_name} Started..."
49
+ transfer!
50
+ if respond_to?(:cycle!, true) && (keep.to_i > 0 || keep.is_a?(Time))
51
+ cycle!
52
+ end
53
+ Logger.info "#{storage_name} Finished!"
54
+ end
55
+
56
+ private
57
+
58
+ ##
59
+ # Return the remote path for the current or given package.
60
+ def remote_path(pkg = package)
61
+ path.empty? ? File.join(pkg.trigger, pkg.time) :
62
+ File.join(path, pkg.trigger, pkg.time)
63
+ end
64
+ alias remote_path_for remote_path
65
+
66
+ def storage_name
67
+ @storage_name ||= self.class.to_s.sub("Backup::", "") +
68
+ (storage_id ? " (#{storage_id})" : "")
69
+ end
70
+ end
71
+ end
72
+ end