backup 3.3.2 → 3.4.0
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.
- checksums.yaml +4 -4
- data/lib/backup/cleaner.rb +22 -29
- data/lib/backup/database/base.rb +5 -1
- data/lib/backup/model.rb +4 -4
- data/lib/backup/package.rb +3 -4
- data/lib/backup/splitter.rb +12 -14
- data/lib/backup/storage/base.rb +25 -53
- data/lib/backup/storage/cloudfiles.rb +18 -38
- data/lib/backup/storage/cycler.rb +6 -6
- data/lib/backup/storage/dropbox.rb +41 -55
- data/lib/backup/storage/ftp.rb +17 -37
- data/lib/backup/storage/local.rb +23 -42
- data/lib/backup/storage/ninefold.rb +28 -69
- data/lib/backup/storage/rsync.rb +18 -24
- data/lib/backup/storage/s3.rb +166 -48
- data/lib/backup/storage/scp.rb +13 -41
- data/lib/backup/storage/sftp.rb +16 -37
- data/lib/backup/version.rb +1 -1
- data/templates/general/links +3 -11
- metadata +3 -3
data/lib/backup/storage/rsync.rb
CHANGED
@@ -145,46 +145,40 @@ module Backup
|
|
145
145
|
private
|
146
146
|
|
147
147
|
def transfer!
|
148
|
-
|
148
|
+
write_password_file
|
149
|
+
create_remote_path
|
149
150
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
files_to_transfer_for(@package) do |local_file, remote_file|
|
154
|
-
src = "'#{ File.join(local_path, local_file) }'"
|
155
|
-
dest = "#{ host_options }'#{ File.join(dest_path, remote_file) }'"
|
151
|
+
package.filenames.each do |filename|
|
152
|
+
src = "'#{ File.join(Config.tmp_path, filename) }'"
|
153
|
+
dest = "#{ host_options }'#{ File.join(remote_path, filename) }'"
|
156
154
|
Logger.info "Syncing to #{ dest }..."
|
157
155
|
run("#{ rsync_command } #{ src } #{ dest }")
|
158
156
|
end
|
159
|
-
|
160
|
-
Logger.info "#{ storage_name } Finished!"
|
161
157
|
ensure
|
162
|
-
remove_password_file
|
158
|
+
remove_password_file
|
163
159
|
end
|
164
160
|
|
165
|
-
##
|
166
161
|
# Storage::RSync doesn't cycle
|
167
162
|
def cycle!; end
|
168
163
|
|
169
164
|
##
|
170
|
-
# Other storages
|
171
|
-
# which adds an additional timestamp directory to the path.
|
165
|
+
# Other storages add an additional timestamp directory to this path.
|
172
166
|
# This is not desired here, since we need to transfer the package files
|
173
167
|
# to the same location each time.
|
174
168
|
#
|
175
169
|
# Note: In v4.0, the additional trigger directory will to be dropped
|
176
|
-
# from
|
177
|
-
# be stored directly in #path.
|
178
|
-
def
|
179
|
-
@
|
170
|
+
# from remote_path for both local and :ssh mode, so the package files
|
171
|
+
# will be stored directly in #path.
|
172
|
+
def remote_path
|
173
|
+
@remote_path ||= begin
|
180
174
|
if host
|
181
175
|
if mode == :ssh
|
182
|
-
File.join(path.sub(/^~\//, ''),
|
176
|
+
File.join(path.sub(/^~\//, ''), package.trigger)
|
183
177
|
else
|
184
178
|
path.sub(/^~\//, '').sub(/\/$/, '')
|
185
179
|
end
|
186
180
|
else
|
187
|
-
File.join(File.expand_path(path),
|
181
|
+
File.join(File.expand_path(path), package.trigger)
|
188
182
|
end
|
189
183
|
end
|
190
184
|
end
|
@@ -197,12 +191,12 @@ module Backup
|
|
197
191
|
# This is only applicable locally and in :ssh mode.
|
198
192
|
# In :ssh_daemon and :rsync_daemon modes the `path` would include a
|
199
193
|
# module name that must define a path on the remote that already exists.
|
200
|
-
def
|
194
|
+
def create_remote_path
|
201
195
|
if host
|
202
196
|
run("#{ utility(:ssh) } #{ ssh_transport_args } #{ host } " +
|
203
|
-
%Q["mkdir -p '#{
|
197
|
+
%Q["mkdir -p '#{ remote_path }'"]) if mode == :ssh
|
204
198
|
else
|
205
|
-
FileUtils.mkdir_p
|
199
|
+
FileUtils.mkdir_p(remote_path)
|
206
200
|
end
|
207
201
|
end
|
208
202
|
|
@@ -254,7 +248,7 @@ module Backup
|
|
254
248
|
args.rstrip
|
255
249
|
end
|
256
250
|
|
257
|
-
def write_password_file
|
251
|
+
def write_password_file
|
258
252
|
return unless host && rsync_password && mode != :ssh
|
259
253
|
|
260
254
|
@password_file = Tempfile.new('backup-rsync-password')
|
@@ -262,7 +256,7 @@ module Backup
|
|
262
256
|
@password_file.close
|
263
257
|
end
|
264
258
|
|
265
|
-
def remove_password_file
|
259
|
+
def remove_password_file
|
266
260
|
@password_file.delete if @password_file
|
267
261
|
end
|
268
262
|
|
data/lib/backup/storage/s3.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
##
|
4
|
-
# Only load the Fog gem when the Backup::Storage::S3 class is loaded
|
5
3
|
Backup::Dependency.load('fog')
|
4
|
+
require 'base64'
|
5
|
+
require 'digest/md5'
|
6
6
|
|
7
7
|
module Backup
|
8
8
|
module Storage
|
@@ -13,81 +13,199 @@ module Backup
|
|
13
13
|
attr_accessor :access_key_id, :secret_access_key
|
14
14
|
|
15
15
|
##
|
16
|
-
# Amazon S3 bucket name
|
17
|
-
attr_accessor :bucket
|
16
|
+
# Amazon S3 bucket name
|
17
|
+
attr_accessor :bucket
|
18
18
|
|
19
19
|
##
|
20
20
|
# Region of the specified S3 bucket
|
21
21
|
attr_accessor :region
|
22
22
|
|
23
23
|
##
|
24
|
-
#
|
25
|
-
|
26
|
-
|
24
|
+
# Chunk size, specified in MiB, for S3 Multipart Upload.
|
25
|
+
#
|
26
|
+
# Each backup package file that is greater than +chunk_size+
|
27
|
+
# will be uploaded using AWS' Multipart Upload.
|
28
|
+
#
|
29
|
+
# Package files less than or equal to +chunk_size+ will be
|
30
|
+
# uploaded via a single PUT request.
|
31
|
+
#
|
32
|
+
# Minimum allowed: 5 (but may be disabled with 0)
|
33
|
+
# Default: 5
|
34
|
+
attr_accessor :chunk_size
|
27
35
|
|
28
|
-
|
36
|
+
##
|
37
|
+
# Number of times to retry failed operations.
|
38
|
+
#
|
39
|
+
# The retry count is reset when the failing operation succeeds,
|
40
|
+
# so each operation that fails will be retried this number of times.
|
41
|
+
# Once a single failed operation exceeds +max_retries+, the entire
|
42
|
+
# storage operation will fail.
|
43
|
+
#
|
44
|
+
# Operations that may fail and be retried include:
|
45
|
+
# - Multipart initiation requests.
|
46
|
+
# - Each multipart upload of +chunk_size+. (retries the chunk)
|
47
|
+
# - Multipart upload completion requests.
|
48
|
+
# - Each file uploaded not using multipart upload. (retries the file)
|
49
|
+
#
|
50
|
+
# Default: 10
|
51
|
+
attr_accessor :max_retries
|
29
52
|
|
53
|
+
##
|
54
|
+
# Time in seconds to pause before each retry.
|
55
|
+
#
|
56
|
+
# Default: 30
|
57
|
+
attr_accessor :retry_waitsec
|
58
|
+
|
59
|
+
def initialize(model, storage_id = nil, &block)
|
60
|
+
super
|
30
61
|
instance_eval(&block) if block_given?
|
62
|
+
|
63
|
+
@chunk_size ||= 5 # MiB
|
64
|
+
@max_retries ||= 10
|
65
|
+
@retry_waitsec ||= 30
|
66
|
+
@path ||= 'backups'
|
67
|
+
path.sub!(/^\//, '')
|
31
68
|
end
|
32
69
|
|
33
70
|
private
|
34
71
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
72
|
+
def connection
|
73
|
+
@connection ||= begin
|
74
|
+
conn = Fog::Storage.new(
|
75
|
+
:provider => 'AWS',
|
76
|
+
:aws_access_key_id => access_key_id,
|
77
|
+
:aws_secret_access_key => secret_access_key,
|
78
|
+
:region => region
|
79
|
+
)
|
80
|
+
conn.sync_clock
|
81
|
+
conn
|
82
|
+
end
|
39
83
|
end
|
40
84
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
)
|
85
|
+
def transfer!
|
86
|
+
package.filenames.each do |filename|
|
87
|
+
src = File.join(Config.tmp_path, filename)
|
88
|
+
dest = File.join(remote_path, filename)
|
89
|
+
Logger.info "Storing '#{ bucket }/#{ dest }'..."
|
90
|
+
Uploader.new(connection, bucket, src, dest,
|
91
|
+
chunk_size, max_retries, retry_waitsec).run
|
92
|
+
end
|
50
93
|
end
|
51
94
|
|
52
|
-
|
53
|
-
|
95
|
+
# Called by the Cycler.
|
96
|
+
# Any error raised will be logged as a warning.
|
97
|
+
def remove!(package)
|
98
|
+
Logger.info "Removing backup package dated #{ package.time }..."
|
99
|
+
|
100
|
+
remote_path = remote_path_for(package)
|
101
|
+
resp = connection.get_bucket(bucket, :prefix => remote_path)
|
102
|
+
keys = resp.body['Contents'].map {|entry| entry['Key'] }
|
103
|
+
|
104
|
+
raise Errors::Storage::S3::NotFoundError,
|
105
|
+
"Package at '#{ remote_path }' not found" if keys.empty?
|
106
|
+
|
107
|
+
connection.delete_multiple_objects(bucket, keys)
|
54
108
|
end
|
55
109
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
110
|
+
class Uploader
|
111
|
+
attr_reader :connection, :bucket, :src, :dest
|
112
|
+
attr_reader :chunk_size, :max_retries, :retry_waitsec
|
113
|
+
attr_reader :upload_id, :parts
|
114
|
+
|
115
|
+
def initialize(connection, bucket, src, dest,
|
116
|
+
chunk_size, max_retries, retry_waitsec)
|
117
|
+
@connection = connection
|
118
|
+
@bucket = bucket
|
119
|
+
@src = src
|
120
|
+
@dest = dest
|
121
|
+
@chunk_size = 1024**2 * chunk_size
|
122
|
+
@max_retries = max_retries
|
123
|
+
@retry_waitsec = retry_waitsec
|
124
|
+
@parts = []
|
125
|
+
end
|
60
126
|
|
61
|
-
|
127
|
+
def run
|
128
|
+
if chunk_size > 0 && File.size(src) > chunk_size
|
129
|
+
initiate_multipart
|
130
|
+
upload_parts
|
131
|
+
complete_multipart
|
132
|
+
else
|
133
|
+
upload
|
134
|
+
end
|
135
|
+
rescue => err
|
136
|
+
raise error_with(err, 'Upload Failed!')
|
137
|
+
end
|
62
138
|
|
63
|
-
|
64
|
-
Logger.info "#{storage_name} started transferring " +
|
65
|
-
"'#{ local_file }' to bucket '#{ bucket }'."
|
139
|
+
private
|
66
140
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
)
|
141
|
+
def upload
|
142
|
+
md5 = Base64.encode64(Digest::MD5.file(src).digest).chomp
|
143
|
+
with_retries do
|
144
|
+
File.open(src, 'r') do |file|
|
145
|
+
connection.put_object(bucket, dest, file, { 'Content-MD5' => md5 })
|
146
|
+
end
|
71
147
|
end
|
72
148
|
end
|
73
|
-
end
|
74
149
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
150
|
+
def initiate_multipart
|
151
|
+
with_retries do
|
152
|
+
resp = connection.initiate_multipart_upload(bucket, dest)
|
153
|
+
@upload_id = resp.body['UploadId']
|
154
|
+
end
|
155
|
+
end
|
81
156
|
|
82
|
-
|
157
|
+
def upload_parts
|
158
|
+
File.open(src, 'r') do |file|
|
159
|
+
part_number = 0
|
160
|
+
while data = file.read(chunk_size)
|
161
|
+
part_number += 1
|
162
|
+
md5 = Base64.encode64(Digest::MD5.digest(data)).chomp
|
163
|
+
with_retries do
|
164
|
+
resp = connection.upload_part(
|
165
|
+
bucket, dest, upload_id, part_number, data,
|
166
|
+
{ 'Content-MD5' => md5 }
|
167
|
+
)
|
168
|
+
parts << resp.headers['ETag']
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
83
173
|
|
84
|
-
|
85
|
-
|
86
|
-
|
174
|
+
def complete_multipart
|
175
|
+
with_retries do
|
176
|
+
connection.complete_multipart_upload(bucket, dest, upload_id, parts)
|
177
|
+
end
|
178
|
+
end
|
87
179
|
|
88
|
-
|
180
|
+
def with_retries
|
181
|
+
retries = 0
|
182
|
+
begin
|
183
|
+
yield
|
184
|
+
rescue => err
|
185
|
+
retries += 1
|
186
|
+
raise if retries > max_retries
|
187
|
+
|
188
|
+
Logger.info error_with(err, "Retry ##{ retries } of #{ max_retries }.")
|
189
|
+
sleep(retry_waitsec)
|
190
|
+
retry
|
191
|
+
end
|
89
192
|
end
|
90
|
-
|
193
|
+
|
194
|
+
# Avoid wrapping Excon::Errors::HTTPStatusError since it's message
|
195
|
+
# includes `request.inspect`. For multipart uploads, this includes
|
196
|
+
# the String#inspect output of `file.read(chunk_size)`.
|
197
|
+
def error_with(err, msg)
|
198
|
+
if err.is_a? Excon::Errors::HTTPStatusError
|
199
|
+
Errors::Storage::S3::UploaderError.new(<<-EOS)
|
200
|
+
#{ msg }
|
201
|
+
Reason: #{ err.class }
|
202
|
+
response => #{ err.response.inspect }
|
203
|
+
EOS
|
204
|
+
else
|
205
|
+
Errors::Storage::S3::UploaderError.wrap(err, msg)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end # class Uploader
|
91
209
|
|
92
210
|
end
|
93
211
|
end
|
data/lib/backup/storage/scp.rb
CHANGED
@@ -14,78 +14,50 @@ module Backup
|
|
14
14
|
# Server IP Address and SCP port
|
15
15
|
attr_accessor :ip, :port
|
16
16
|
|
17
|
-
##
|
18
|
-
# Path to store backups to
|
19
|
-
attr_accessor :path
|
20
|
-
|
21
|
-
##
|
22
|
-
# Creates a new instance of the storage object
|
23
17
|
def initialize(model, storage_id = nil, &block)
|
24
|
-
super
|
18
|
+
super
|
19
|
+
instance_eval(&block) if block_given?
|
25
20
|
|
26
21
|
@port ||= 22
|
27
22
|
@path ||= 'backups'
|
28
|
-
|
29
|
-
instance_eval(&block) if block_given?
|
30
|
-
|
31
|
-
@path = path.sub(/^\~\//, '')
|
23
|
+
path.sub!(/^~\//, '')
|
32
24
|
end
|
33
25
|
|
34
26
|
private
|
35
27
|
|
36
|
-
##
|
37
|
-
# Establishes a connection to the remote server
|
38
|
-
# and yields the Net::SSH connection.
|
39
|
-
# Net::SCP will use this connection to transfer backups
|
40
28
|
def connection
|
41
29
|
Net::SSH.start(
|
42
30
|
ip, username, :password => password, :port => port
|
43
31
|
) {|ssh| yield ssh }
|
44
32
|
end
|
45
33
|
|
46
|
-
##
|
47
|
-
# Transfers the archived file to the specified remote server
|
48
34
|
def transfer!
|
49
|
-
remote_path = remote_path_for(@package)
|
50
|
-
|
51
35
|
connection do |ssh|
|
52
36
|
ssh.exec!("mkdir -p '#{ remote_path }'")
|
53
37
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
ssh.scp.upload!(
|
59
|
-
File.join(local_path, local_file),
|
60
|
-
File.join(remote_path, remote_file)
|
61
|
-
)
|
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)
|
62
43
|
end
|
63
44
|
end
|
64
45
|
end
|
65
46
|
|
66
|
-
|
67
|
-
#
|
68
|
-
# Any error raised will be rescued during Cycling
|
69
|
-
# and a warning will be logged, containing the error message.
|
47
|
+
# Called by the Cycler.
|
48
|
+
# Any error raised will be logged as a warning.
|
70
49
|
def remove!(package)
|
71
|
-
|
72
|
-
|
73
|
-
messages = []
|
74
|
-
transferred_files_for(package) do |local_file, remote_file|
|
75
|
-
messages << "#{storage_name} started removing " +
|
76
|
-
"'#{local_file}' from '#{ip}'."
|
77
|
-
end
|
78
|
-
Logger.info messages.join("\n")
|
50
|
+
Logger.info "Removing backup package dated #{ package.time }..."
|
79
51
|
|
80
52
|
errors = []
|
81
53
|
connection do |ssh|
|
82
|
-
ssh.exec!("rm -r '#{
|
54
|
+
ssh.exec!("rm -r '#{ remote_path_for(package) }'") do |ch, stream, data|
|
83
55
|
errors << data if stream == :stderr
|
84
56
|
end
|
85
57
|
end
|
86
58
|
unless errors.empty?
|
87
59
|
raise Errors::Storage::SCP::SSHError,
|
88
|
-
|
60
|
+
"Net::SSH reported the following errors:\n" +
|
89
61
|
errors.join("\n")
|
90
62
|
end
|
91
63
|
end
|
data/lib/backup/storage/sftp.rb
CHANGED
@@ -14,27 +14,17 @@ module Backup
|
|
14
14
|
# Server IP Address and SFTP port
|
15
15
|
attr_accessor :ip, :port
|
16
16
|
|
17
|
-
##
|
18
|
-
# Path to store backups to
|
19
|
-
attr_accessor :path
|
20
|
-
|
21
|
-
##
|
22
|
-
# Creates a new instance of the storage object
|
23
17
|
def initialize(model, storage_id = nil, &block)
|
24
|
-
super
|
18
|
+
super
|
19
|
+
instance_eval(&block) if block_given?
|
25
20
|
|
26
21
|
@port ||= 22
|
27
22
|
@path ||= 'backups'
|
28
|
-
|
29
|
-
instance_eval(&block) if block_given?
|
30
|
-
|
31
|
-
@path = path.sub(/^\~\//, '')
|
23
|
+
path.sub!(/^~\//, '')
|
32
24
|
end
|
33
25
|
|
34
26
|
private
|
35
27
|
|
36
|
-
##
|
37
|
-
# Establishes a connection to the remote server
|
38
28
|
def connection
|
39
29
|
Net::SFTP.start(
|
40
30
|
ip, username,
|
@@ -43,39 +33,28 @@ module Backup
|
|
43
33
|
) {|sftp| yield sftp }
|
44
34
|
end
|
45
35
|
|
46
|
-
##
|
47
|
-
# Transfers the archived file to the specified remote server
|
48
36
|
def transfer!
|
49
|
-
remote_path = remote_path_for(@package)
|
50
|
-
|
51
37
|
connection do |sftp|
|
52
|
-
create_remote_path(
|
38
|
+
create_remote_path(sftp)
|
53
39
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
sftp.upload!(
|
59
|
-
File.join(local_path, local_file),
|
60
|
-
File.join(remote_path, remote_file)
|
61
|
-
)
|
40
|
+
package.filenames.each do |filename|
|
41
|
+
src = File.join(Config.tmp_path, filename)
|
42
|
+
dest = File.join(remote_path, filename)
|
43
|
+
Logger.info "Storing '#{ ip }:#{ dest }'..."
|
44
|
+
sftp.upload!(src, dest)
|
62
45
|
end
|
63
46
|
end
|
64
47
|
end
|
65
48
|
|
66
|
-
|
67
|
-
#
|
68
|
-
# Any error raised will be rescued during Cycling
|
69
|
-
# and a warning will be logged, containing the error message.
|
49
|
+
# Called by the Cycler.
|
50
|
+
# Any error raised will be logged as a warning.
|
70
51
|
def remove!(package)
|
71
|
-
|
52
|
+
Logger.info "Removing backup package dated #{ package.time }..."
|
72
53
|
|
54
|
+
remote_path = remote_path_for(package)
|
73
55
|
connection do |sftp|
|
74
|
-
|
75
|
-
|
76
|
-
"'#{ local_file }' from '#{ ip }'."
|
77
|
-
|
78
|
-
sftp.remove!(File.join(remote_path, remote_file))
|
56
|
+
package.filenames.each do |filename|
|
57
|
+
sftp.remove!(File.join(remote_path, filename))
|
79
58
|
end
|
80
59
|
|
81
60
|
sftp.rmdir!(remote_path)
|
@@ -90,7 +69,7 @@ module Backup
|
|
90
69
|
# '/') and loop through that to create the directories one by one.
|
91
70
|
# Net::SFTP raises an exception when the directory it's trying to create
|
92
71
|
# already exists, so we have rescue it
|
93
|
-
def create_remote_path(
|
72
|
+
def create_remote_path(sftp)
|
94
73
|
path_parts = Array.new
|
95
74
|
remote_path.split('/').each do |path_part|
|
96
75
|
path_parts << path_part
|