backup_utility 1.0.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,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
+