s3-tar-backup 1.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 842cea517d3276df06766424c4354d644069df99
4
+ data.tar.gz: 1ad93ccb5dd87a00b794692643cdf9dfc4936bcd
5
+ SHA512:
6
+ metadata.gz: bc7a8b17a71626eef16fc974d4725a2cb11482d4cdb51201093c3a51e1fe12565228666a2302f5d1498f298b8579ceb23c2e913a75682961e5425f00e842ca84
7
+ data.tar.gz: f7b8d94f5f23c8f234c2f1003b6a48132baacba2df14db0b9754b45cdea812f783e20ffee995b20595864fbf8141280ffd6d65160b33ef097abc1d5ed3966d02
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,250 @@
1
+ S3-tar-backup
2
+ =============
3
+
4
+ About
5
+ -----
6
+
7
+ This tool allows you to backup a set of directories to an Amazon S3 bucket, using incremental backups.
8
+ You can then restore the files at a later date.
9
+
10
+ This tool was built as a replacement for duplicity, after duplicity started throwing errors and generally being a bit unreliable.
11
+ It uses command-line tar to create incremental backup snapshots, and the aws-s3 gem to upload these to S3.
12
+
13
+ In practice, it turns out that this tool has few lower bandwidth and CPU requirements, and can restore a backup in a fraction of the time that duplicity would take.
14
+
15
+ Installation
16
+ ------------
17
+
18
+ This tool is not yet a ruby gem, and unless people ask me, it will remain that way.
19
+
20
+ Therefore, to install:
21
+
22
+ ```
23
+ $ git clone git://github.com/canton7/s3-tar-backup.git
24
+ $ cd s3-tar-backup
25
+ $ sudo rake install
26
+ ```
27
+
28
+ Configuration
29
+ -------------
30
+
31
+ ### Introduction
32
+
33
+ Configuration is done using an ini file, which can be in the location of your choice.
34
+
35
+ The config file consists of two sections: a global `[settings]` section, followed by profiles.
36
+ With the exception of two keys, every configuration key can be specified either in `[settings]`, in a profile, or both (in which case the profile value is either replaces, or is added to, the value from `[settings]`).
37
+
38
+ ### Global config
39
+
40
+ ```ini
41
+ [settings]
42
+ ; These two keys must be located in this section
43
+ ; You can use environmental variables instead of these -- see below
44
+ aws_access_key_id = <your aws access key>
45
+ aws_secret_access_key = <your aws secret key>
46
+
47
+ ; Set this to the AWS region you want to use
48
+ ; See http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
49
+ aws_region = <your aws region>
50
+
51
+ ; The rest of the keys can either be located here, or in your profiles, or both
52
+ ; The value from the profile will replace the value specified here
53
+ full_if_older_than = <timespec>
54
+
55
+ ; Choose one of the following two settings
56
+ remove_older_than = <timespec>
57
+ remove_all_but_n_full = <number>
58
+
59
+ backup_dir = </path/to/dir>
60
+ dest = <bucket_name>/<path>
61
+ pre-backup = <some command>
62
+ post-backup = <some command>
63
+
64
+ compression = <compression_type>
65
+
66
+ always_full = <bool>
67
+
68
+ ; You have have multiple lines of the following types.
69
+ ; Values from here and from your profiles will be combined
70
+ source = </path/to/another/source>
71
+ source = </path/to/another/source>
72
+ exclude = </some/dir>
73
+ exclude = </some/other/dir>
74
+
75
+ ```
76
+
77
+ `aws_access_key_id` and `aws_secret_access_key` are fairly obvious -- you'll have been given these when you signed up for S3.
78
+ If you can't find them, [look here](http://aws.amazon.com/security-credentials).
79
+ You can use the environmental variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY instead if you prefer -- the env vars will override the config options.
80
+
81
+ `full_if_older_than` tells s3-tar-backup how long if should leave between full backups.
82
+ `<timespec>` is stolen from duplicity. It is given as an interval, which is a number followed by one of the characers s, m, h, D, W, M, or Y (indicating seconds, minutes, hours, days, weeks, months, or years respectively), or a series of such pairs.
83
+ For instance, "1h45m" indicates the time that was one hour and 45 minutes ago.
84
+ The calendar here is unsophisticated: a month is always 30 days, a year is always 365 days, and a day is always 86400 seconds.
85
+
86
+ `remove_older_than` tells s3-tar-backup to remove old backups which are older than `<timespec>` (see above for the format of `<timespec>`).
87
+
88
+ `remove_all_but_n_full` tells s3-tar-backup to remove all backups which were made before the last `n` full backups.
89
+
90
+ `backup_dir` is the directory used (a) to store temporary data, and (b) to store a record of what files were backed up last time (tar's snar file).
91
+ You can delete this dir at any time, but that will slow down the next backup slightly.
92
+
93
+ `dest` is the place where your backups are stored. It consists of the name of the S3 bucket (buckets aren't created automatically), followed by the folder to store objects in, for example `my-backups/tar/`.
94
+
95
+ `pre-backup` and `post-backup` are two hooks, which are run before and after a backup, respectively.
96
+ These lines are optional -- you can have no pre-backup or post-backup lines anywhere in your config if you wish.
97
+ Note that `post-backup` is only run after a successful command.
98
+ These can be used to do things such as back up a mysql database.
99
+ Note that you can have multiple `pre-backup` and `post-backup` lines -- all of the listed commands will be executed.
100
+
101
+ `compression` gives the compression type.
102
+ Valid values are `gzip`, `bzip2`, `lzma`, `lzma2`.
103
+ s3-tar-backup is capable of auto-detecting the format of a previously-backed-up archive, and so changing this value will not invalidate previous backups.
104
+
105
+ `always_full` is an optional key which have have the value `True` or `False`.
106
+ This is used to say that incremental backups should never be used.
107
+
108
+ `source` contains the folders to be backed up.
109
+
110
+ `exclude` lines specify files/dirs to exclude from the backup.
111
+ See the `--exclude` option to tar.
112
+ The exclude lines are optional -- you can have no exclude lines anywhere in your config if you wish.
113
+
114
+ **Note:** You can have multiple profiles which use the same `dest` and `backup_dir`.
115
+
116
+ ### Profile config
117
+
118
+ Next, define your profiles.
119
+
120
+ Profiles are used to specify, or override from the global config, those config keys which may be specified in either the global config or in a profile.
121
+
122
+ A profile takes the form:
123
+
124
+ ```ini
125
+ [profile "profile_name"]
126
+ ; You can optionally specify the following keys
127
+ backup_dir = </path/to/dir>
128
+ source = </path/to/source>
129
+ source = </path/to/another/source>
130
+ dest = <bucket_name>/<path>
131
+ exclude = </some/dir>
132
+ pre-backup = <some command>
133
+ post-backup = <some command>
134
+ ```
135
+
136
+ `profile_name` is the name of the profile. You'll use this later.
137
+
138
+ ### Example config file
139
+
140
+ ```ini
141
+ [settings]
142
+ aws_access_key = ABCD
143
+ aws_secret_access_key = ABCDE
144
+ aws_region = eu-west-1
145
+ ; Do a new full backup every 2 weeks
146
+ full_if_older_than = 2W
147
+ ; Keep 5 sets of full backups
148
+ remove_all_but_n_full = 5
149
+ backup_dir = /root/.backup
150
+ dest = my-backups/tar
151
+ ; You may prefer bzip2, as it has a much lower CPU cost
152
+ compression = lzma2
153
+
154
+ [profile "www"]
155
+ source = /srv/http
156
+
157
+ [profile "home"]
158
+ source = /home/me
159
+ source = /root
160
+ exclude = .backup
161
+ ; Do full backups less rarely
162
+ full_if_older_than = 4W
163
+
164
+ [profile "mysql"]
165
+ pre-backup = mysqldump -uuser -ppassword --all-databases > /tmp/mysql_dump.sql
166
+ source = /tmp/mysql_dump.sql
167
+ post-backup = rm /tmp/mysql_dump.sql
168
+ ; My MySQL dumps are so small that incremental backups actually add more overhead
169
+ always_full = True
170
+ ```
171
+
172
+ Usage
173
+ -----
174
+
175
+ s3-tar-backup works in a number of different modes: backup, restore, cleanup, backup-config, list-backups.
176
+
177
+ ### Backup
178
+
179
+ ```
180
+ s3-tar-backup --config <config_file> [--profile <profile>] --backup [--full] [--verbose]
181
+ ```
182
+
183
+ You can use `-c` instead of `--config`, and `-p` instead of `--profile`.
184
+
185
+ `<config_file>` is the path to the file you created above, and `<profile>` is the name of a profile inside it.
186
+ You can also specify multiple profiles.
187
+
188
+ If no profile is specified, all profiles are backed up.
189
+
190
+ `--full` will force s3-tar-backup to do a full backup (instead of an incremental one), regardless of which it thinks it should do based on your cofnig file.
191
+
192
+ `--verbose` will get tar to list the files that it is backing up.
193
+
194
+ Example:
195
+
196
+ ```
197
+ s3-tar-backup -c ~/.backup/config.ini -p www home --backup
198
+ ```
199
+
200
+ ### Cleanup
201
+
202
+ **Note:** Cleans are automatically done at the end of each backup.
203
+
204
+ ```
205
+ s3-tar-backup --config <config_file> [--profile <profile>] --cleanup
206
+ ```
207
+
208
+ s3-tar-backup will go through all old backups, and remove those specified by `remove_all_but_n_full` or `remove_older_than`.
209
+
210
+ ### Restore
211
+
212
+ ```
213
+ s3-tar-backup --config <config_file> [--profile <profile>] --restore <restore_dir> [--restore_date <restore_date>] [--verbose]
214
+ ```
215
+
216
+ This command will get s3-tar-backup to fetch all the necessary data to restore the latest version of your backup (or an older one if you use `--restore-date`), and stick it into `<restore_dir>`.
217
+
218
+ Using `<restore_date>`, you can tell s3-tar-backup to restore the first backup before the specified date.
219
+ The date format to use is `YYYYMM[DD[hh[mm[ss]]]]`, for example `20110406` means `2011-04-06 00:00:00`, while `201104062143` means `2011-04-06 21:43:00`.
220
+
221
+ `--verbose` makes tar spit out the files that it restores.
222
+
223
+ Examples:
224
+
225
+ ```
226
+ s3-tar-backup -c ~/.backup/config.ini -p www home --restore my_restore/
227
+ s3-tar-backup -c ~/.backup/config.ini -p mysql --restore my_restore/ --restore_date 201104062143
228
+ ```
229
+
230
+ ### Backup Config file
231
+
232
+ ```
233
+ s3-tar-backup --config <config_file> [--profile <profile>] --backup-config [--verbose]
234
+ ```
235
+
236
+ This command is used to backup the specified configuration file.
237
+ Where it is backed up to depends on your setup:
238
+
239
+ - If you've specified `dest` under `[settings]`, this location is used
240
+ - If you're only got one profile, and `dest` is under this profile, then this location is used.
241
+ - If you have multiple profiles, and there's no `dest` under `[settings]`, you must specify a profile, and this profile's `dest` will be used.
242
+
243
+ ### List backups
244
+
245
+ ```
246
+ s3-tar-backup --config <config_file> [--profile <profile>] --list-backups [--verbose]
247
+ ```
248
+
249
+ This command is used to view information on the current backed-up archives for the specified profile(s) (or all profiles).
250
+ This is handy if you need to restore a backup, and want to know things such as how much data you'll have to download, or what dates are available to restore from.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname($PROGRAM_NAME)+File::SEPARATOR+'..'+File::SEPARATOR+'lib')
4
+
5
+ require 's3_tar_backup'
6
+
7
+ S3TarBackup::Main.new.run
@@ -0,0 +1,312 @@
1
+ require 'aws-sdk'
2
+ require 'trollop'
3
+ require 's3_tar_backup/ini_parser'
4
+ require 's3_tar_backup/backup'
5
+ require 's3_tar_backup/version'
6
+
7
+ module S3TarBackup
8
+ class Main
9
+ UPLOAD_TRIES = 5
10
+
11
+ def run
12
+ opts = Trollop::options do
13
+ version VERSION
14
+ banner "Backs up files to, and restores files from, Amazon's S3 storage, using tar incremental backups\n\n" \
15
+ "Usage:\ns3-tar-backup -c config.ini [-p profile] --backup [--full] [-v]\n" \
16
+ "s3-tar-backup -c config.ini [-p profile] --cleanup [-v]\n" \
17
+ "s3-tar-backup -c config.ini [-p profile] --restore restore_dir\n\t[--restore_date date] [-v]\n" \
18
+ "s3-tar-backup -c config.ini [-p profile] --backup-config [--verbose]\n" \
19
+ "s3-tar-backup -c config.ini [-p profile] --list-backups\n\n" \
20
+ "Option details:\n"
21
+ opt :config, "Configuration file", :short => 'c', :type => :string, :required => true
22
+ opt :backup, "Make an incremental backup"
23
+ opt :full, "Make the backup a full backup"
24
+ opt :profile, "The backup profile(s) to use (default all)", :short => 'p', :type => :strings
25
+ opt :cleanup, "Clean up old backups"
26
+ opt :restore, "Restore a backup to the specified dir", :type => :string
27
+ opt :restore_date, "Restore a backup from the specified date. Format YYYYMM[DD[hh[mm[ss]]]]", :type => :string
28
+ opt :backup_config, "Backs up the specified configuration file"
29
+ opt :list_backups, "List the stored backup info for one or more profiles"
30
+ opt :verbose, "Show verbose output", :short => 'v'
31
+ conflicts :backup, :cleanup, :restore, :backup_config, :list_backups
32
+ end
33
+
34
+
35
+ Trollop::die "--full requires --backup" if opts[:full] && !opts[:backup]
36
+ Trollop::die "--restore-date requires --restore" if opts[:restore_date_given] && !opts[:restore_given]
37
+ unless opts[:backup] || opts[:cleanup] || opts[:restore_given] || opts[:backup_config] || opts[:list_backups]
38
+ Trollop::die "Need one of --backup, --cleanup, --restore, --backup-config, --list-backups"
39
+ end
40
+
41
+ begin
42
+ raise "Config file #{opts[:config]} not found" unless File.exists?(opts[:config])
43
+ config = IniParser.new(opts[:config]).load
44
+ profiles = opts[:profile] || config.find_sections(/^profile\./).keys.map{ |k| k.to_s.split('.', 2)[1] }
45
+ @s3 = connect_s3(
46
+ ENV['AWS_ACCESS_KEY_ID'] || config['settings.aws_access_key_id'],
47
+ ENV['AWS_SECRET_ACCESS_KEY'] || config['settings.aws_secret_access_key'],
48
+ config.get('settings.aws_region', false)
49
+ )
50
+
51
+ # This is a bit of a special case
52
+ if opts[:backup_config]
53
+ dest = config.get('settings.dest', false)
54
+ raise "You must specify a single profile (used to determine the location to back up to) " \
55
+ "if backing up config and dest key is not in [settings]" if !dest && profiles.count != 1
56
+ dest ||= config["profile.#{profiles[0]}.dest"]
57
+ puts "===== Backing up config file #{opts[:config]} ====="
58
+ bucket, prefix = (config.get('settings.dest', false) || config["profile.#{profiles[0]}.dest"]).split('/', 2)
59
+ puts "Uploading #{opts[:config]} to #{bucket}/#{prefix}/#{File.basename(opts[:config])}"
60
+ upload(opts[:config], bucket, "#{prefix}/#{File.basename(opts[:config])}")
61
+ return
62
+ end
63
+
64
+ profiles.dup.each do |profile|
65
+ raise "No such profile: #{profile}" unless config.has_section?("profile.#{profile}")
66
+ opts[:profile] = profile
67
+ backup_config = gen_backup_config(opts[:profile], config)
68
+ prev_backups = get_objects(backup_config[:bucket], backup_config[:dest_prefix], opts[:profile])
69
+ perform_backup(opts, prev_backups, backup_config) if opts[:backup]
70
+ perform_cleanup(prev_backups, backup_config) if opts[:backup] || opts[:cleanup]
71
+ perform_restore(opts, prev_backups, backup_config) if opts[:restore_given]
72
+ perform_list_backups(prev_backups, backup_config) if opts[:list_backups]
73
+ end
74
+ rescue Exception => e
75
+ raise e
76
+ Trollop::die e.to_s
77
+ end
78
+ end
79
+
80
+ def connect_s3(access_key, secret_key, region)
81
+ warn "No AWS region specified (config key settings.s3_region). Assuming eu-west-1" unless region
82
+ AWS::S3.new(access_key_id: access_key, secret_access_key: secret_key, region: region || 'eu-west-1')
83
+ end
84
+
85
+ def gen_backup_config(profile, config)
86
+ bucket, dest_prefix = (config.get("profile.#{profile}.dest", false) || config['settings.dest']).split('/', 2)
87
+ backup_config = {
88
+ :backup_dir => config.get("profile.#{profile}.backup_dir", false) || config['settings.backup_dir'],
89
+ :name => profile,
90
+ :sources => [*config.get("profile.#{profile}.source", [])] + [*config.get("settings.source", [])],
91
+ :exclude => [*config.get("profile.#{profile}.exclude", [])] + [*config.get("settings.exclude", [])],
92
+ :bucket => bucket,
93
+ :dest_prefix => dest_prefix.chomp('/'),
94
+ :pre_backup => [*config.get("profile.#{profile}.pre-backup", [])] + [*config.get('settings.pre-backup', [])],
95
+ :post_backup => [*config.get("profile.#{profile}.post-backup", [])] + [*config.get('settings.post-backup', [])],
96
+ :full_if_older_than => config.get("profile.#{profile}.full_if_older_than", false) || config['settings.full_if_older_than'],
97
+ :remove_older_than => config.get("profile.#{profile}.remove_older_than", false) || config.get('settings.remove_older_than', false),
98
+ :remove_all_but_n_full => config.get("profile.#{profile}.remove_all_but_n_full", false) || config.get('settings.remove_all_but_n_full', false),
99
+ :compression => (config.get("profile.#{profile}.compression", false) || config.get('settings.compression', 'gzip')).to_sym,
100
+ :always_full => config.get('settings.always_full', false) || config.get("profile.#{profile}.always_full", false),
101
+ }
102
+ backup_config
103
+ end
104
+
105
+ def perform_backup(opts, prev_backups, backup_config)
106
+ puts "===== Backing up profile #{backup_config[:name]} ====="
107
+ backup_config[:pre_backup].each_with_index do |cmd, i|
108
+ puts "Executing pre-backup hook #{i+1}"
109
+ system(cmd)
110
+ end
111
+ full_required = full_required?(backup_config[:full_if_older_than], prev_backups)
112
+ puts "Last full backup is too old. Forcing a full backup" if full_required && !opts[:full] && backup_config[:always_full]
113
+ if full_required || opts[:full] || backup_config[:always_full]
114
+ backup_full(backup_config, opts[:verbose])
115
+ else
116
+ backup_incr(backup_config, opts[:verbose])
117
+ end
118
+ backup_config[:post_backup].each_with_index do |cmd, i|
119
+ puts "Executing post-backup hook #{i+1}"
120
+ system(cmd)
121
+ end
122
+ end
123
+
124
+ def perform_cleanup(prev_backups, backup_config)
125
+ puts "===== Cleaning up profile #{backup_config[:name]} ====="
126
+ remove = []
127
+ if age_str = backup_config[:remove_older_than]
128
+ age = parse_interval(age_str)
129
+ remove = prev_backups.select{ |o| o[:date] < age }
130
+ # Don't want to delete anything before the last full backup
131
+ unless remove.empty?
132
+ kept = remove.slice!(remove.rindex{ |o| o[:type] == :full }..-1).count
133
+ puts "Keeping #{kept} old backups as part of a chain" if kept > 1
134
+ end
135
+ elsif keep_n = backup_config[:remove_all_but_n_full]
136
+ keep_n = keep_n.to_i
137
+ # Get the date of the last full backup to keep
138
+ if last_full_to_keep = prev_backups.select{ |o| o[:type] == :full }[-keep_n]
139
+ # If there is a last full one...
140
+ remove = prev_backups.select{ |o| o[:date] < last_full_to_keep[:date] }
141
+ end
142
+ end
143
+
144
+ if remove.empty?
145
+ puts "Nothing to do"
146
+ else
147
+ puts "Removing #{remove.count} old backup files"
148
+ end
149
+ remove.each do |object|
150
+ @s3.buckets[backup_config[:bucket]].objects["#{backup_config[:dest_prefix]}/#{object[:name]}"].delete
151
+ end
152
+ end
153
+
154
+ # Config should have the keys
155
+ # backup_dir, name, soruces, exclude, bucket, dest_prefix
156
+ def backup_incr(config, verbose=false)
157
+ puts "Starting new incremental backup"
158
+ backup = Backup.new(config[:backup_dir], config[:name], config[:sources], config[:exclude], config[:compression])
159
+
160
+ # Try and get hold of the snar file
161
+ unless backup.snar_exists?
162
+ puts "Failed to find snar file. Attempting to download..."
163
+ s3_snar = "#{config[:dest_prefix]}/#{backup.snar}"
164
+ object = @s3.buckets[config[:bucket]].objects[s3_snar]
165
+ if object.exists?
166
+ puts "Found file on S3. Downloading"
167
+ open(backup.snar_path, 'wb') do |f|
168
+ object.read do |chunk|
169
+ f.write(chunk)
170
+ end
171
+ end
172
+ else
173
+ puts "Failed to download snar file. Defaulting to full backup"
174
+ end
175
+ end
176
+
177
+ backup(config, backup, verbose)
178
+ end
179
+
180
+ def backup_full(config, verbose=false)
181
+ puts "Starting new full backup"
182
+ backup = Backup.new(config[:backup_dir], config[:name], config[:sources], config[:exclude], config[:compression])
183
+ # Nuke the snar file -- forces a full backup
184
+ File.delete(backup.snar_path) if File.exists?(backup.snar_path)
185
+ backup(config, backup, verbose)
186
+ end
187
+
188
+ def backup(config, backup, verbose=false)
189
+ system(backup.backup_cmd(verbose))
190
+ puts "Uploading #{config[:bucket]}/#{config[:dest_prefix]}/#{File.basename(backup.archive)} (#{bytes_to_human(File.size(backup.archive))})"
191
+ upload(backup.archive, config[:bucket], "#{config[:dest_prefix]}/#{File.basename(backup.archive)}")
192
+ puts "Uploading snar (#{bytes_to_human(File.size(backup.snar_path))})"
193
+ upload(backup.snar_path, config[:bucket], "#{config[:dest_prefix]}/#{File.basename(backup.snar)}")
194
+ File.delete(backup.archive)
195
+ end
196
+
197
+ def upload(source, bucket, dest_name)
198
+ tries = 0
199
+ begin
200
+ @s3.buckets[bucket].objects.create(dest_name, Pathname.new(source))
201
+ rescue Errno::ECONNRESET => e
202
+ tries += 1
203
+ if tries <= UPLOAD_TRIES
204
+ puts "Upload Exception: #{e}"
205
+ puts "Retrying #{tries}/#{UPLOAD_TRIES}..."
206
+ retry
207
+ else
208
+ raise e
209
+ end
210
+ end
211
+ puts "Succeeded" if tries > 0
212
+ end
213
+
214
+ def perform_restore(opts, prev_backups, backup_config)
215
+ puts "===== Restoring profile #{backup_config[:name]} ====="
216
+ # If restore date given, parse
217
+ if opts[:restore_date_given]
218
+ m = opts[:restore_date].match(/(\d\d\d\d)(\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?/)
219
+ raise "Unknown date format in --restore-to" if m.nil?
220
+ restore_to = Time.new(*m[1..-1].map{ |s| s.to_i if s })
221
+ else
222
+ restore_to = Time.now
223
+ end
224
+
225
+ # Find the index of the first backup, incremental or full, before that date
226
+ restore_end_index = prev_backups.rindex{ |o| o[:date] < restore_to }
227
+ raise "Failed to find a backup for that date" unless restore_end_index
228
+
229
+ # Find the first full backup before that one
230
+ restore_start_index = prev_backups[0..restore_end_index].rindex{ |o| o[:type] == :full }
231
+
232
+ restore_dir = opts[:restore].chomp('/') << '/'
233
+
234
+ Dir.mkdir(restore_dir) unless Dir.exists?(restore_dir)
235
+ raise "Detination dir is not a directory" unless File.directory?(restore_dir)
236
+
237
+ prev_backups[restore_start_index..restore_end_index].each do |object|
238
+ puts "Fetching #{backup_config[:bucket]}/#{backup_config[:dest_prefix]}/#{object[:name]} (#{bytes_to_human(object[:size])})"
239
+ dl_file = "#{backup_config[:backup_dir]}/#{object[:name]}"
240
+ open(dl_file, 'wb') do |f|
241
+ @s3.buckets[backup_config[:bucket]].objects["#{backup_config[:dest_prefix]}/#{object[:name]}"].read do |chunk|
242
+ f.write(chunk)
243
+ end
244
+ end
245
+ puts "Extracting..."
246
+ system(Backup.restore_cmd(restore_dir, dl_file, opts[:verbose]))
247
+
248
+ File.delete(dl_file)
249
+ end
250
+ end
251
+
252
+ def perform_list_backups(prev_backups, backup_config)
253
+ # prev_backups alreays contains just the files for the current profile
254
+ puts "===== Backups list for #{backup_config[:name]} ====="
255
+ puts "Type: N: Date:#{' '*18}Size: Chain Size: Format:\n\n"
256
+ prev_type = ''
257
+ total_size = 0
258
+ chain_length = 0
259
+ chain_cum_size = 0
260
+ prev_backups.each do |object|
261
+ type = object[:type] == prev_type && object[:type] == :incr ? " -- " : object[:type].to_s.capitalize
262
+ prev_type = object[:type]
263
+ chain_length += 1
264
+ chain_length = 0 if object[:type] == :full
265
+ chain_cum_size = 0 if object[:type] == :full
266
+ chain_cum_size += object[:size]
267
+
268
+ chain_length_str = (chain_length == 0 ? '' : chain_length.to_s).ljust(3)
269
+ chain_cum_size_str = (object[:type] == :full ? '' : bytes_to_human(chain_cum_size)).ljust(8)
270
+ puts "#{type} #{chain_length_str} #{object[:date].strftime('%F %T')} #{bytes_to_human(object[:size]).ljust(8)} " \
271
+ "#{chain_cum_size_str} (#{object[:compression]})"
272
+ total_size += object[:size]
273
+ end
274
+ puts "\n"
275
+ puts "Total size: #{bytes_to_human(total_size)}"
276
+ puts "\n"
277
+ end
278
+
279
+ def get_objects(bucket, prefix, profile)
280
+ objects = @s3.buckets[bucket].objects.with_prefix(prefix).map do |object|
281
+ Backup.parse_object(object, profile)
282
+ end
283
+ objects.compact.sort_by{ |o| o[:date] }
284
+ end
285
+
286
+ def parse_interval(interval_str)
287
+ time = Time.now
288
+ time -= $1.to_i if interval_str =~ /(\d+)s/
289
+ time -= $1.to_i*60 if interval_str =~ /(\d+)m/
290
+ time -= $1.to_i*3600 if interval_str =~ /(\d+)h/
291
+ time -= $1.to_i*86400 if interval_str =~ /(\d+)D/
292
+ time -= $1.to_i*604800 if interval_str =~ /(\d+)W/
293
+ time -= $1.to_i*2592000 if interval_str =~ /(\d+)M/
294
+ time -= $1.to_i*31536000 if interval_str =~ /(\d+)Y/
295
+ time
296
+ end
297
+
298
+ def full_required?(interval_str, objects)
299
+ time = parse_interval(interval_str)
300
+ objects.select{ |o| o[:type] == :full && o[:date] > time }.empty?
301
+ end
302
+
303
+ def bytes_to_human(n)
304
+ count = 0
305
+ while n >= 1014 && count < 4
306
+ n /= 1024.0
307
+ count += 1
308
+ end
309
+ format("%.2f", n) << %w(B KB MB GB TB)[count]
310
+ end
311
+ end
312
+ end