backup_utility 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module BackupUtility; end
2
+ require 'backup_utility/postgres'
3
+ require 'backup_utility/mongo'
@@ -0,0 +1,228 @@
1
+ require 'fileutils'
2
+
3
+ CODE_DIR = '/home/twist/code'
4
+ BACKUP_DIR = '/home/twist/backups'
5
+ SHARED_DIR = '/mnt/shared_storage2/database/backups'
6
+ class BackupUtility::Base
7
+ def initialize(backup_info)
8
+ @backup_dir = backup_info[:backup_dir] || BACKUP_DIR
9
+ @shared_dir = backup_info[:shared_dir] || SHARED_DIR
10
+ @code_dir = backup_info[:code_dir] || CODE_DIR
11
+ @backup_machine = backup_info[:backup_machine] || 'a1-stats'
12
+
13
+ @send_to_shared = backup_info.fetch(:send_to_shared, true)
14
+ @send_to_s3 = backup_info.fetch(:send_to_s3, true)
15
+ @move_backups = backup_info.fetch(:move_backups, true)
16
+ @use_code = backup_info.fetch(:use_code, false)
17
+ @use_scp = backup_info.fetch(:use_scp, false)
18
+ @save = backup_info.fetch(:save, true)
19
+ @clean_shared_age = backup_info[:clean_shared_age]
20
+ @clean_shared_age = ::Rails.env == 'production' ? 30 : 14 if @clean_shared_age.blank?
21
+
22
+ @clean_backups_age = backup_info[:clean_backups_age]
23
+ if @clean_backups_age.blank?
24
+ if ::Rails.env == 'production'
25
+ @cleanup_backups_age = @move_backups ? 7 : 14
26
+ else
27
+ @cleanup_backups_age = @move_backups ? 3 : 7
28
+ end
29
+ end
30
+ @last_dst = nil
31
+ end
32
+
33
+ def log_and_time(out)
34
+ log(out)
35
+ start = Time.now
36
+ ret = yield
37
+ took = Time.now - start
38
+ log("took #{took}")
39
+ ret
40
+ end
41
+
42
+ def log(out)
43
+ puts "#{Time.now} - #{out}"
44
+ end
45
+
46
+ def backup(settings = {}, append = nil)
47
+ now = Time.now
48
+ storage_name, working_dir, backup_file = get_paths(settings, append)
49
+ if !perform_dump(settings, working_dir)
50
+ log("could not perform dump properly")
51
+ return false
52
+ end
53
+ if custom?
54
+ # no need to tar up the directory, all we need to do is rename
55
+ # the file in the backup_dir to the backup name
56
+ log("moving file #{@last_dst} to #{backup_file}")
57
+ File.mv(@last_dst, backup_file)
58
+ ret = 0
59
+ else
60
+ base_name = File.basename(working_dir)
61
+ out = log_and_time("taring up backup file #{backup_file}") do
62
+ # tar up the combined directory
63
+ `tar czf #{backup_file} -C #{@backup_dir} #{base_name}`
64
+ end
65
+ ret=$?
66
+ end
67
+ if ret.to_i != 0
68
+ log("error taring file : #{out}")
69
+ exit 1
70
+ end
71
+ if File.exists?(backup_file)
72
+ log("cleaning up working dir #{@working_dir}")
73
+ FileUtils.rm_rf(working_dir)
74
+ else
75
+ log("backup file #{backup_file} was not created properly")
76
+ return false
77
+ end
78
+ store_backup(storage_name, backup_file)
79
+ taken = Time.now - now
80
+ log("time taken to dump backup #{taken}")
81
+ end
82
+
83
+ def get_paths(settings = {}, append = nil)
84
+ append = settings[:append] if append.nil?
85
+ prepend = settings[:prepend]
86
+ now = Time.now
87
+ storage_name = "#{::Rails.env}_latest"
88
+ format = settings.fetch(:date_format, '%Y-%m-%d-%H')
89
+ date = now.strftime(format)
90
+ storage_name += "_#{append}" if append
91
+ base_name = "#{prepend}#{date}-#{::Rails.env}"
92
+ base_name += "-#{append}" if append
93
+ if custom?
94
+ ext = '.dump.gz'
95
+ else
96
+ ext = '.tar.gz'
97
+ end
98
+ storage_name += ext
99
+ back_name = "#{base_name}#{ext}"
100
+ working_dir = File.join(@backup_dir, base_name)
101
+ Dir.mkdir(working_dir) if !File.exists?(working_dir)
102
+ backup_file = File.join(@backup_dir, back_name)
103
+ [storage_name, working_dir, backup_file]
104
+ end
105
+
106
+ def custom?
107
+ false
108
+ end
109
+
110
+ def store_backup(storage_name, backup_file)
111
+ if @save
112
+ # send file to s3
113
+ if @send_to_s3
114
+ log("sending backup file to s3")
115
+ send_file_to_s3(backup_file)
116
+ end
117
+ if @send_to_shared
118
+ if @use_scp
119
+ # maintain the name so we can send it to the storage properly
120
+ back_name = File.basename(backup_file)
121
+ shared_file = File.join(@shared_dir, back_name)
122
+ log("scping backup file to #{shared_file}")
123
+ `/usr/bin/scp #{backup_file} #{@backup_user}@#{@backup_machine}:#{shared_file}`
124
+ else
125
+ shared_file = File.join(@shared_dir, storage_name)
126
+ log("copying backup file to #{shared_file}")
127
+ FileUtils.copy_file(backup_file, shared_file)
128
+ end
129
+ end
130
+ cleanup_backups
131
+ cleanup_shared
132
+ else
133
+ log("skip save")
134
+ end
135
+ end
136
+
137
+ def shared_dst(create_if_missing = false)
138
+ return nil if !File.exists?(@shared_dir)
139
+ dst = File.join(@shared_dir, "#{::Rails.env}_daily")
140
+ Dir.mkdir(dst) if !File.exists?(dst) && create_if_missing
141
+ dst
142
+ end
143
+
144
+ def cleanup_backups(age = nil)
145
+ return false if !File.exists?(@backup_dir)
146
+ age = @cleanup_backups_age if age.blank?
147
+
148
+ clean_dir = @backup_dir
149
+ cleanup_dir(clean_dir, age) do |file|
150
+ if @move_backups
151
+ dst_file = shared_dst(true)
152
+ next if dst_file.blank? || !File.exists?(dst_file)
153
+ FileUtils.mv(file, dst_file)
154
+ else
155
+ File.delete(file)
156
+ end
157
+ end
158
+ end
159
+
160
+ def cleanup_shared(age = nil)
161
+ clean_dir = shared_dst
162
+ return false if clean_dir.nil? || !File.exists?(clean_dir)
163
+ age = @clean_shared_age if age.blank?
164
+ cleanup_dir(clean_dir, age) do |file|
165
+ File.delete(file)
166
+ end
167
+ end
168
+
169
+ def cleanup_dir(clean_dir, age, display = true)
170
+ cleaned = 0
171
+ total = 0
172
+ Dir["#{clean_dir}/*.tar.gz"].each do |file|
173
+ stat = File.stat(file)
174
+ if stat.mtime < age.to_i.days.ago
175
+ yield file
176
+ cleaned += stat.size
177
+ total += 1
178
+ end
179
+ end
180
+ log("cleaned up #{total} for #{cleaned} bytes in #{clean_dir}") if display
181
+ [total, cleaned]
182
+ end
183
+
184
+ def send_dir_to_s3
185
+ Dir.foreach(@backup_dir) do |f|
186
+ next if f[0] == ?.
187
+ file = File.join(backup_dir, f)
188
+ send_file_to_s3(file)
189
+ end
190
+ end
191
+
192
+ def send_file_to_s3(backup_file)
193
+ if @use_code
194
+ `/usr/bin/python #{@code_dir}/S3Storage.py upload #{backup_file}`
195
+ return true
196
+ end
197
+ bucket = 'twistage-backup'
198
+ # first check if backup file exists on s3, and then upload it
199
+ name = File.basename(backup_file)
200
+ s3 = S3Storage.default_connection
201
+ files = s3.list_bucket({:prefix => name}, bucket)
202
+ size = File.size(backup_file)
203
+ amt = 5
204
+ if files.size == 0
205
+ amt.times do |x|
206
+ log("uploading #{backup_file} to s3")
207
+ begin
208
+ start = Time.now
209
+ s3.put_file(backup_file, bucket)
210
+ taken = Time.now - start
211
+ mb = (size / 1.megabyte) / taken
212
+ log("file uploaded in #{taken} seconds (#{mb} MB/s)")
213
+ return true
214
+ rescue StandardError => err
215
+ if x+1 == amt
216
+ log("could not upload file #{backup_file} : #{err.to_s}")
217
+ return false
218
+ end
219
+ log("could not send file #{err.to_s}, retrying (#{x+1} of #{amt})")
220
+ sleep(15)
221
+ end
222
+ end
223
+ else
224
+ log("#{backup_file} already exists in s3")
225
+ end
226
+ true
227
+ end
228
+ end
@@ -0,0 +1,98 @@
1
+ class BackupUtility::File < BackupUtility::Base
2
+
3
+ def self.init(type)
4
+ save = ENV['save'] != 'false'
5
+ use_code = false
6
+ # don't send to shared, since we're going do it on "cleanup"
7
+ if ::Rails.env == 'production'
8
+ send_to_s3 = true
9
+ else
10
+ send_to_s3 = false
11
+ end
12
+ use_code = send_to_s3
13
+ settings = build_settings(type)
14
+ return [nil, nil] if settings.nil?
15
+ db_info = {:save => save,
16
+ :send_to_s3 => send_to_s3,
17
+ :send_to_shared => false,
18
+ :use_code => use_code,
19
+ :shared_dir => "/mnt/shared_storage2/backups/#{type}",
20
+ :backup_dir => "/home/twist/backups/#{type}",
21
+ :clean_shared_age => ENV['clean_shared_age']
22
+ }
23
+ [BackupUtility::File.new(db_info), settings]
24
+ end
25
+
26
+ def self.build_settings(type)
27
+ age = (ENV['age'] || 5).to_i
28
+ if type == :custom
29
+ src_dir = ENV['custom_dir']
30
+ label = ENV['custom_label']
31
+ if src_dir.nil? || label.nil?
32
+ puts "missing custom dir or label"
33
+ return nil
34
+ end
35
+ type = label.to_sym
36
+ else
37
+ type = type.to_sym
38
+ map = {:logs => '/home/twist/stats_prod/shared/nginx_logs/archive'
39
+ }
40
+ if map.has_key?(type)
41
+ src_dir = map[type]
42
+ else
43
+ # check if the type exists on disk
44
+ src_dir = "/home/twist/stats_prod/shared/log/#{type}_dumps"
45
+ end
46
+ if src_dir.blank? || !File.exists?(src_dir)
47
+ puts "could not find directory to backup for #{type} (src dir : #{src_dir})"
48
+ return nil
49
+ end
50
+ end
51
+ {:type => type, :src_dir => src_dir, :age => age.days.ago, :date_format => '%Y-%m-%d'}
52
+ end
53
+
54
+ def initialize(backup_info)
55
+ super(backup_info)
56
+ end
57
+
58
+ def get_paths(settings = {}, append = nil)
59
+ to_backup = find_dir(settings)
60
+ return [nil, nil, nil] if to_backup.nil?
61
+ date = File.basename(to_backup)
62
+ type = settings[:type]
63
+ base_name = "stats_#{type}_#{date}.tar.gz"
64
+ backup_file = File.join(@backup_dir, base_name)
65
+ working_dir = File.join(@backup_dir, date)
66
+ settings[:to_backup] = to_backup
67
+ [nil, working_dir, backup_file]
68
+ end
69
+
70
+ def find_dir(settings)
71
+ src_dir = settings[:src_dir]
72
+ return nil if src_dir.blank? || !File.exists?(src_dir)
73
+ age = settings[:age]
74
+ to_backup = nil
75
+ Dir.entries(src_dir).each do |name|
76
+ next if name[0] == ?.
77
+ dir = File.join(src_dir, name)
78
+ next if !File.directory?(dir)
79
+ mtime = File.mtime(dir)
80
+ if mtime < age
81
+ to_backup = dir
82
+ puts "found #{dir} to backup"
83
+ break
84
+ end
85
+ end
86
+ to_backup
87
+ end
88
+
89
+ def perform_dump(settings, working_dir)
90
+ return false if working_dir.blank?
91
+ # move everything in the to_backup dir to the working_dir, and then remove it
92
+ to_backup = settings[:to_backup]
93
+ return false if to_backup.blank?
94
+ `mv #{to_backup} #{@backup_dir}`
95
+ return false if File.exists?(to_backup)
96
+ true
97
+ end
98
+ end
@@ -0,0 +1,37 @@
1
+ class BackupUtility::Mongo < BackupUtility::Base
2
+
3
+ def self.init(include_env = true)
4
+ save = ENV['save'] != 'false'
5
+ if ::Rails.env == 'staging'
6
+ send_to_s3 = false
7
+ send_to_shared = true
8
+ elsif ::Rails.env == 'production'
9
+ send_to_s3 = true
10
+ send_to_shared = true
11
+ elsif ::Rails.env == 'development'
12
+ send_to_s3 = false
13
+ send_to_shared = false
14
+ end
15
+ use_code = send_to_s3
16
+ db_info = {:backup_dir => ENV['backup_dir'],
17
+ :save => save,
18
+ :send_to_s3 => send_to_s3,
19
+ :send_to_shared => send_to_shared,
20
+ :use_code => use_code
21
+ }
22
+ BackupUtility::Mongo.new(db_info)
23
+ end
24
+
25
+ def perform_dump(settings, working_dir)
26
+ type = settings[:server_type] || 'data_store'
27
+ server = MongoServer.select(type)
28
+ return false if server.blank?
29
+ db_names = settings[:db_names] || 'videos_day,tracks_day,ref'
30
+ db_names = db_names.split(',')
31
+ db_names.each do |db_name|
32
+ puts "dumping mongo db #{db_name} to #{working_dir}"
33
+ server.dump(working_dir, db_name)
34
+ end
35
+ true
36
+ end
37
+ end
@@ -0,0 +1,260 @@
1
+ require 'fileutils'
2
+
3
+ BIN='/usr/bin'
4
+
5
+ class BackupUtility::Postgres < BackupUtility::Base
6
+
7
+ def self.init(append = 'twist', include_env = true)
8
+ save = ENV['save'] != 'false'
9
+ move_backups = true
10
+ use_scp = use_code = false
11
+ if ::Rails.env == 'staging'
12
+ pg_user = 'admin'
13
+ db_name = "#{append}_stage"
14
+ send_to_s3 = false
15
+ send_to_shared = true
16
+ elsif ::Rails.env == 'production'
17
+ pg_user = "#{append}_db"
18
+ db_name = "#{append}_prod"
19
+ send_to_s3 = true
20
+ send_to_shared = true
21
+ use_scp = use_code = append == 'stats'
22
+ # move the backup files to shared, but only if its not stats, since it does its own thing
23
+ move_backups = append != 'stats'
24
+ elsif ::Rails.env == 'development'
25
+ pg_user = ENV['pg_user'] || 'admin'
26
+ db_name = ENV['db_name'] || "#{append}_dev"
27
+ send_to_s3 = false
28
+ # always set to false
29
+ save = false
30
+ send_to_shared = false
31
+ end
32
+ db_info = {:db_name => db_name, :pg_user => pg_user,
33
+ :backup_dir => ENV['backup_dir'],
34
+ :save => save,
35
+ :send_to_s3 => send_to_s3,
36
+ :send_to_shared => send_to_shared,
37
+ :backup_format => ENV['backup_format'],
38
+ :use_code => use_code,
39
+ :move_backups => move_backups,
40
+ :use_scp => use_scp
41
+ }
42
+ if include_env
43
+ # look for these keys in the ENV variable
44
+ [:shared_dir, :backup_user, :backup_machine, :pg_host, :pg_port].each do |key|
45
+ val = ENV[key.to_s]
46
+ db_info[key] = val if val
47
+ end
48
+ end
49
+ BackupUtility::Postgres.new(db_info)
50
+ end
51
+
52
+ def initialize(db_info)
53
+ super(db_info)
54
+ @db_name = db_info.fetch(:db_name)
55
+ @pg_user = db_info.fetch(:pg_user, 'postgres')
56
+ config = ActiveRecord::Base.configurations[Rails.env]
57
+ if config
58
+ default_port = config['port']
59
+ else
60
+ default_port = nil
61
+ end
62
+ @pg_host = db_info.fetch(:pg_host, nil)
63
+ @pg_port = db_info.fetch(:pg_port, default_port)
64
+ @backup_user = db_info[:backup_user] || 'backup'
65
+ @backup_format = db_info[:backup_format] || 'plain'
66
+ raise "invalid format #{@backup_format}" if !['plain', 'custom', 'tar'].include?(@backup_format)
67
+ @save = db_info.fetch(:save, true)
68
+ @last_dst = nil
69
+ end
70
+
71
+ def get_paths(settings = {}, append = nil)
72
+ if !settings.fetch(:ignore, []).empty? || !settings.fetch(:only, []).empty?
73
+ name = "partial"
74
+ settings[:date_format] = '%Y-%m-%d-%H-%M'
75
+ else
76
+ name = "full"
77
+ end
78
+ name += "_#{append}" if append
79
+ super(settings, name)
80
+ end
81
+
82
+ def pg_dump_command
83
+ cmd = "#{BIN}/pg_dump -F#{@backup_format} -U #{@pg_user}"
84
+ cmd += " -h #{@pg_host}" if @pg_host
85
+ cmd += " -p #{@pg_port}" if @pg_port
86
+ cmd
87
+ end
88
+
89
+ def custom?
90
+ @backup_format == 'custom'
91
+ end
92
+
93
+ def dump_schema(working_dir)
94
+ _dump(working_dir, '--schema-only', '-schema')
95
+ end
96
+
97
+ def dump_data(working_dir, ignore = [])
98
+ ignore_string = custom? ? '' : '--data-only'
99
+ ignore.each {|i| ignore_string += ' -T '+i} if ignore.size > 0
100
+ _dump(working_dir, ignore_string)
101
+ end
102
+
103
+ def dump_data_only(working_dir, only = [])
104
+ only_string = custom? ? '' : '--data-only'
105
+ only.each {|o| only_string += ' -t '+o} if only.size > 0
106
+ label = "-table-" + only.join('-')
107
+ _dump(working_dir, only_string, label)
108
+ end
109
+
110
+ def _dump(working_dir, append_string = '', label='')
111
+ ext = custom? ? 'dump' : 'sql'
112
+ dst = File.join(working_dir, "#{@db_name}#{label}.#{ext}.gz")
113
+ log_and_time("dumping data (format #{@backup_format}) to #{dst}") do
114
+ cmd = "#{pg_dump_command} #{append_string} #{@db_name} | gzip > #{dst}"
115
+ @last_dst = dst
116
+ `#{cmd}`
117
+ end
118
+ File.exists?(dst)
119
+ end
120
+
121
+ def perform_dump(settings, working_dir)
122
+ # if the backup format is custom, do a full dump since we can do a bunch
123
+ # of stuff with pg_restore, which means we don't need the individual dumps
124
+ ignore = settings[:ignore] || []
125
+ only = settings[:only]
126
+ if custom?
127
+ if !only.nil? && !only.empty?
128
+ return dump_data_only(working_dir, only)
129
+ else
130
+ return dump_data(working_dir, ignore)
131
+ end
132
+ end
133
+ if !settings[:skip_schema]
134
+ if !dump_schema(working_dir)
135
+ log("failed to dump schema")
136
+ false
137
+ end
138
+ else
139
+ log("skipping schema dump")
140
+ end
141
+ separate = settings[:separate]
142
+ if separate
143
+ # first dump the database without any of the separate tables
144
+ log("dumping database without separate tables")
145
+ if !dump_data(working_dir, separate + ignore)
146
+ false
147
+ end
148
+ separate.each do |table|
149
+ log("dumping data only for table #{table}")
150
+ if !dump_data_only(working_dir, [table])
151
+ log("failed to dump individual table #{table}")
152
+ false
153
+ end
154
+ end
155
+ true
156
+ elsif !only.nil? && !only.empty?
157
+ dump_data_only(working_dir, only)
158
+ else
159
+ dump_data(working_dir, ignore)
160
+ end
161
+ end
162
+
163
+ def select_cmd(table, where, label)
164
+ "select * from #{table} where #{where} ORDER BY #{label} ASC"
165
+ end
166
+
167
+ def psql_dump_cmd(cmd, dump_file)
168
+ cmd = "#{BIN}/psql -c \"#{cmd}\" -F '"
169
+ cmd += 9.chr
170
+ cmd += "' -A -d #{@db_name} -U #{@pg_user} -t | gzip > #{dump_file}"
171
+ cmd
172
+ end
173
+
174
+ def dump_table_by_id(table, id, working_dir)
175
+ where = "id > #{id}"
176
+ cmd = select_cmd(table, where, 'id')
177
+ dump_file = File.join(working_dir, "dump_#{table}_#{id}.sql.gz")
178
+ dump_cmd = psql_dump_cmd(cmd, dump_file)
179
+ log("dumping table by id to #{dump_file}")
180
+ `#{dump_cmd}`
181
+ ret = $?
182
+ if !File.exists?(dump_file)
183
+ log("could not create dump #{dump_file}")
184
+ false
185
+ end
186
+ true
187
+ end
188
+
189
+ def determine_label(table)
190
+ if table =~ /hour/
191
+ 'hour'
192
+ elsif table =~ /month/
193
+ 'month'
194
+ else
195
+ 'day'
196
+ end
197
+ end
198
+
199
+ def dump_daily_data(table, day, dump_dir)
200
+ format = '%m-%d-%Y'
201
+ start_str = day.strftime(format)
202
+ # determine the label by the table
203
+ label = determine_label(table)
204
+ end_str = (day + 1.day).strftime(format)
205
+ cmd = select_cmd(table, "#{label} >= '#{start_str}' AND #{label} < '#{end_str}'", label)
206
+ dump_name = "#{start_str}.sql.gz"
207
+ dump_file = File.join(dump_dir, dump_name)
208
+ # if the file exists, skip it as it appears to be already dumped
209
+ if File.exists?(dump_file)
210
+ log("dump file #{dump_file} already created, skipping")
211
+ return false
212
+ end
213
+ log("dumping stats to #{dump_file}")
214
+ dump_cmd = psql_dump_cmd(cmd, dump_file)
215
+ out = `#{dump_cmd}`
216
+ ret = $?
217
+ if !File.exists?(dump_file)
218
+ log("could not create dump #{dump_file}")
219
+ false
220
+ end
221
+ true
222
+ end
223
+
224
+ def dump_monthly_data(table, end_date = nil)
225
+ # the sql directory structure will look like this
226
+ # BACKUP_DIR
227
+ # -> monthly-12-2009
228
+ # -> hits_by_hour_video_geo
229
+ # -> 12-1.2009.sql.gz
230
+ # -> 12-2.2009.sql.gz
231
+ # -> 12-3.2009.sql.gz
232
+ # then each directory under the monthly will be tarballed,
233
+ # and uploaded to s3 with the name monthly-12-2009-hits_by_hour_video_geo.tar.gz
234
+ end_date = Time.now if end_date.blank?
235
+ # normalize to beginning of month
236
+ end_date = Time.utc(end_date.year, end_date.month, 1)
237
+ format = '%m-%Y'
238
+ start_day = (end_date - 1.month)
239
+ # normalize to the beginning of month
240
+ start_day = Time.utc(start_day.year, start_day.month, 1)
241
+ start_str = start_day.strftime(format)
242
+ table_dir = File.join(@backup_dir, "monthly-#{start_str}", table)
243
+ FileUtils.makedirs(table_dir)
244
+ # now iterate through all the days up till the end_date, and dump each file
245
+ while start_day < end_date
246
+ dump_daily_data(table, start_day, table_dir)
247
+ start_day += 1.day
248
+ end
249
+ # once we have the dir, tar ball the entire table dir
250
+ tar_ball = File.join(@backup_dir, "monthly-#{start_str}-#{table}.tar.gz")
251
+ `tar czvf #{tar_ball} #{table_dir}`
252
+ if File.exists?(tar_ball)
253
+ send_file_to_s3(tar_ball)
254
+ # now delete the tarball
255
+ File.delete(tar_ball)
256
+ else
257
+ log("tar ball does not exist #{tar_ball}")
258
+ end
259
+ end
260
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backup_utility
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Bruce Wang
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-11-02 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Create database dumps and store them in a remote location
22
+ email: bwang@twistage.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/backup_utility.rb
31
+ - lib/backup_utility/base.rb
32
+ - lib/backup_utility/file.rb
33
+ - lib/backup_utility/mongo.rb
34
+ - lib/backup_utility/postgres.rb
35
+ has_rdoc: true
36
+ homepage: http://rubygems.org/gems/backup_utility
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.7
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Backup Utility
67
+ test_files: []
68
+