mysql_truck 0.1.1 → 0.2.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.
- data/TODO +1 -4
- data/bin/mysql_truck +6 -1
- data/lib/mysql_truck/dumper.rb +175 -0
- data/lib/mysql_truck/helper.rb +63 -0
- data/lib/mysql_truck/loader.rb +179 -0
- data/lib/mysql_truck/version.rb +1 -1
- data/lib/mysql_truck.rb +8 -269
- metadata +7 -4
data/TODO
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
# TODO
|
2
2
|
|
3
|
-
* Download/extract/import one table at a time.
|
4
|
-
* Rework backup so that table schema and indexes are separated.
|
5
|
-
* Rework restore so that schema is applied, data is imported, then indexes are
|
6
|
-
applied.
|
7
3
|
* Add ability to manage number of backups on S3
|
8
4
|
* Better error handling messages rather than stack dumps
|
5
|
+
* Add ability to import a table as a tmp table, then rename.
|
data/bin/mysql_truck
CHANGED
@@ -58,7 +58,12 @@ parser = OptionParser.new do |opts|
|
|
58
58
|
|
59
59
|
opts.on("-t", "--skip-tables TABLES",
|
60
60
|
"List of tables to skip separated by commas.") do |tables|
|
61
|
-
options[:
|
61
|
+
options[:skip_data_for_tables] = tables.split(",")
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on("-e", "--exec-smartly",
|
65
|
+
"On dumping, do not dump tables that have already been dumped. On loading, if the files were already downloaded, do not redownload. This option allows for resuming a previous load/dump that failed.") do
|
66
|
+
options[:smartly] = true
|
62
67
|
end
|
63
68
|
|
64
69
|
opts.on_tail("-h", "--help", "Show this message") do
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module MysqlTruck
|
2
|
+
class Dumper
|
3
|
+
include FileUtils
|
4
|
+
include Helper
|
5
|
+
|
6
|
+
REGEX = /,?\s*(UNIQUE)?\s*KEY\s`[A-Za-z\d_]+`\s*\([A-Za-z\d_,`]+\),?\s*/m
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
@time = Time.now # Sets the directory for dump
|
11
|
+
|
12
|
+
initialize_s3
|
13
|
+
initialize_directories
|
14
|
+
end
|
15
|
+
|
16
|
+
def dump
|
17
|
+
dump_data
|
18
|
+
upload
|
19
|
+
remove_directories
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump_data
|
23
|
+
tables.each do |table|
|
24
|
+
puts "Dumping #{table}..."
|
25
|
+
next if gzip_files_exist?(table) && smartly?
|
26
|
+
|
27
|
+
if dump_table?(table)
|
28
|
+
|
29
|
+
# This command creates a table_name.sql and a table_name.txt file
|
30
|
+
cmd = "mysqldump --quick -T #{tmp_path} "
|
31
|
+
cmd += csv_options
|
32
|
+
cmd += "#{db_connection_options} #{table}"
|
33
|
+
puts cmd
|
34
|
+
`#{cmd}`
|
35
|
+
|
36
|
+
# `mysqldump` creates files with .txt extensions, so we rename it.
|
37
|
+
mv filename(table)[:txt_file], filename(table)[:csv_file]
|
38
|
+
end
|
39
|
+
|
40
|
+
if split_schema_file?(table)
|
41
|
+
schema_contents = filename(table)[:schema_file].read
|
42
|
+
|
43
|
+
# Create schema with no indexes
|
44
|
+
File.open(filename(table)[:no_index_sql_file], 'w') do |f|
|
45
|
+
f.write(schema_contents.gsub(REGEX, ''))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create an alter table
|
49
|
+
indices = []
|
50
|
+
File.open(filename(table)[:index_sql_file], 'w') do |f|
|
51
|
+
f.write("ALTER TABLE #{table}\n")
|
52
|
+
|
53
|
+
schema_contents.gsub(/^,?\s*((UNIQUE)?\s*KEY\s`[A-Za-z\d_]+`\s*\([A-Za-z\d_,`]+\)),?\s*$/) do |part|
|
54
|
+
indices << $1
|
55
|
+
end
|
56
|
+
f.write(indices.collect {|i| "ADD #{i}"}.join(",\n"))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if gzip_files?(table)
|
61
|
+
puts "gzipping #{filename(table)[:no_index_sql_file]}."
|
62
|
+
`gzip #{filename(table)[:no_index_sql_file]}`
|
63
|
+
|
64
|
+
puts "gzipping #{filename(table)[:index_sql_file]}."
|
65
|
+
`gzip #{filename(table)[:index_sql_file]}`
|
66
|
+
|
67
|
+
puts "gziping #{filename(table)[:csv_file]}."
|
68
|
+
`gzip #{filename(table)[:csv_file]}`
|
69
|
+
end
|
70
|
+
|
71
|
+
puts "#{table} dumped.\n\n"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def upload
|
76
|
+
Dir["#{tmp_path}/*"].each do |file|
|
77
|
+
next if File.extname(file) != ".gz"
|
78
|
+
puts "Uploading #{file} ..."
|
79
|
+
upload_file file
|
80
|
+
end
|
81
|
+
puts "Finished uploading backups."
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def smartly?
|
87
|
+
config[:smartly]
|
88
|
+
end
|
89
|
+
|
90
|
+
def upload_file(local_file)
|
91
|
+
path = Pathname.new(local_file)
|
92
|
+
s3_path = bucket_path.join(path.basename)
|
93
|
+
@bucket.put(s3_path, open(path), {}, nil, {
|
94
|
+
'x-amz-storage-class' => 'REDUCED_REDUNDANCY'
|
95
|
+
})
|
96
|
+
end
|
97
|
+
|
98
|
+
def tables
|
99
|
+
return config[:only_tables] if config[:only_tables]
|
100
|
+
unless @tables
|
101
|
+
res = `mysql #{db_connection_options} -e "SHOW TABLES"`
|
102
|
+
@tables = res.split[1..-1]
|
103
|
+
end
|
104
|
+
@tables
|
105
|
+
end
|
106
|
+
|
107
|
+
def bucket_path
|
108
|
+
@bucket_path ||= Pathname.new(bucket_dir).join(@time.strftime("%Y-%m-%d-%H-%M"))
|
109
|
+
end
|
110
|
+
|
111
|
+
def filename(table)
|
112
|
+
@table_filenames ||= {}
|
113
|
+
@table_filenames[table] ||= {
|
114
|
+
:schema_file => tmp_path.join("#{table}.sql"),
|
115
|
+
:no_index_sql_file => tmp_path.join("#{table}.no_index.sql"),
|
116
|
+
:index_sql_file => tmp_path.join("#{table}.indices.sql"),
|
117
|
+
:txt_file => tmp_path.join("#{table}.txt"),
|
118
|
+
:csv_file => tmp_path.join("#{table}.csv"),
|
119
|
+
:gz_no_index_sql_file => tmp_path.join("#{table}.no_index.sql.gz"),
|
120
|
+
:gz_index_sql_file => tmp_path.join("#{table}.indices.sql.gz"),
|
121
|
+
:gz_csv_file => tmp_path.join("#{table}.csv.gz"),
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def gzip_files_exist?(table)
|
126
|
+
tmp_path.join("#{table}.sql.gz").file? &&
|
127
|
+
tmp_path.join("#{table}.no_index.sql.gz") &&
|
128
|
+
tmp_path.join("#{table}.indexes.sql.gz") &&
|
129
|
+
tmp_path.join("#{table}.csv.gz")
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def dump_table?(table)
|
134
|
+
!smartly? ||
|
135
|
+
(
|
136
|
+
smartly? &&
|
137
|
+
!filename(table)[:schema_file].exist? &&
|
138
|
+
!filename(table)[:csv_file].exist? &&
|
139
|
+
!filename(table)[:no_index_sql_file].exist? &&
|
140
|
+
!filename(table)[:index_sql_file].exist? &&
|
141
|
+
!filename(table)[:gz_no_index_sql_file].exist? &&
|
142
|
+
!filename(table)[:gz_index_sql_file].exist? &&
|
143
|
+
!filename(table)[:gz_csv_file].exist?
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
def split_schema_file?(table)
|
148
|
+
!smartly? ||
|
149
|
+
(
|
150
|
+
smartly? &&
|
151
|
+
filename(table)[:schema_file].exist? &&
|
152
|
+
filename(table)[:csv_file].exist? &&
|
153
|
+
!filename(table)[:no_index_sql_file].exist? &&
|
154
|
+
!filename(table)[:index_sql_file].exist? &&
|
155
|
+
!filename(table)[:gz_no_index_sql_file].exist? &&
|
156
|
+
!filename(table)[:gz_index_sql_file].exist? &&
|
157
|
+
!filename(table)[:gz_csv_file].exist?
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def gzip_files?(table)
|
162
|
+
!smartly? ||
|
163
|
+
(
|
164
|
+
smartly? &&
|
165
|
+
filename(table)[:schema_file].exist? &&
|
166
|
+
filename(table)[:csv_file].exist? &&
|
167
|
+
filename(table)[:no_index_sql_file].exist? &&
|
168
|
+
filename(table)[:index_sql_file].exist? &&
|
169
|
+
!filename(table)[:gz_no_index_sql_file].exist? &&
|
170
|
+
!filename(table)[:gz_index_sql_file].exist? &&
|
171
|
+
!filename(table)[:gz_csv_file].exist?
|
172
|
+
)
|
173
|
+
end
|
174
|
+
end # class Dumper
|
175
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module MysqlTruck
|
2
|
+
module Helper
|
3
|
+
include FileUtils
|
4
|
+
|
5
|
+
def config
|
6
|
+
@config
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize_s3
|
10
|
+
@s3 = RightAws::S3.new(
|
11
|
+
config[:s3_access_key],
|
12
|
+
config[:s3_secret_access_key])
|
13
|
+
@bucket = @s3.bucket(config[:bucket])
|
14
|
+
end
|
15
|
+
|
16
|
+
def db_connection_options
|
17
|
+
opts = %Q[ -u #{config[:username]} ]
|
18
|
+
opts += %Q[ -p"#{config[:password]}" ] unless config[:password].nil?
|
19
|
+
opts += %Q[ -h #{config[:host]} --default-character-set=utf8 ]
|
20
|
+
opts += %Q[ #{config[:database]} ]
|
21
|
+
opts
|
22
|
+
end
|
23
|
+
|
24
|
+
def local_host?
|
25
|
+
config[:host] == '127.0.0.1' || config[:host] == 'localhost'
|
26
|
+
end
|
27
|
+
|
28
|
+
def remote_host?
|
29
|
+
!local_host?
|
30
|
+
end
|
31
|
+
|
32
|
+
def csv_options
|
33
|
+
" --fields-enclosed-by=\\\" --fields-terminated-by=, "
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize_directories
|
37
|
+
mkdir_p base_path
|
38
|
+
mkdir_p tmp_path
|
39
|
+
chmod 0777, tmp_path
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_directories
|
43
|
+
rm_r tmp_path, :force => true
|
44
|
+
end
|
45
|
+
|
46
|
+
def tmp_path
|
47
|
+
raise "@time not initialized" unless @time
|
48
|
+
base_path.join(@time.strftime("%Y-%m-%d"))
|
49
|
+
end
|
50
|
+
|
51
|
+
def base_path
|
52
|
+
if config[:dump_dir]
|
53
|
+
config[:dump_dir].is_a?(Pathname) ? config[:dump_dir].join("mysqltruck") : Pathname.new(config[:dump_dir]).join("mysqltruck")
|
54
|
+
else
|
55
|
+
Pathname.new("/tmp/mysqltruck")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def bucket_dir
|
60
|
+
"mysql/#{config[:bucket_dir] || config[:database]}/"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module MysqlTruck
|
2
|
+
class Loader
|
3
|
+
include Helper
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
initialize_s3
|
9
|
+
end
|
10
|
+
|
11
|
+
# only import schema for these tables
|
12
|
+
def skip_data_for_tables
|
13
|
+
config[:skip_data_for_tables] || []
|
14
|
+
end
|
15
|
+
|
16
|
+
# only import these tables schema+data
|
17
|
+
def only_tables
|
18
|
+
config[:only_tables] || []
|
19
|
+
end
|
20
|
+
|
21
|
+
def load_latest
|
22
|
+
prefix = backups.first
|
23
|
+
|
24
|
+
# Set directory where backup is downloaded to
|
25
|
+
@time = Time.new(*prefix.split("/").last.split("-"))
|
26
|
+
initialize_directories
|
27
|
+
|
28
|
+
puts "Downloading backups"
|
29
|
+
puts "-------------------"
|
30
|
+
@bucket.keys(:prefix => prefix).each do |key|
|
31
|
+
puts "\n#{key}"
|
32
|
+
next unless (filename = download_file(key))
|
33
|
+
|
34
|
+
# gunzip file
|
35
|
+
if tmp_path.join(filename).exist?
|
36
|
+
print " - Inflating #{filename} ... "
|
37
|
+
`gunzip -f #{tmp_path.join(filename)}`
|
38
|
+
print "complete.\n"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Load data
|
43
|
+
puts "\nLoading schema and data by table"
|
44
|
+
puts "--------------------------------"
|
45
|
+
if remote_host?
|
46
|
+
import_cmd = "mysqlimport --local --compress #{db_connection_options}"
|
47
|
+
else
|
48
|
+
import_cmd = "mysqlimport #{db_connection_options}"
|
49
|
+
end
|
50
|
+
import_cmd += csv_options
|
51
|
+
|
52
|
+
# Find all .no_index.sql files and process
|
53
|
+
Dir["#{tmp_path}/*.no_index.sql"].each do |file|
|
54
|
+
table = File.basename(file, ".no_index.sql")
|
55
|
+
puts "\nProcessing #{table}"
|
56
|
+
|
57
|
+
schema_file = Pathname.new(file)
|
58
|
+
index_file = tmp_path.join("#{table}.indices.sql")
|
59
|
+
csv_file = tmp_path.join("#{table}.csv")
|
60
|
+
|
61
|
+
|
62
|
+
print " - Loading schema for #{table} ... "
|
63
|
+
cmd = "cat #{schema_file} | mysql #{db_connection_options}"
|
64
|
+
`#{cmd}`
|
65
|
+
print "complete.\n"
|
66
|
+
|
67
|
+
if csv_file.exist?
|
68
|
+
print " - Importing #{schema_file.basename(".sql")} ... "
|
69
|
+
`#{import_cmd} #{csv_file}`
|
70
|
+
print "complete.\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
if index_file.exist?
|
74
|
+
print " - Adding indices for #{schema_file.basename(".no_index.sql")} ... "
|
75
|
+
cmd = "cat #{index_file} | mysql #{db_connection_options}"
|
76
|
+
`#{cmd}`
|
77
|
+
print "complete.\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
schema_file.delete if schema_file.exist?
|
81
|
+
index_file.delete if index_file.exist?
|
82
|
+
csv_file.delete if csv_file.exist?
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "Backup loaded."
|
86
|
+
|
87
|
+
# This isn't in an ensure block because we want to keep around
|
88
|
+
# downloads if there's a failure importing a table.
|
89
|
+
# remove_directories
|
90
|
+
|
91
|
+
rescue Exception => e
|
92
|
+
puts e.message
|
93
|
+
puts e.backtrace.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
def download_file(key)
|
97
|
+
filename = File.basename(key.name)
|
98
|
+
|
99
|
+
unless should_download_file?(filename)
|
100
|
+
puts " [ SKIP ]"
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
file = tmp_path.join(filename)
|
105
|
+
unzipped_file = tmp_path.join(file.basename(".gz"))
|
106
|
+
if !smartly? || (smartly? && !unzipped_file.exist?)
|
107
|
+
print " - Downloading... "
|
108
|
+
|
109
|
+
file.open("wb") do |f|
|
110
|
+
@bucket.s3.interface.get(@bucket.name, key.name) do |chunk|
|
111
|
+
f.write chunk
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
puts "complete."
|
116
|
+
else
|
117
|
+
puts " already downloaded."
|
118
|
+
end
|
119
|
+
|
120
|
+
filename
|
121
|
+
end
|
122
|
+
|
123
|
+
def should_download_file?(filename)
|
124
|
+
table_name = filename.gsub(/\..*\..*$/, '')
|
125
|
+
|
126
|
+
if only_tables.empty? and skip_data_for_tables.empty?
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
|
130
|
+
# If we're targetting specific tables, then we always want both
|
131
|
+
# schema and csv files.
|
132
|
+
if !only_tables.empty?
|
133
|
+
return only_tables.include?(table_name)
|
134
|
+
end
|
135
|
+
|
136
|
+
if filename.match(/\.csv\.gz$/)
|
137
|
+
is_data = true
|
138
|
+
is_schema = false
|
139
|
+
else
|
140
|
+
is_data = false
|
141
|
+
is_schema = true
|
142
|
+
end
|
143
|
+
|
144
|
+
if !skip_data_for_tables.empty?
|
145
|
+
if is_schema or (is_data and !skip_data_for_tables.include?(table_name))
|
146
|
+
return true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
false
|
151
|
+
end
|
152
|
+
|
153
|
+
# Get a list of backups stored on S3.
|
154
|
+
#
|
155
|
+
# Returns an array of s3 paths that look like:
|
156
|
+
#
|
157
|
+
# mysql/YYYY-MM-DD
|
158
|
+
#
|
159
|
+
# Array elements are sorted with the latest date first.
|
160
|
+
def backups
|
161
|
+
unless @backups
|
162
|
+
@backups = []
|
163
|
+
# Backups are stored in the mysql/ directory
|
164
|
+
@bucket.s3.interface.incrementally_list_bucket(@bucket.name, {
|
165
|
+
:prefix => "#{bucket_dir}", :delimiter => "/"
|
166
|
+
}) do |item|
|
167
|
+
@backups += item[:common_prefixes]
|
168
|
+
end
|
169
|
+
@backups = @backups.sort { |a,b| b <=> a }
|
170
|
+
end
|
171
|
+
@backups
|
172
|
+
end
|
173
|
+
|
174
|
+
def smartly?
|
175
|
+
config[:smartly]
|
176
|
+
end
|
177
|
+
|
178
|
+
end # class Loader
|
179
|
+
end
|
data/lib/mysql_truck/version.rb
CHANGED
data/lib/mysql_truck.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
-
require "mysql_truck/version"
|
2
1
|
require "right_aws"
|
3
|
-
require
|
4
|
-
require
|
2
|
+
require "fileutils"
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
require "mysql_truck/version"
|
6
|
+
require "mysql_truck/helper"
|
7
|
+
require "mysql_truck/dumper"
|
8
|
+
require "mysql_truck/loader"
|
9
|
+
|
5
10
|
|
6
11
|
# MysqlTruck
|
7
12
|
#
|
@@ -21,270 +26,4 @@ module MysqlTruck
|
|
21
26
|
puts "Unknown action #{action}"
|
22
27
|
end
|
23
28
|
end
|
24
|
-
|
25
|
-
module Helper
|
26
|
-
include FileUtils
|
27
|
-
|
28
|
-
def config
|
29
|
-
@config
|
30
|
-
end
|
31
|
-
|
32
|
-
def initialize_s3
|
33
|
-
@s3 = RightAws::S3.new(
|
34
|
-
config[:s3_access_key],
|
35
|
-
config[:s3_secret_access_key])
|
36
|
-
@bucket = @s3.bucket(config[:bucket])
|
37
|
-
end
|
38
|
-
|
39
|
-
def db_connection_options
|
40
|
-
opts = %Q[ -u #{config[:username]} ]
|
41
|
-
opts += %Q[ -p"#{config[:password]}" ] unless config[:password].nil?
|
42
|
-
opts += %Q[ -h #{config[:host]} --default-character-set=utf8 ]
|
43
|
-
opts += %Q[ #{config[:database]} ]
|
44
|
-
opts
|
45
|
-
end
|
46
|
-
|
47
|
-
def csv_options
|
48
|
-
" --fields-enclosed-by=\\\" --fields-terminated-by=, "
|
49
|
-
end
|
50
|
-
|
51
|
-
def initialize_directories
|
52
|
-
mkdir_p base_path
|
53
|
-
mkdir_p tmp_path
|
54
|
-
chmod 0777, tmp_path
|
55
|
-
end
|
56
|
-
|
57
|
-
def remove_directories
|
58
|
-
rm_r tmp_path, :force => true
|
59
|
-
end
|
60
|
-
|
61
|
-
def tmp_path
|
62
|
-
raise "@time not initialized yet" unless @time
|
63
|
-
base_path.join(@time.strftime("%Y-%m-%d-%H-%M"))
|
64
|
-
end
|
65
|
-
|
66
|
-
def base_path
|
67
|
-
if config[:dump_dir]
|
68
|
-
config[:dump_dir].is_a?(Pathname) ? config[:dump_dir].join("mysqltruck") : Pathname.new(config[:dump_dir]).join("mysqltruck")
|
69
|
-
else
|
70
|
-
Pathname.new("/tmp/mysqltruck")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def bucket_dir
|
75
|
-
"mysql/#{config[:bucket_dir] || config[:database]}/"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
class Dumper
|
80
|
-
include FileUtils
|
81
|
-
include Helper
|
82
|
-
|
83
|
-
def initialize(config)
|
84
|
-
@config = config
|
85
|
-
@time = Time.now # Sets the directory for dump
|
86
|
-
|
87
|
-
initialize_s3
|
88
|
-
initialize_directories
|
89
|
-
end
|
90
|
-
|
91
|
-
def dump
|
92
|
-
dump_data
|
93
|
-
upload
|
94
|
-
|
95
|
-
ensure
|
96
|
-
remove_directories
|
97
|
-
end
|
98
|
-
|
99
|
-
def dump_data
|
100
|
-
tables.each do |table|
|
101
|
-
schema_file = tmp_path.join("#{table}.sql")
|
102
|
-
csv_file = tmp_path.join("#{table}.txt")
|
103
|
-
puts "Dumping #{table}."
|
104
|
-
|
105
|
-
# This command creates a table_name.sql and a table_name.txt file
|
106
|
-
cmd = "mysqldump --quick -T #{tmp_path} "
|
107
|
-
cmd += csv_options
|
108
|
-
cmd += "#{db_connection_options} #{table}"
|
109
|
-
puts cmd
|
110
|
-
`#{cmd}`
|
111
|
-
|
112
|
-
# `mysqldump` creates files with .txt extensions, so we rename it.
|
113
|
-
path, file = csv_file.split
|
114
|
-
csv_file = path.join("#{file.basename(".txt")}.csv")
|
115
|
-
mv path.join(file), csv_file
|
116
|
-
|
117
|
-
puts "gziping #{schema_file}."
|
118
|
-
`gzip #{schema_file}`
|
119
|
-
|
120
|
-
puts "gziping #{csv_file}."
|
121
|
-
`gzip #{csv_file}`
|
122
|
-
|
123
|
-
puts "#{table} dumped.\n\n"
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
def upload
|
128
|
-
Dir["#{tmp_path}/*"].each do |file|
|
129
|
-
upload_file file
|
130
|
-
end
|
131
|
-
puts "Finished uploading backups."
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
|
136
|
-
|
137
|
-
def upload_file(local_file)
|
138
|
-
path = Pathname.new(local_file)
|
139
|
-
s3_path = bucket_path.join(path.basename)
|
140
|
-
@bucket.put(s3_path, open(path), {}, nil, {
|
141
|
-
'x-amz-storage-class' => 'REDUCED_REDUNDANCY'
|
142
|
-
})
|
143
|
-
end
|
144
|
-
|
145
|
-
def tables
|
146
|
-
unless @tables
|
147
|
-
res = `mysql #{db_connection_options} -e "SHOW TABLES"`
|
148
|
-
@tables = res.split[1..-1]
|
149
|
-
end
|
150
|
-
@tables
|
151
|
-
end
|
152
|
-
|
153
|
-
def bucket_path
|
154
|
-
@bucket_path ||= Pathname.new(bucket_dir).join(@time.strftime("%Y-%m-%d-%H-%M"))
|
155
|
-
end
|
156
|
-
end # class Dumper
|
157
|
-
|
158
|
-
|
159
|
-
class Loader
|
160
|
-
include Helper
|
161
|
-
include FileUtils
|
162
|
-
|
163
|
-
def initialize(config)
|
164
|
-
@config = config
|
165
|
-
initialize_s3
|
166
|
-
end
|
167
|
-
|
168
|
-
# only import schema for these tables
|
169
|
-
def skip_data_for_tables
|
170
|
-
config[:skip_data_for_tables] || []
|
171
|
-
end
|
172
|
-
|
173
|
-
# only import these tables schema+data
|
174
|
-
def only_tables
|
175
|
-
config[:only_tables] || []
|
176
|
-
end
|
177
|
-
|
178
|
-
def load_latest
|
179
|
-
prefix = backups.first
|
180
|
-
|
181
|
-
# Set directory where backup is downloaded to
|
182
|
-
@time = Time.new(*prefix.split("/").last.split("-"))
|
183
|
-
initialize_directories
|
184
|
-
|
185
|
-
puts "Downloading backups ..."
|
186
|
-
@bucket.keys(:prefix => prefix).each do |key|
|
187
|
-
next unless (filename = download_file(key))
|
188
|
-
|
189
|
-
# gunzip file
|
190
|
-
print " -- Inflating #{filename} ... "
|
191
|
-
`gunzip #{tmp_path.join(filename)}`
|
192
|
-
print "complete.\n"
|
193
|
-
end
|
194
|
-
|
195
|
-
# Load data
|
196
|
-
puts "Loading schema and data by table"
|
197
|
-
import_cmd = "mysqlimport #{db_connection_options}"
|
198
|
-
import_cmd += csv_options
|
199
|
-
Dir["#{tmp_path}/*.sql"].each do |file|
|
200
|
-
print " - Loading schema for #{File.basename(file, ".sql")} ... "
|
201
|
-
cmd = "cat #{file} | mysql #{db_connection_options}"
|
202
|
-
`#{cmd}`
|
203
|
-
print "complete.\n"
|
204
|
-
|
205
|
-
csv_file = "#{tmp_path}/#{File.basename(file, ".sql")}.csv"
|
206
|
-
if File.exists?(csv_file)
|
207
|
-
print " - Importing #{File.basename(csv_file, ".csv")} ... "
|
208
|
-
`#{import_cmd} #{csv_file}`
|
209
|
-
print "complete.\n"
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
puts "Backup loaded."
|
214
|
-
|
215
|
-
rescue Exception => e
|
216
|
-
puts e.message
|
217
|
-
puts e.backtrace.join("\n")
|
218
|
-
ensure
|
219
|
-
remove_directories
|
220
|
-
end
|
221
|
-
|
222
|
-
def download_file(key)
|
223
|
-
filename = File.basename(key.name)
|
224
|
-
print "#{filename}... "
|
225
|
-
|
226
|
-
unless should_download_file?(filename)
|
227
|
-
puts " [ SKIP ]"
|
228
|
-
return
|
229
|
-
end
|
230
|
-
|
231
|
-
print " Downloading... "
|
232
|
-
|
233
|
-
File.open(tmp_path.join(filename), "wb") do |f|
|
234
|
-
@bucket.s3.interface.get(@bucket.name, key.name) do |chunk|
|
235
|
-
f.write chunk
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
puts "complete."
|
240
|
-
filename
|
241
|
-
end
|
242
|
-
|
243
|
-
def should_download_file?(filename)
|
244
|
-
table_name = filename.gsub(/\..*\..*$/, '')
|
245
|
-
|
246
|
-
if only_tables.empty? and skip_data_for_tables.empty?
|
247
|
-
return true
|
248
|
-
end
|
249
|
-
|
250
|
-
if filename.match(/\.csv\.gz$/)
|
251
|
-
is_data = true
|
252
|
-
is_schema = false
|
253
|
-
else
|
254
|
-
is_data = false
|
255
|
-
is_schema = true
|
256
|
-
end
|
257
|
-
|
258
|
-
if !only_tables.empty?
|
259
|
-
return only_tables.include?(table_name)
|
260
|
-
end
|
261
|
-
|
262
|
-
if !skip_data_for_tables.empty?
|
263
|
-
if is_schema or (is_data and !skip_data_for_tables.include?(table_name))
|
264
|
-
return true
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
# Get a list of backups stored on S3.
|
270
|
-
#
|
271
|
-
# Returns an array of s3 paths that look like:
|
272
|
-
#
|
273
|
-
# mysql/YYYY-MM-DD-HH-MM
|
274
|
-
#
|
275
|
-
# Array elements are sorted with the latest date first.
|
276
|
-
def backups
|
277
|
-
unless @backups
|
278
|
-
@backups = []
|
279
|
-
# Backups are stored in the mysql/ directory
|
280
|
-
@bucket.s3.interface.incrementally_list_bucket(@bucket.name, {
|
281
|
-
:prefix => "#{bucket_dir}", :delimiter => "/"
|
282
|
-
}) do |item|
|
283
|
-
@backups += item[:common_prefixes]
|
284
|
-
end
|
285
|
-
@backups = @backups.sort { |a,b| b <=> a }
|
286
|
-
end
|
287
|
-
@backups
|
288
|
-
end
|
289
|
-
end # class Loader
|
290
29
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql_truck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,11 +11,11 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2012-05-30 00:00:00.000000000Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: right_aws
|
18
|
-
requirement: &
|
18
|
+
requirement: &70339584428840 !ruby/object:Gem::Requirement
|
19
19
|
none: false
|
20
20
|
requirements:
|
21
21
|
- - ! '>='
|
@@ -23,7 +23,7 @@ dependencies:
|
|
23
23
|
version: '0'
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
|
-
version_requirements: *
|
26
|
+
version_requirements: *70339584428840
|
27
27
|
description: Mysql database backup tool. Dumps/Loads to/from S3.
|
28
28
|
email:
|
29
29
|
- peter@paydrotalks.com
|
@@ -39,6 +39,9 @@ files:
|
|
39
39
|
- TODO
|
40
40
|
- bin/mysql_truck
|
41
41
|
- lib/mysql_truck.rb
|
42
|
+
- lib/mysql_truck/dumper.rb
|
43
|
+
- lib/mysql_truck/helper.rb
|
44
|
+
- lib/mysql_truck/loader.rb
|
42
45
|
- lib/mysql_truck/version.rb
|
43
46
|
- mysql_truck.gemspec
|
44
47
|
homepage: ''
|