sauberia-db2s3 0.3.2.1
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.
- data/.gitignore +2 -0
- data/HISTORY +15 -0
- data/README.rdoc +40 -0
- data/Rakefile +4 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/db2s3/tasks.rb +1 -0
- data/lib/db2s3.rb +177 -0
- data/rails/init.rb +1 -0
- data/spec/db2s3_spec.rb +26 -0
- data/spec/mysql_drop_schema.sql +1 -0
- data/spec/mysql_schema.sql +4 -0
- data/spec/s3_config.example.rb +5 -0
- data/spec/spec_helper.rb +24 -0
- data/tasks/tasks.rake +26 -0
- metadata +99 -0
data/.gitignore
ADDED
data/HISTORY
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
0.3.1 (7 Dec 2009)
|
2
|
+
- Fixed hardcoded DB name in statistics task
|
3
|
+
0.3.0 (6 Dec 2009)
|
4
|
+
- Added db2s3:backup:clean task to delete old backups
|
5
|
+
- Added dependency on activesupport for the clean task
|
6
|
+
0.2.6 (6 Dec 2009)
|
7
|
+
- Remove metrics task, since it was far too inaccurate
|
8
|
+
- Add statistics task to show you the size of your tables
|
9
|
+
- Only add username to mysql command line if provided in database.yml
|
10
|
+
0.2.5
|
11
|
+
- Use host provided in database.yml
|
12
|
+
0.2.4 (2 Sep 2009)
|
13
|
+
- Fix credentials bug
|
14
|
+
0.2.3
|
15
|
+
- Keep old backups around
|
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= DB2S3
|
2
|
+
|
3
|
+
A rails plugin to backup Mysql to Amazon S3. You're looking at a monthly spend of four cents. So pony up you cheap bastard, and store your backups on S3
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
# In config/environment.rb
|
8
|
+
config.gem "db2s3", :source => "http://gemcutter.org"
|
9
|
+
|
10
|
+
# In Rakefile
|
11
|
+
require 'db2s3/tasks'
|
12
|
+
|
13
|
+
# In config/initializers/db2s3.rb
|
14
|
+
DB2S3::Config.instance_eval do
|
15
|
+
S3 = {
|
16
|
+
:access_key_id => 'yourkey',
|
17
|
+
:secret_access_key => 'yoursecretkey',
|
18
|
+
:bucket => 'yourapp-db-backup'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
# DB credentials are read from your rails environment
|
22
|
+
|
23
|
+
rake gems:install
|
24
|
+
|
25
|
+
# Add to your crontab or whatever
|
26
|
+
rake db2s3:backup:full
|
27
|
+
rake db2s3:backup:incremental # Unimplemented
|
28
|
+
|
29
|
+
# Handy tasks
|
30
|
+
rake db2s3:statistics # Shows you the size of your DB
|
31
|
+
rake db2s3:backup:restore # You should be testing this regularly
|
32
|
+
rake db2s3:backup:clean # Clean up old backups - cron this
|
33
|
+
|
34
|
+
== Development
|
35
|
+
|
36
|
+
Specs are really week. This code is bit hackish but is being used by quite a few people.
|
37
|
+
|
38
|
+
== Kudos
|
39
|
+
|
40
|
+
http://github.com/pauldowman/blog_code_examples/tree/master/mysql_s3_backup
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.2
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
data/lib/db2s3/tasks.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/../../tasks/*.rake"].each { |ext| load ext }
|
data/lib/db2s3.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_support' # The new one
|
3
|
+
rescue LoadError
|
4
|
+
require 'activesupport' # The old one
|
5
|
+
end
|
6
|
+
require 's3'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
class DB2S3
|
10
|
+
class Config
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
def full_backup
|
17
|
+
file_name = dump_file_name(Time.now)
|
18
|
+
store.store(file_name, open(dump_db.path))
|
19
|
+
store.store(most_recent_dump_file_name, file_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def restore
|
23
|
+
dump_file_name = store.fetch(most_recent_dump_file_name).read
|
24
|
+
file = store.fetch(dump_file_name)
|
25
|
+
run "gunzip -c #{file.path} | mysql #{mysql_options}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: This method really needs specs
|
29
|
+
def clean
|
30
|
+
files = file_objects_from_paths(store.list("#{dump_file_name_prefix}-"))
|
31
|
+
determine_what_to_keep files
|
32
|
+
delete_surplus_backups files
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_objects_from_paths paths
|
36
|
+
paths.collect do |path| {
|
37
|
+
:path => path,
|
38
|
+
:date => Time.parse(path.split('-').last.split('.').first),
|
39
|
+
:keep => false
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def determine_what_to_keep files
|
45
|
+
# Keep all backups from the past day
|
46
|
+
files.select {|x| x[:date] >= 1.day.ago }.map! do |backup_for_day|
|
47
|
+
backup_for_day[:keep] = true
|
48
|
+
end
|
49
|
+
# Keep one backup per day from the last week
|
50
|
+
files.select {|x| x[:date] >= 1.week.ago }.group_by {|x| x[:date].strftime("%u") }.values.map! do |backups_for_last_week|
|
51
|
+
backups_for_last_week.sort_by{|x| x[:path] }.first[:keep] = true
|
52
|
+
end
|
53
|
+
# Keep one backup per week from the last 28 days
|
54
|
+
files.select {|x| x[:date] >= 28.days.ago }.group_by {|x| x[:date].strftime("%Y%W") }.values.map! do |backups_for_last_28_days|
|
55
|
+
backups_for_last_28_days.sort_by{|x| x[:path] }.first[:keep] = true
|
56
|
+
end
|
57
|
+
# Keep one backup per month since forever
|
58
|
+
files.group_by {|x| x[:date].strftime("%Y%m") }.values.map! do |backups_for_month|
|
59
|
+
backups_for_month.sort_by{|x| x[:path] }.first[:keep] = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete_surplus_backups files
|
64
|
+
files.each do |file|
|
65
|
+
store.delete(file[:path]) unless file[:keep]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def statistics
|
70
|
+
# From http://mysqlpreacher.com/wordpress/tag/table-size/
|
71
|
+
results = ActiveRecord::Base.connection.execute(<<-EOS)
|
72
|
+
SELECT
|
73
|
+
engine,
|
74
|
+
ROUND(data_length/1024/1024,2) total_size_mb,
|
75
|
+
ROUND(index_length/1024/1024,2) total_index_size_mb,
|
76
|
+
table_rows,
|
77
|
+
table_name article_attachment
|
78
|
+
FROM information_schema.tables
|
79
|
+
WHERE table_schema = '#{db_credentials[:database]}'
|
80
|
+
ORDER BY total_size_mb + total_index_size_mb desc;
|
81
|
+
EOS
|
82
|
+
rows = []
|
83
|
+
results.each {|x| rows << x.to_a }
|
84
|
+
rows
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def dump_db
|
90
|
+
dump_file = Tempfile.new('dump')
|
91
|
+
cmd = "mysqldump --quick --single-transaction --create-options #{mysql_options}"
|
92
|
+
cmd += " | gzip > #{dump_file.path}"
|
93
|
+
run(cmd)
|
94
|
+
|
95
|
+
dump_file
|
96
|
+
end
|
97
|
+
|
98
|
+
def mysql_options
|
99
|
+
cmd = ''
|
100
|
+
cmd += " -u #{db_credentials[:username]} " unless db_credentials[:username].nil?
|
101
|
+
cmd += " -p'#{db_credentials[:password]}'" unless db_credentials[:password].nil?
|
102
|
+
cmd += " -h '#{db_credentials[:host]}'" unless db_credentials[:host].nil?
|
103
|
+
cmd += " #{db_credentials[:database]}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def store
|
107
|
+
@store ||= S3Store.new
|
108
|
+
end
|
109
|
+
|
110
|
+
def dump_file_name_prefix
|
111
|
+
"dump-#{db_credentials[:database]}"
|
112
|
+
end
|
113
|
+
|
114
|
+
def dump_file_name time
|
115
|
+
"#{dump_file_name_prefix}-#{time.utc.strftime("%Y%m%d%H%M%S")}.sql.gz"
|
116
|
+
end
|
117
|
+
|
118
|
+
def most_recent_dump_file_name
|
119
|
+
"most-recent-#{dump_file_name_prefix}.txt"
|
120
|
+
end
|
121
|
+
|
122
|
+
def run(command)
|
123
|
+
result = system(command)
|
124
|
+
raise("error, process exited with status #{$?.exitstatus}") unless result
|
125
|
+
end
|
126
|
+
|
127
|
+
def db_credentials
|
128
|
+
ActiveRecord::Base.connection.instance_eval { @config } # Dodgy!
|
129
|
+
end
|
130
|
+
|
131
|
+
class S3Store
|
132
|
+
def initialize
|
133
|
+
@connected = false
|
134
|
+
end
|
135
|
+
|
136
|
+
def ensure_connected
|
137
|
+
return if @connected
|
138
|
+
s3_service = S3::Service.new(DB2S3::Config::S3.slice(:access_key_id, :secret_access_key).merge(:use_ssl => true))
|
139
|
+
@bucket = s3_service.buckets.build(DB2S3::Config::S3[:bucket])
|
140
|
+
@connected = true
|
141
|
+
end
|
142
|
+
|
143
|
+
def store(file_name, file)
|
144
|
+
ensure_connected
|
145
|
+
object = bucket.objects.build(file_name)
|
146
|
+
object.content = file.class == String ? file : (file.rewind; file.read)
|
147
|
+
object.save
|
148
|
+
end
|
149
|
+
|
150
|
+
def fetch(file_name)
|
151
|
+
ensure_connected
|
152
|
+
file = Tempfile.new('dump')
|
153
|
+
file.binmode if file.respond_to?(:binmode)
|
154
|
+
file.write(bucket.objects.find(file_name).content)
|
155
|
+
file.rewind
|
156
|
+
file
|
157
|
+
end
|
158
|
+
|
159
|
+
def list(prefix)
|
160
|
+
ensure_connected
|
161
|
+
bucket.objects.find_all(:prefix => prefix).collect {|x| x.key }
|
162
|
+
end
|
163
|
+
|
164
|
+
def delete(file_name)
|
165
|
+
if object = bucket.objects.find(file_name)
|
166
|
+
object.delete
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def bucket
|
173
|
+
@bucket
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/db2s3')
|
data/spec/db2s3_spec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe 'db2s3' do
|
4
|
+
def load_schema
|
5
|
+
`cat '#{File.dirname(__FILE__) + '/mysql_schema.sql'}' | mysql -u #{DBConfig[:username]} #{DBConfig[:database]}`
|
6
|
+
end
|
7
|
+
|
8
|
+
def drop_schema
|
9
|
+
`cat '#{File.dirname(__FILE__) + '/mysql_drop_schema.sql'}' | mysql -u #{DBConfig[:username]} #{DBConfig[:database]}`
|
10
|
+
end
|
11
|
+
|
12
|
+
class Person < ActiveRecord::Base
|
13
|
+
end
|
14
|
+
|
15
|
+
if DB2S3::Config.const_defined?('S3')
|
16
|
+
it 'can save and restore a backup to S3' do
|
17
|
+
db2s3 = DB2S3.new
|
18
|
+
load_schema
|
19
|
+
Person.create!(:name => "Baxter")
|
20
|
+
db2s3.full_backup
|
21
|
+
drop_schema
|
22
|
+
db2s3.restore
|
23
|
+
Person.find_by_name("Baxter").should_not be_nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
DROP TABLE IF EXISTS people;
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'active_record' # The new one
|
5
|
+
rescue LoadError
|
6
|
+
require 'activerecord' # The old one
|
7
|
+
end
|
8
|
+
|
9
|
+
require File.dirname(__FILE__) + '/../lib/db2s3'
|
10
|
+
if File.exists?(File.dirname(__FILE__) + '/s3_config.rb')
|
11
|
+
require File.dirname(__FILE__) + '/s3_config.rb'
|
12
|
+
else
|
13
|
+
puts "s3_config.rb does not exist - not running live tests"
|
14
|
+
end
|
15
|
+
|
16
|
+
DBConfig = {
|
17
|
+
:adapter => "mysql",
|
18
|
+
:encoding => "utf8",
|
19
|
+
:database => 'db2s3_unittest',
|
20
|
+
:username => "root"
|
21
|
+
}
|
22
|
+
|
23
|
+
ActiveRecord::Base.configurations = { 'production' => DBConfig }
|
24
|
+
ActiveRecord::Base.establish_connection(:production)
|
data/tasks/tasks.rake
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
namespace :db2s3 do
|
2
|
+
namespace :backup do
|
3
|
+
desc "Save a full back to S3"
|
4
|
+
task :full => :environment do
|
5
|
+
DB2S3.new.full_backup
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Restore your DB from S3"
|
9
|
+
task :restore => :environment do
|
10
|
+
DB2S3.new.restore
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Keep all backups for the last day, one per day for the last week, one per week for the last 28 days, and one per month before that. Delete the rest."
|
14
|
+
task :clean => :environment do
|
15
|
+
DB2S3.new.clean
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Show table sizes for your database"
|
20
|
+
task :statistics => :environment do
|
21
|
+
rows = DB2S3.new.statistics
|
22
|
+
rows.sort_by {|x| -x[3].to_i }
|
23
|
+
header = [["Type", "Data MB", "Index", "Rows", "Name"], []]
|
24
|
+
puts (header + rows).collect {|x| x.join("\t") }
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sauberia-db2s3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 93
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 2
|
10
|
+
- 1
|
11
|
+
version: 0.3.2.1
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- Xavier Shay
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-10-22 00:00:00 +01:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: s3
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 29
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
- 3
|
34
|
+
- 7
|
35
|
+
version: 0.3.7
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
description: db2s3 provides rake tasks for backing up and restoring your DB to S3
|
39
|
+
email: contact@rhnh.net
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- README.rdoc
|
46
|
+
files:
|
47
|
+
- .gitignore
|
48
|
+
- HISTORY
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- VERSION
|
52
|
+
- init.rb
|
53
|
+
- lib/db2s3.rb
|
54
|
+
- lib/db2s3/tasks.rb
|
55
|
+
- rails/init.rb
|
56
|
+
- spec/db2s3_spec.rb
|
57
|
+
- spec/mysql_drop_schema.sql
|
58
|
+
- spec/mysql_schema.sql
|
59
|
+
- spec/s3_config.example.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
- tasks/tasks.rake
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/xaviershay/db2s3
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --charset=UTF-8
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.3.7
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Summarize your gem
|
96
|
+
test_files:
|
97
|
+
- spec/db2s3_spec.rb
|
98
|
+
- spec/s3_config.example.rb
|
99
|
+
- spec/spec_helper.rb
|