backedup 5.0.0.beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +33 -0
  4. data/bin/backedup +5 -0
  5. data/bin/docker_test +24 -0
  6. data/lib/backup/archive.rb +169 -0
  7. data/lib/backup/binder.rb +18 -0
  8. data/lib/backup/cleaner.rb +112 -0
  9. data/lib/backup/cli.rb +370 -0
  10. data/lib/backup/cloud_io/base.rb +38 -0
  11. data/lib/backup/cloud_io/cloud_files.rb +296 -0
  12. data/lib/backup/cloud_io/gcs.rb +121 -0
  13. data/lib/backup/cloud_io/s3.rb +253 -0
  14. data/lib/backup/cloud_io/swift.rb +96 -0
  15. data/lib/backup/compressor/base.rb +32 -0
  16. data/lib/backup/compressor/bzip2.rb +35 -0
  17. data/lib/backup/compressor/custom.rb +49 -0
  18. data/lib/backup/compressor/gzip.rb +73 -0
  19. data/lib/backup/compressor/pbzip2.rb +45 -0
  20. data/lib/backup/config/dsl.rb +102 -0
  21. data/lib/backup/config/helpers.rb +137 -0
  22. data/lib/backup/config.rb +118 -0
  23. data/lib/backup/database/base.rb +86 -0
  24. data/lib/backup/database/mongodb.rb +186 -0
  25. data/lib/backup/database/mysql.rb +191 -0
  26. data/lib/backup/database/openldap.rb +93 -0
  27. data/lib/backup/database/postgresql.rb +164 -0
  28. data/lib/backup/database/redis.rb +176 -0
  29. data/lib/backup/database/riak.rb +79 -0
  30. data/lib/backup/database/sqlite.rb +55 -0
  31. data/lib/backup/encryptor/base.rb +27 -0
  32. data/lib/backup/encryptor/gpg.rb +737 -0
  33. data/lib/backup/encryptor/open_ssl.rb +74 -0
  34. data/lib/backup/errors.rb +53 -0
  35. data/lib/backup/logger/console.rb +48 -0
  36. data/lib/backup/logger/fog_adapter.rb +25 -0
  37. data/lib/backup/logger/logfile.rb +131 -0
  38. data/lib/backup/logger/syslog.rb +114 -0
  39. data/lib/backup/logger.rb +197 -0
  40. data/lib/backup/model.rb +472 -0
  41. data/lib/backup/notifier/base.rb +126 -0
  42. data/lib/backup/notifier/campfire.rb +61 -0
  43. data/lib/backup/notifier/command.rb +99 -0
  44. data/lib/backup/notifier/datadog.rb +104 -0
  45. data/lib/backup/notifier/flowdock.rb +99 -0
  46. data/lib/backup/notifier/hipchat.rb +116 -0
  47. data/lib/backup/notifier/http_post.rb +114 -0
  48. data/lib/backup/notifier/mail.rb +232 -0
  49. data/lib/backup/notifier/nagios.rb +65 -0
  50. data/lib/backup/notifier/pagerduty.rb +79 -0
  51. data/lib/backup/notifier/prowl.rb +68 -0
  52. data/lib/backup/notifier/pushover.rb +71 -0
  53. data/lib/backup/notifier/ses.rb +123 -0
  54. data/lib/backup/notifier/slack.rb +147 -0
  55. data/lib/backup/notifier/twitter.rb +55 -0
  56. data/lib/backup/notifier/zabbix.rb +60 -0
  57. data/lib/backup/package.rb +51 -0
  58. data/lib/backup/packager.rb +106 -0
  59. data/lib/backup/pipeline.rb +120 -0
  60. data/lib/backup/splitter.rb +73 -0
  61. data/lib/backup/storage/base.rb +66 -0
  62. data/lib/backup/storage/cloud_files.rb +156 -0
  63. data/lib/backup/storage/cycler.rb +70 -0
  64. data/lib/backup/storage/dropbox.rb +206 -0
  65. data/lib/backup/storage/ftp.rb +116 -0
  66. data/lib/backup/storage/gcs.rb +93 -0
  67. data/lib/backup/storage/local.rb +61 -0
  68. data/lib/backup/storage/qiniu.rb +65 -0
  69. data/lib/backup/storage/rsync.rb +246 -0
  70. data/lib/backup/storage/s3.rb +155 -0
  71. data/lib/backup/storage/scp.rb +65 -0
  72. data/lib/backup/storage/sftp.rb +80 -0
  73. data/lib/backup/storage/swift.rb +124 -0
  74. data/lib/backup/storage/webdav.rb +102 -0
  75. data/lib/backup/syncer/base.rb +67 -0
  76. data/lib/backup/syncer/cloud/base.rb +176 -0
  77. data/lib/backup/syncer/cloud/cloud_files.rb +81 -0
  78. data/lib/backup/syncer/cloud/local_file.rb +97 -0
  79. data/lib/backup/syncer/cloud/s3.rb +109 -0
  80. data/lib/backup/syncer/rsync/base.rb +50 -0
  81. data/lib/backup/syncer/rsync/local.rb +27 -0
  82. data/lib/backup/syncer/rsync/pull.rb +47 -0
  83. data/lib/backup/syncer/rsync/push.rb +201 -0
  84. data/lib/backup/template.rb +41 -0
  85. data/lib/backup/utilities.rb +234 -0
  86. data/lib/backup/version.rb +3 -0
  87. data/lib/backup.rb +145 -0
  88. data/templates/cli/archive +28 -0
  89. data/templates/cli/compressor/bzip2 +4 -0
  90. data/templates/cli/compressor/custom +7 -0
  91. data/templates/cli/compressor/gzip +4 -0
  92. data/templates/cli/config +123 -0
  93. data/templates/cli/databases/mongodb +15 -0
  94. data/templates/cli/databases/mysql +18 -0
  95. data/templates/cli/databases/openldap +24 -0
  96. data/templates/cli/databases/postgresql +16 -0
  97. data/templates/cli/databases/redis +16 -0
  98. data/templates/cli/databases/riak +17 -0
  99. data/templates/cli/databases/sqlite +11 -0
  100. data/templates/cli/encryptor/gpg +27 -0
  101. data/templates/cli/encryptor/openssl +9 -0
  102. data/templates/cli/model +26 -0
  103. data/templates/cli/notifier/zabbix +15 -0
  104. data/templates/cli/notifiers/campfire +12 -0
  105. data/templates/cli/notifiers/command +32 -0
  106. data/templates/cli/notifiers/datadog +57 -0
  107. data/templates/cli/notifiers/flowdock +16 -0
  108. data/templates/cli/notifiers/hipchat +16 -0
  109. data/templates/cli/notifiers/http_post +32 -0
  110. data/templates/cli/notifiers/mail +24 -0
  111. data/templates/cli/notifiers/nagios +13 -0
  112. data/templates/cli/notifiers/pagerduty +12 -0
  113. data/templates/cli/notifiers/prowl +11 -0
  114. data/templates/cli/notifiers/pushover +11 -0
  115. data/templates/cli/notifiers/ses +15 -0
  116. data/templates/cli/notifiers/slack +22 -0
  117. data/templates/cli/notifiers/twitter +13 -0
  118. data/templates/cli/splitter +7 -0
  119. data/templates/cli/storages/cloud_files +11 -0
  120. data/templates/cli/storages/dropbox +20 -0
  121. data/templates/cli/storages/ftp +13 -0
  122. data/templates/cli/storages/gcs +8 -0
  123. data/templates/cli/storages/local +8 -0
  124. data/templates/cli/storages/qiniu +12 -0
  125. data/templates/cli/storages/rsync +17 -0
  126. data/templates/cli/storages/s3 +16 -0
  127. data/templates/cli/storages/scp +15 -0
  128. data/templates/cli/storages/sftp +15 -0
  129. data/templates/cli/storages/swift +19 -0
  130. data/templates/cli/storages/webdav +13 -0
  131. data/templates/cli/syncers/cloud_files +22 -0
  132. data/templates/cli/syncers/rsync_local +20 -0
  133. data/templates/cli/syncers/rsync_pull +28 -0
  134. data/templates/cli/syncers/rsync_push +28 -0
  135. data/templates/cli/syncers/s3 +27 -0
  136. data/templates/general/links +3 -0
  137. data/templates/general/version.erb +2 -0
  138. data/templates/notifier/mail/failure.erb +16 -0
  139. data/templates/notifier/mail/success.erb +16 -0
  140. data/templates/notifier/mail/warning.erb +16 -0
  141. data/templates/storage/dropbox/authorization_url.erb +6 -0
  142. data/templates/storage/dropbox/authorized.erb +4 -0
  143. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  144. metadata +1255 -0
@@ -0,0 +1,65 @@
1
+ require "qiniu"
2
+
3
+ module Backup
4
+ module Storage
5
+ class Qiniu < Base
6
+ include Storage::Cycler
7
+ class Error < Backup::Error; end
8
+
9
+ ##
10
+ # Qiniu API credentials
11
+ attr_accessor :access_key, :secret_key
12
+
13
+ ##
14
+ # Qiniu bucket name
15
+ attr_accessor :bucket
16
+
17
+ def initialize(model, storage_id = nil)
18
+ super
19
+
20
+ @path ||= "backups"
21
+
22
+ check_configuration
23
+ config_credentials
24
+ end
25
+
26
+ private
27
+
28
+ def transfer!
29
+ package.filenames.each do |filename|
30
+ src = File.join(Config.tmp_path, filename)
31
+ dest = File.join(remote_path, filename)
32
+ Logger.info "Storing '#{dest}'..."
33
+
34
+ ::Qiniu.upload_file(uptoken: ::Qiniu.generate_upload_token,
35
+ bucket: bucket,
36
+ file: src,
37
+ key: dest)
38
+ end
39
+ end
40
+
41
+ # Called by the Cycler.
42
+ # Any error raised will be logged as a warning.
43
+ def remove!(package)
44
+ Logger.info "Removing backup package dated #{package.time}..."
45
+ remote_path = remote_path_for(package)
46
+ package.filenames.each do |filename|
47
+ ::Qiniu.delete(bucket, File.join(remote_path, filename))
48
+ end
49
+ end
50
+
51
+ def check_configuration
52
+ required = %w[access_key secret_key bucket]
53
+
54
+ raise Error, <<-EOS if required.map { |name| send(name) }.any?(&:nil?)
55
+ Configuration Error
56
+ #{required.map { |name| "##{name}" }.join(", ")} are all required
57
+ EOS
58
+ end
59
+
60
+ def config_credentials
61
+ ::Qiniu.establish_connection!(access_key: access_key, secret_key: secret_key)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,246 @@
1
+ module Backup
2
+ module Storage
3
+ class RSync < Base
4
+ include Utilities::Helpers
5
+
6
+ ##
7
+ # Mode of operation
8
+ #
9
+ # [:ssh (default)]
10
+ # Connects to the remote via SSH.
11
+ # Does not use an rsync daemon on the remote.
12
+ #
13
+ # [:ssh_daemon]
14
+ # Connects to the remote via SSH.
15
+ # Spawns a single-use daemon on the remote, which allows certain
16
+ # daemon features (like modules) to be used.
17
+ #
18
+ # [:rsync_daemon]
19
+ # Connects directly to an rsync daemon via TCP.
20
+ # Data transferred is not encrypted.
21
+ #
22
+ attr_accessor :mode
23
+
24
+ ##
25
+ # Server Address
26
+ #
27
+ # If not specified, the storage operation will be local.
28
+ attr_accessor :host
29
+
30
+ ##
31
+ # SSH or RSync port
32
+ #
33
+ # For `:ssh` or `:ssh_daemon` mode, this specifies the SSH port to use
34
+ # and defaults to 22.
35
+ #
36
+ # For `:rsync_daemon` mode, this specifies the TCP port to use
37
+ # and defaults to 873.
38
+ attr_accessor :port
39
+
40
+ ##
41
+ # SSH User
42
+ #
43
+ # If the user running the backup is not the same user that needs to
44
+ # authenticate with the remote server, specify the user here.
45
+ #
46
+ # The user must have SSH keys setup for passphrase-less access to the
47
+ # remote. If the SSH User does not have passphrase-less keys, or no
48
+ # default keys in their `~/.ssh` directory, you will need to use the
49
+ # `-i` option in `:additional_ssh_options` to specify the
50
+ # passphrase-less key to use.
51
+ #
52
+ # Used only for `:ssh` and `:ssh_daemon` modes.
53
+ attr_accessor :ssh_user
54
+
55
+ ##
56
+ # Additional SSH Options
57
+ #
58
+ # Used to supply a String or Array of options to be passed to the SSH
59
+ # command in `:ssh` and `:ssh_daemon` modes.
60
+ #
61
+ # For example, if you need to supply a specific SSH key for the `ssh_user`,
62
+ # you would set this to: "-i '/path/to/id_rsa'". Which would produce:
63
+ #
64
+ # rsync -e "ssh -p 22 -i '/path/to/id_rsa'"
65
+ #
66
+ # Arguments may be single-quoted, but should not contain any double-quotes.
67
+ #
68
+ # Used only for `:ssh` and `:ssh_daemon` modes.
69
+ attr_accessor :additional_ssh_options
70
+
71
+ ##
72
+ # RSync User
73
+ #
74
+ # If the user running the backup is not the same user that needs to
75
+ # authenticate with the rsync daemon, specify the user here.
76
+ #
77
+ # Used only for `:ssh_daemon` and `:rsync_daemon` modes.
78
+ attr_accessor :rsync_user
79
+
80
+ ##
81
+ # RSync Password
82
+ #
83
+ # If specified, Backup will write the password to a temporary file and
84
+ # use it with rsync's `--password-file` option for daemon authentication.
85
+ #
86
+ # Note that setting this will override `rsync_password_file`.
87
+ #
88
+ # Used only for `:ssh_daemon` and `:rsync_daemon` modes.
89
+ attr_accessor :rsync_password
90
+
91
+ ##
92
+ # RSync Password File
93
+ #
94
+ # If specified, this path will be passed to rsync's `--password-file`
95
+ # option for daemon authentication.
96
+ #
97
+ # Used only for `:ssh_daemon` and `:rsync_daemon` modes.
98
+ attr_accessor :rsync_password_file
99
+
100
+ ##
101
+ # Additional String or Array of options for the rsync cli
102
+ attr_accessor :additional_rsync_options
103
+
104
+ ##
105
+ # Flag for compressing (only compresses for the transfer)
106
+ attr_accessor :compress
107
+
108
+ ##
109
+ # Path to store the synced backup package file(s) to.
110
+ #
111
+ # If no +host+ is specified, then +path+ will be local, and the only
112
+ # other used option would be +additional_rsync_options+.
113
+ # +path+ will be expanded, so '~/my_path' will expand to '$HOME/my_path'.
114
+ #
115
+ # If a +host+ is specified, this will be a path on the host.
116
+ # If +mode+ is `:ssh` (default), then any relative path, or path starting
117
+ # with '~/' will be relative to the directory the ssh_user is logged
118
+ # into. For `:ssh_daemon` or `:rsync_daemon` modes, this would reference
119
+ # an rsync module/path.
120
+ #
121
+ # In :ssh_daemon and :rsync_daemon modes, +path+ (or path defined by
122
+ # your rsync module) must already exist.
123
+ #
124
+ # In :ssh mode or local operation (no +host+ specified), +path+ will
125
+ # be created if needed - either locally, or on the remote for :ssh mode.
126
+ attr_accessor :path
127
+
128
+ def initialize(model, storage_id = nil)
129
+ super
130
+
131
+ @mode ||= :ssh
132
+ @port ||= mode == :rsync_daemon ? 873 : 22
133
+ @compress ||= false
134
+ @path ||= "~/backups"
135
+ end
136
+
137
+ private
138
+
139
+ def transfer!
140
+ write_password_file
141
+ create_remote_path
142
+
143
+ package.filenames.each do |filename|
144
+ src = "'#{File.join(Config.tmp_path, filename)}'"
145
+ dest = "#{host_options}'#{File.join(remote_path, filename)}'"
146
+ Logger.info "Syncing to #{dest}..."
147
+ run("#{rsync_command} #{src} #{dest}")
148
+ end
149
+ ensure
150
+ remove_password_file
151
+ end
152
+
153
+ ##
154
+ # Other storages add an additional timestamp directory to this path.
155
+ # This is not desired here, since we need to transfer the package files
156
+ # to the same location each time.
157
+ def remote_path
158
+ @remote_path ||= begin
159
+ if host
160
+ path.sub(/^~\//, "").sub(/\/$/, "")
161
+ else
162
+ File.expand_path(path)
163
+ end
164
+ end
165
+ end
166
+
167
+ ##
168
+ # Runs a 'mkdir -p' command on the host (or locally) to ensure the
169
+ # dest_path exists. This is used because we're transferring a single
170
+ # file, and rsync won't attempt to create the intermediate directories.
171
+ #
172
+ # This is only applicable locally and in :ssh mode.
173
+ # In :ssh_daemon and :rsync_daemon modes the `path` would include a
174
+ # module name that must define a path on the remote that already exists.
175
+ def create_remote_path
176
+ if host
177
+ return unless mode == :ssh
178
+ run "#{utility(:ssh)} #{ssh_transport_args} #{host} " +
179
+ %("mkdir -p '#{remote_path}'")
180
+ else
181
+ FileUtils.mkdir_p(remote_path)
182
+ end
183
+ end
184
+
185
+ def host_options
186
+ @host_options ||= begin
187
+ if !host
188
+ ""
189
+ elsif mode == :ssh
190
+ "#{host}:"
191
+ else
192
+ user = "#{rsync_user}@" if rsync_user
193
+ "#{user}#{host}::"
194
+ end
195
+ end
196
+ end
197
+
198
+ def rsync_command
199
+ @rsync_command ||= begin
200
+ cmd = utility(:rsync) << " --archive" <<
201
+ " #{Array(additional_rsync_options).join(" ")}".rstrip
202
+ cmd << compress_option << password_option << transport_options if host
203
+ cmd
204
+ end
205
+ end
206
+
207
+ def compress_option
208
+ compress ? " --compress" : ""
209
+ end
210
+
211
+ def password_option
212
+ return "" if mode == :ssh
213
+
214
+ path = @password_file ? @password_file.path : rsync_password_file
215
+ path ? " --password-file='#{File.expand_path(path)}'" : ""
216
+ end
217
+
218
+ def transport_options
219
+ if mode == :rsync_daemon
220
+ " --port #{port}"
221
+ else
222
+ %( -e "#{utility(:ssh)} #{ssh_transport_args}")
223
+ end
224
+ end
225
+
226
+ def ssh_transport_args
227
+ args = "-p #{port} "
228
+ args << "-l #{ssh_user} " if ssh_user
229
+ args << Array(additional_ssh_options).join(" ")
230
+ args.rstrip
231
+ end
232
+
233
+ def write_password_file
234
+ return unless host && rsync_password && mode != :ssh
235
+
236
+ @password_file = Tempfile.new("backup-rsync-password")
237
+ @password_file.write(rsync_password)
238
+ @password_file.close
239
+ end
240
+
241
+ def remove_password_file
242
+ @password_file.delete if @password_file
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,155 @@
1
+ require "backup/cloud_io/s3"
2
+
3
+ module Backup
4
+ module Storage
5
+ class S3 < Base
6
+ include Storage::Cycler
7
+ class Error < Backup::Error; end
8
+
9
+ ##
10
+ # Amazon Simple Storage Service (S3) Credentials
11
+ attr_accessor :access_key_id, :secret_access_key, :use_iam_profile
12
+
13
+ ##
14
+ # Amazon S3 bucket name
15
+ attr_accessor :bucket
16
+
17
+ ##
18
+ # Region of the specified S3 bucket
19
+ attr_accessor :region
20
+
21
+ ##
22
+ # Multipart chunk size, specified in MiB.
23
+ #
24
+ # Each package file larger than +chunk_size+
25
+ # will be uploaded using S3 Multipart Upload.
26
+ #
27
+ # Minimum: 5 (but may be disabled with 0)
28
+ # Maximum: 5120
29
+ # Default: 5
30
+ attr_accessor :chunk_size
31
+
32
+ ##
33
+ # Number of times to retry failed operations.
34
+ #
35
+ # Default: 10
36
+ attr_accessor :max_retries
37
+
38
+ ##
39
+ # Time in seconds to pause before each retry.
40
+ #
41
+ # Default: 30
42
+ attr_accessor :retry_waitsec
43
+
44
+ ##
45
+ # Encryption algorithm to use for Amazon Server-Side Encryption
46
+ #
47
+ # Supported values:
48
+ #
49
+ # - :aes256
50
+ #
51
+ # Default: nil
52
+ attr_accessor :encryption
53
+
54
+ ##
55
+ # Storage class to use for the S3 objects uploaded
56
+ #
57
+ # Supported values:
58
+ #
59
+ # - :standard (default)
60
+ # - :standard_ia
61
+ # - :reduced_redundancy
62
+ #
63
+ # Default: :standard
64
+ attr_accessor :storage_class
65
+
66
+ ##
67
+ # Additional options to pass along to fog.
68
+ # e.g. Fog::Storage.new({ :provider => 'AWS' }.merge(fog_options))
69
+ attr_accessor :fog_options
70
+
71
+ def initialize(model, storage_id = nil)
72
+ super
73
+
74
+ @chunk_size ||= 5 # MiB
75
+ @max_retries ||= 10
76
+ @retry_waitsec ||= 30
77
+ @path ||= "backups"
78
+ @storage_class ||= :standard
79
+
80
+ @path = @path.sub(/^\//, "")
81
+
82
+ check_configuration
83
+ end
84
+
85
+ private
86
+
87
+ def cloud_io
88
+ @cloud_io ||= CloudIO::S3.new(
89
+ access_key_id: access_key_id,
90
+ secret_access_key: secret_access_key,
91
+ use_iam_profile: use_iam_profile,
92
+ region: region,
93
+ bucket: bucket,
94
+ encryption: encryption,
95
+ storage_class: storage_class,
96
+ max_retries: max_retries,
97
+ retry_waitsec: retry_waitsec,
98
+ chunk_size: chunk_size,
99
+ fog_options: fog_options
100
+ )
101
+ end
102
+
103
+ def transfer!
104
+ package.filenames.each do |filename|
105
+ src = File.join(Config.tmp_path, filename)
106
+ dest = File.join(remote_path, filename)
107
+ Logger.info "Storing '#{bucket}/#{dest}'..."
108
+ cloud_io.upload(src, dest)
109
+ end
110
+ end
111
+
112
+ # Called by the Cycler.
113
+ # Any error raised will be logged as a warning.
114
+ def remove!(package)
115
+ Logger.info "Removing backup package dated #{package.time}..."
116
+
117
+ remote_path = remote_path_for(package)
118
+ objects = cloud_io.objects(remote_path)
119
+
120
+ raise Error, "Package at '#{remote_path}' not found" if objects.empty?
121
+
122
+ cloud_io.delete(objects)
123
+ end
124
+
125
+ def check_configuration
126
+ required =
127
+ if use_iam_profile
128
+ %w[bucket]
129
+ else
130
+ %w[access_key_id secret_access_key bucket]
131
+ end
132
+ raise Error, <<-EOS if required.map { |name| send(name) }.any?(&:nil?)
133
+ Configuration Error
134
+ #{required.map { |name| "##{name}" }.join(", ")} are all required
135
+ EOS
136
+
137
+ raise Error, <<-EOS if chunk_size > 0 && !chunk_size.between?(5, 5120)
138
+ Configuration Error
139
+ #chunk_size must be between 5 and 5120 (or 0 to disable multipart)
140
+ EOS
141
+
142
+ raise Error, <<-EOS if encryption && encryption.to_s.upcase != "AES256"
143
+ Configuration Error
144
+ #encryption must be :aes256 or nil
145
+ EOS
146
+
147
+ classes = ["STANDARD", "STANDARD_IA", "REDUCED_REDUNDANCY"]
148
+ raise Error, <<-EOS unless classes.include?(storage_class.to_s.upcase)
149
+ Configuration Error
150
+ #storage_class must be :standard or :standard_ia or :reduced_redundancy
151
+ EOS
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,65 @@
1
+ require "net/scp"
2
+
3
+ module Backup
4
+ module Storage
5
+ class SCP < Base
6
+ include Storage::Cycler
7
+ class Error < Backup::Error; end
8
+
9
+ ##
10
+ # Server credentials
11
+ attr_accessor :username, :password, :ssh_options
12
+
13
+ ##
14
+ # Server IP Address and SCP port
15
+ attr_accessor :ip, :port
16
+
17
+ def initialize(model, storage_id = nil)
18
+ super
19
+
20
+ @port ||= 22
21
+ @path ||= "backups"
22
+ @ssh_options ||= {}
23
+ path.sub!(/^~\//, "")
24
+ end
25
+
26
+ private
27
+
28
+ def connection
29
+ Net::SSH.start(
30
+ ip, username, { password: password, port: port }.merge(ssh_options)
31
+ ) { |ssh| yield ssh }
32
+ end
33
+
34
+ def transfer!
35
+ connection do |ssh|
36
+ ssh.exec!("mkdir -p '#{remote_path}'")
37
+
38
+ package.filenames.each do |filename|
39
+ src = File.join(Config.tmp_path, filename)
40
+ dest = File.join(remote_path, filename)
41
+ Logger.info "Storing '#{ip}:#{dest}'..."
42
+ ssh.scp.upload!(src, dest)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Called by the Cycler.
48
+ # Any error raised will be logged as a warning.
49
+ def remove!(package)
50
+ Logger.info "Removing backup package dated #{package.time}..."
51
+
52
+ errors = []
53
+ connection do |ssh|
54
+ ssh.exec!("rm -r '#{remote_path_for(package)}'") do |_, stream, data|
55
+ errors << data if stream == :stderr
56
+ end
57
+ end
58
+ unless errors.empty?
59
+ raise Error, "Net::SSH reported the following errors:\n" +
60
+ errors.join("\n")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ require "net/sftp"
2
+
3
+ module Backup
4
+ module Storage
5
+ class SFTP < Base
6
+ include Storage::Cycler
7
+
8
+ ##
9
+ # Server credentials
10
+ attr_accessor :username, :password, :ssh_options
11
+
12
+ ##
13
+ # Server IP Address and SFTP port
14
+ attr_accessor :ip, :port
15
+
16
+ def initialize(model, storage_id = nil)
17
+ super
18
+
19
+ @ssh_options ||= {}
20
+ @port ||= 22
21
+ @path ||= "backups"
22
+ path.sub!(/^~\//, "")
23
+ end
24
+
25
+ private
26
+
27
+ def connection
28
+ Net::SFTP.start(
29
+ ip, username, { password: password, port: port }.merge(ssh_options)
30
+ ) { |sftp| yield sftp }
31
+ end
32
+
33
+ def transfer!
34
+ connection do |sftp|
35
+ create_remote_path(sftp)
36
+
37
+ package.filenames.each do |filename|
38
+ src = File.join(Config.tmp_path, filename)
39
+ dest = File.join(remote_path, filename)
40
+ Logger.info "Storing '#{ip}:#{dest}'..."
41
+ sftp.upload!(src, dest)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Called by the Cycler.
47
+ # Any error raised will be logged as a warning.
48
+ def remove!(package)
49
+ Logger.info "Removing backup package dated #{package.time}..."
50
+
51
+ remote_path = remote_path_for(package)
52
+ connection do |sftp|
53
+ package.filenames.each do |filename|
54
+ sftp.remove!(File.join(remote_path, filename))
55
+ end
56
+
57
+ sftp.rmdir!(remote_path)
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Creates (if they don't exist yet) all the directories on the remote
63
+ # server in order to upload the backup file. Net::SFTP does not support
64
+ # paths to directories that don't yet exist when creating new
65
+ # directories. Instead, we split the parts up in to an array (for each
66
+ # '/') and loop through that to create the directories one by one.
67
+ # Net::SFTP raises an exception when the directory it's trying to create
68
+ # already exists, so we have rescue it
69
+ def create_remote_path(sftp)
70
+ path_parts = []
71
+ remote_path.split("/").each do |path_part|
72
+ path_parts << path_part
73
+ begin
74
+ sftp.mkdir!(path_parts.join("/"))
75
+ rescue Net::SFTP::StatusException; end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end