dumpr 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +1 -0
- data/bin/csv2sqlinsert +35 -0
- data/bin/dumpr +4 -0
- data/dumpr.gemspec +22 -0
- data/lib/dumpr.rb +34 -0
- data/lib/dumpr/chunkpipe.rb +51 -0
- data/lib/dumpr/cli.rb +156 -0
- data/lib/dumpr/driver.rb +222 -0
- data/lib/dumpr/driver/mysql.rb +30 -0
- data/lib/dumpr/util.rb +102 -0
- data/lib/dumpr/version.rb +3 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dd3cfde68c281a599acafcb492f09bc434ca3a80d066f3617e6d6c504aa2c3f2
|
4
|
+
data.tar.gz: 1208625e0f1dbfef63210656a3313905ce1d1ec5f74c51718652781495998636
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '0148d00c1f332b28dc6f1e640792c4c58bed2242abc64e1094e10e4fc534c589e71bc9e929b23988e9a9053a00b1ceb9a3989704f9856abb8d77b7a9521794ad'
|
7
|
+
data.tar.gz: 75414e3b37615572f70df9dba1a8cbca2b07609bee818cf1ca44d11d32bb060e4d5b4c6cb6238f4edea33e2fa5009805bf149d22e4f2c17946b41640174dcad4
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.DS_Store
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) James Dickson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
Software), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
## Dumpr
|
2
|
+
A Ruby Gem that's objective is to make dumping and importing databases easy.
|
3
|
+
|
4
|
+
Features:
|
5
|
+
* generates gzipped sql dump files for a specific database or specific tables.
|
6
|
+
* automates transfer of dump files to remote hosts
|
7
|
+
* automates import
|
8
|
+
|
9
|
+
Executables installed:
|
10
|
+
* dumpr
|
11
|
+
|
12
|
+
### Dependencies
|
13
|
+
* [Ruby ≥ 1.8.7](http://www.ruby-lang.org/en/downloads/)
|
14
|
+
|
15
|
+
**SSH access is assumed to be automated with .ssh/config entries**
|
16
|
+
|
17
|
+
### Installation
|
18
|
+
|
19
|
+
```sh
|
20
|
+
gem install dumpr
|
21
|
+
```
|
22
|
+
### Usage
|
23
|
+
|
24
|
+
Use the `dumpr` executable to export and import your databases.
|
25
|
+
|
26
|
+
#### dumpr
|
27
|
+
|
28
|
+
The *dumpr* command can be used to export and import database dumps.
|
29
|
+
|
30
|
+
*Exporting*
|
31
|
+
|
32
|
+
Generate yourdb.sql.gz and transfer it to server2
|
33
|
+
|
34
|
+
```sh
|
35
|
+
dumpr --user user --password pw --db yourdb --dumpfile yourdb.sql --destination dbserver2:/data/dumps/
|
36
|
+
```
|
37
|
+
|
38
|
+
*Importing*
|
39
|
+
|
40
|
+
Then, over on dbserver2, import your dump file
|
41
|
+
```sh
|
42
|
+
dumpr -i --user user --password pw --dumpfile /data/dumps/yourdb.sql
|
43
|
+
```
|
44
|
+
|
45
|
+
## Ruby API
|
46
|
+
|
47
|
+
You can write your own scripts that use a *Dumpr::Driver*
|
48
|
+
|
49
|
+
### Exporting
|
50
|
+
|
51
|
+
Generate yourdb.sql.gz and transfer it to server2
|
52
|
+
```ruby
|
53
|
+
Dumpr::Driver::Mysql.export(
|
54
|
+
:user => 'backupuser', :pass => 'dbpass',
|
55
|
+
:db => 'yourdb',
|
56
|
+
:destination => 'server2:/data/dumps/yourdb.sql'
|
57
|
+
)
|
58
|
+
```
|
59
|
+
|
60
|
+
### Importing
|
61
|
+
|
62
|
+
Then, over on dbserver2, import your dump file
|
63
|
+
```ruby
|
64
|
+
Dumpr::Driver::Mysql.import(
|
65
|
+
:user => 'importuser', :pass => 'pass',
|
66
|
+
:db => 'yourdb',
|
67
|
+
:dumpfile => '/data/dumps/yourdb.sql'
|
68
|
+
)
|
69
|
+
```
|
70
|
+
|
71
|
+
### Standard Dumpr::Driver options
|
72
|
+
|
73
|
+
See *Dumpr::Driver*
|
74
|
+
|
75
|
+
|
76
|
+
## CHANGELOG
|
77
|
+
|
78
|
+
* Version 1.0
|
79
|
+
|
80
|
+
## TODO
|
81
|
+
|
82
|
+
* Dumpr::Driver::Postgres
|
83
|
+
* automate importing after an export (socket communication exporter/importer, or just some dumb lockfile checking / polling)
|
84
|
+
* security: stop logging passwords
|
85
|
+
* daemonize
|
86
|
+
* SSH parameters
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/csv2sqlinsert
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# author: jdickson
|
3
|
+
# script: csv2sqlinsert
|
4
|
+
# todo: optparse
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'dumpr/chunkpipe'
|
8
|
+
|
9
|
+
USAGE = "Usage: cat yourdata.csv | #{File.basename($0)} yourtable 1000 > insert.sql"
|
10
|
+
if ARGV.length < 2
|
11
|
+
puts USAGE
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
table_name = ARGV[0]
|
15
|
+
chunk_size = ARGV[1].to_i
|
16
|
+
pause_sec = (ARGV[2] || 0).to_i
|
17
|
+
delim="\n"
|
18
|
+
i = 0
|
19
|
+
total = 0
|
20
|
+
ChunkPipe.open(STDIN, STDOUT, chunk_size, delim) do |lines|
|
21
|
+
i+=1
|
22
|
+
total += lines.length
|
23
|
+
out = ""
|
24
|
+
out << "INSERT IGNORE INTO `#{table_name}` VALUES \n"
|
25
|
+
lines.each_with_index do |line, idx|
|
26
|
+
line = line.strip.gsub('\N', 'NULL')
|
27
|
+
out << "(#{line})" << (idx == (lines.length-1) ? "" : ",") << "\n"
|
28
|
+
end
|
29
|
+
out << ";\n"
|
30
|
+
out << "select sleep(#{pause_sec.to_f});\n" if pause_sec != 0
|
31
|
+
out << "select 'completed chunk #{i} (#{total} records so far)' as '';\n"
|
32
|
+
out
|
33
|
+
end
|
34
|
+
|
35
|
+
exit 0
|
data/bin/dumpr
ADDED
data/dumpr.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "dumpr/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
|
6
|
+
s.name = "dumpr"
|
7
|
+
s.version = Dumpr::Version.to_s
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["James Dickson"]
|
10
|
+
s.email = ["jdickson@bcap.com"]
|
11
|
+
s.homepage = "http://github.com/sixjameses/dumpr"
|
12
|
+
s.summary = "Dump and load databases."
|
13
|
+
s.description = "Dumpr provides an easy way to dump and import databases."
|
14
|
+
s.files = `git ls-files -z`.split("\x0")
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.extra_rdoc_files = ["README.md"]
|
19
|
+
s.licenses = ['MIT']
|
20
|
+
s.required_ruby_version = '>= 2.2.1'
|
21
|
+
#s.add_dependency('highline')
|
22
|
+
end
|
data/lib/dumpr.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'dumpr/version'
|
2
|
+
require 'dumpr/driver'
|
3
|
+
|
4
|
+
module Dumpr
|
5
|
+
|
6
|
+
# error raised when there is a bad configuration
|
7
|
+
class BadConfig < RuntimeError; end
|
8
|
+
|
9
|
+
# error raised when a dump filename already exists
|
10
|
+
class DumpFileExists < RuntimeError; end
|
11
|
+
|
12
|
+
# error raised when there is a dump operation in progress
|
13
|
+
class BusyDumping < RuntimeError; end
|
14
|
+
|
15
|
+
# error raised when there is a command failure
|
16
|
+
class CommandFailure < RuntimeError; end
|
17
|
+
|
18
|
+
def dump(driver, opts)
|
19
|
+
driver = Driver.find(driver).new(opts)
|
20
|
+
driver.dump()
|
21
|
+
end
|
22
|
+
|
23
|
+
def import(driver, opts)
|
24
|
+
driver = Driver.find(driver).new(opts)
|
25
|
+
driver.import()
|
26
|
+
end
|
27
|
+
|
28
|
+
def export(driver, opts)
|
29
|
+
dump(driver, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
module_function :dump, :import, :export
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# author :jdickson
|
2
|
+
# ChunkPipe
|
3
|
+
# An IO pipe that provides generic parsing / rewriting
|
4
|
+
# of a stream of packets (or lines in a file).
|
5
|
+
# It passes chunks of packets to a block
|
6
|
+
# The return value of the block is written to the other end of the pipe
|
7
|
+
# packet delimiter is not removed from packet suffix
|
8
|
+
#
|
9
|
+
# Silly Example that simply passes through the data:
|
10
|
+
# ChunkPipe.open(STDIN, STDOUT, 1000, "\n") {|lines| lines.join }
|
11
|
+
#
|
12
|
+
module ChunkPipe
|
13
|
+
|
14
|
+
MAX_CHUNK_SIZE = 1000000
|
15
|
+
|
16
|
+
def open(reader, writer, chunk_size, packet_delim, read_timeout=3, &block)
|
17
|
+
if chunk_size < 1 || chunk_size > MAX_CHUNK_SIZE
|
18
|
+
raise ArgumentError.new "invalid chunk size #{chunk_size.inspect}"
|
19
|
+
end
|
20
|
+
chunk_idx = 0
|
21
|
+
packet_idx = 0
|
22
|
+
packets = []
|
23
|
+
stopblocking = Thread.new do
|
24
|
+
sleep read_timeout
|
25
|
+
reader.close # this can raise IOError
|
26
|
+
raise IOError.new "ChunkPipe read time out (#{read_timeout}s)"
|
27
|
+
end
|
28
|
+
while packet = reader.gets(packet_delim) do
|
29
|
+
stopblocking.kill if stopblocking.alive?
|
30
|
+
packets << packet
|
31
|
+
if ((packet_idx+1) % chunk_size == 0)
|
32
|
+
data = yield packets
|
33
|
+
writer << data if data
|
34
|
+
chunk_idx+=1
|
35
|
+
packets = []
|
36
|
+
end
|
37
|
+
packet_idx+=1
|
38
|
+
#break if reader.eof?
|
39
|
+
end
|
40
|
+
unless packets.empty?
|
41
|
+
data = yield packets
|
42
|
+
writer << data if data
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
reader.close if reader && !reader.closed?
|
46
|
+
writer.close if writer && !writer.closed?
|
47
|
+
end
|
48
|
+
|
49
|
+
module_function :open
|
50
|
+
|
51
|
+
end
|
data/lib/dumpr/cli.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'dumpr'
|
2
|
+
require 'optparse'
|
3
|
+
require 'ostruct'
|
4
|
+
# command line functions for bin/dumpr
|
5
|
+
module Dumpr
|
6
|
+
class CLI
|
7
|
+
PROG_NAME = File.basename($0)
|
8
|
+
|
9
|
+
|
10
|
+
def self.execute(args)
|
11
|
+
# default options
|
12
|
+
options = {}
|
13
|
+
options[:dumpdir] = Dir.pwd
|
14
|
+
options[:driver] = :mysql
|
15
|
+
options[:gzip] = true
|
16
|
+
|
17
|
+
|
18
|
+
op = OptionParser.new do |opts|
|
19
|
+
|
20
|
+
opts.banner = <<-ENDSTR
|
21
|
+
Usage: #{PROG_NAME} [options]
|
22
|
+
|
23
|
+
Exporting:
|
24
|
+
|
25
|
+
#{PROG_NAME} --user youruser --password yourpass --db yourdb --dumpfile yourdb.sql --destination server2:/data/backups
|
26
|
+
|
27
|
+
Importing:
|
28
|
+
|
29
|
+
#{PROG_NAME} -i --user youruser --password yourpass --db yourdb --dumpfile /data/backups/yourdb.sql
|
30
|
+
|
31
|
+
Don't forget to set up your .ssh/config so you won't be prompted for ssh passwords for file transfers
|
32
|
+
|
33
|
+
Options:
|
34
|
+
|
35
|
+
ENDSTR
|
36
|
+
|
37
|
+
opts.on("-t", "--type [TYPE]", "Database type. (mysql is the default) ") do |val|
|
38
|
+
options[:driver] = val
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-i", "--import", "Import dump file. Default behavior is to dump and export to --destination") do |val|
|
42
|
+
options[:import] = val
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("--all-databases", "dump/import ALL databases") do |val|
|
46
|
+
options[:all_databases] = val
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("--db DATABASE", "--database DATABASE", "Database to dump/import") do |val|
|
50
|
+
options[:database] = val
|
51
|
+
end
|
52
|
+
|
53
|
+
# TODO: Add support to Driver for this
|
54
|
+
opts.on("--databases [x,y,z]", Array, "dump/import multiple databases") do |val|
|
55
|
+
options[:databases] = val
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("--tables [t1,t2,t3]", Array, "dump certain tables, to be used on conjuction with a single --database") do |val|
|
59
|
+
options[:tables] = val
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("-u USER", "--user USER", "Database user") do |val|
|
63
|
+
options[:user] = val
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on("-p PASS", "--password PASS", "--password=pass", "Database password") do |val|
|
67
|
+
options[:password] = val
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on("-h HOST", "--host HOST", "Database host") do |val|
|
71
|
+
options[:host] = val
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on("-P PORT", "--port PORT", "Database port") do |val|
|
75
|
+
options[:port] = val
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on("--dumpfile [DUMPFILE]", "Filename of dump to create/import") do |val|
|
79
|
+
options[:dumpfile] = val
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on("--destination [DESTINATION]", "Destination for dumpfile. This can be a remote host:path.") do |val|
|
83
|
+
options[:destination] = val
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on("--dumpdir", "Default directory for dumpfiles. Default is working directory") do |val|
|
87
|
+
options[:dumpdir] = val
|
88
|
+
end
|
89
|
+
|
90
|
+
opts.on("--dump-options=[DUMPOPTIONS]", "Extra options to be included in dump command") do |val|
|
91
|
+
options[:dump_options] = val
|
92
|
+
end
|
93
|
+
|
94
|
+
opts.on("--no-gzip", "Don't use gzip") do |val|
|
95
|
+
options[:gzip] = false
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on("--gzip-options=[GZIPOPTIONS]", "gzip compression options. Default is -9 (slowest /max compression)") do |val|
|
99
|
+
options[:gzip_options] = val
|
100
|
+
end
|
101
|
+
|
102
|
+
opts.on("--log-file [LOGFILE]", "Log file. Default is stdout.") do |val|
|
103
|
+
options[:log_file] = val
|
104
|
+
end
|
105
|
+
|
106
|
+
opts.on("--force", "Overwrite dumpfile if it exists already.") do |val|
|
107
|
+
options[:force] = val
|
108
|
+
end
|
109
|
+
|
110
|
+
opts.on("-h", "--help", "Show this message") do
|
111
|
+
puts opts
|
112
|
+
exit
|
113
|
+
end
|
114
|
+
|
115
|
+
opts.on("-v", "--version", "Show version") do
|
116
|
+
puts Dumpr::Version
|
117
|
+
exit
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
123
|
+
op.parse!(args)
|
124
|
+
rescue OptionParser::MissingArgument => e
|
125
|
+
puts "invalid arguments. try #{PROG_NAME} --help"
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# do it
|
131
|
+
begin
|
132
|
+
if options[:import]
|
133
|
+
Dumpr.import(options[:driver], options)
|
134
|
+
else
|
135
|
+
Dumpr.export(options[:driver], options)
|
136
|
+
end
|
137
|
+
rescue Dumpr::BadConfig => e
|
138
|
+
puts "bad arguments: #{e.message}.\n See --help"
|
139
|
+
exit 1
|
140
|
+
rescue Dumpr::DumpFileExists => e
|
141
|
+
puts "#{e.message}\nIt looks like this dump exists already. You should move it, or use --force to trash it"
|
142
|
+
exit 1
|
143
|
+
rescue Dumpr::BusyDumping => e
|
144
|
+
puts "#{e.message}\n See --help"
|
145
|
+
exit 1
|
146
|
+
rescue Dumpr::CommandFailure => e
|
147
|
+
puts e.message
|
148
|
+
exit 1
|
149
|
+
end
|
150
|
+
|
151
|
+
exit 0
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
data/lib/dumpr/driver.rb
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
# abstract driver that does everything
|
2
|
+
require 'dumpr'
|
3
|
+
require 'dumpr/util'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Dumpr
|
7
|
+
module Driver
|
8
|
+
|
9
|
+
def find(driver)
|
10
|
+
driver_file = "dumpr/driver/#{driver}"
|
11
|
+
require(driver_file)
|
12
|
+
const_ar = driver.to_s.split("/").reject{|i| i==""}.collect {|i| i.capitalize.gsub(/_(.)/) {$1.upcase} }
|
13
|
+
klass_str = const_ar.join('::')
|
14
|
+
begin
|
15
|
+
klass = const_ar.inject(self) do |mod, const_name|
|
16
|
+
mod.const_get(const_name)
|
17
|
+
end
|
18
|
+
rescue NameError => e
|
19
|
+
raise e
|
20
|
+
raise BadConfig, "could not find `#{klass_str}' in `#{driver_file}'"
|
21
|
+
end
|
22
|
+
raise BadConfig, "#{klass.name} is not a type of Dumpr::Driver!" unless klass < Dumpr::Driver::Base
|
23
|
+
return klass
|
24
|
+
rescue LoadError
|
25
|
+
raise BadConfig, "failed to load '#{driver_file}' !'"
|
26
|
+
end
|
27
|
+
|
28
|
+
module_function :find
|
29
|
+
|
30
|
+
# abstract interface for all drivers
|
31
|
+
class Base
|
32
|
+
attr_reader :opts
|
33
|
+
attr_reader :host, :port, :user, :password, :database, :tables
|
34
|
+
attr_reader :gzip, :gzip_options, :dumpdir, :dumpfile, :dump_options, :import_options
|
35
|
+
attr_reader :destination, :destination_host, :destination_dumpfile
|
36
|
+
|
37
|
+
def initialize(opts)
|
38
|
+
self.configure(opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def configure(opts)
|
42
|
+
opts = (@opts||{}).merge(opts)
|
43
|
+
# db connection settings
|
44
|
+
@host = opts[:host] || "localhost"
|
45
|
+
@port = opts[:port]
|
46
|
+
@user = opts[:user] or raise BadConfig.new ":user => <db user> is required"
|
47
|
+
@password = (opts[:password] || opts[:pass]) or raise BadConfig.new ":pass => <db password> is required"
|
48
|
+
|
49
|
+
# dump all_databases or specific database(s)
|
50
|
+
@all_databases = nil
|
51
|
+
@database = nil
|
52
|
+
@databases = nil
|
53
|
+
@tables = nil
|
54
|
+
if (opts[:database] || opts[:db])
|
55
|
+
@database = (opts[:database] || opts[:db])
|
56
|
+
@tables = [opts[:table], opts[:tables]].flatten.uniq.compact
|
57
|
+
elsif opts[:databases]
|
58
|
+
@databases = [opts[:databases]].flatten.uniq.compact # not used/supported yet
|
59
|
+
elsif opts[:all_databases]
|
60
|
+
@all_databases = true
|
61
|
+
else
|
62
|
+
raise BadConfig.new ":database => <db schema name> is required"
|
63
|
+
end
|
64
|
+
|
65
|
+
# dump settings
|
66
|
+
@gzip = opts[:gzip].nil? ? true : opts[:gzip]
|
67
|
+
@gzip_options = opts[:gzip_options] || "-9"
|
68
|
+
@dumpdir = opts[:dumpdir] || "./"
|
69
|
+
@dumpfile = (opts[:dumpfile] || opts[:filename]) or raise BadConfig.new ":dumpfile => <file.sql> is required"
|
70
|
+
@dumpfile = @dumpfile[0].chr == "/" ? @dumpfile : File.join(@dumpdir, @dumpfile)
|
71
|
+
@dumpfile.chomp!(".gz")
|
72
|
+
# (optional) :destination is where dumps are exported to, and can be a remote host:path
|
73
|
+
@destination = opts[:destination] || @dumpfile
|
74
|
+
if @destination.include?(":")
|
75
|
+
@destination_host, @destination_dumpfile = @destination.split(":")[0], @destination.split(":")[1]
|
76
|
+
else
|
77
|
+
@destination_host, @destination_dumpfile = "localhost", @destination
|
78
|
+
end
|
79
|
+
# destination might be a path only, so build the entire filepath
|
80
|
+
if File.extname(@destination_dumpfile) == ""
|
81
|
+
@destination_dumpfile = File.join(@destination_dumpfile, File.basename(@dumpfile))
|
82
|
+
end
|
83
|
+
@destination_dumpfile.chomp!(".gz")
|
84
|
+
@dump_options = opts[:dump_options]
|
85
|
+
@import_options = opts[:import_options]
|
86
|
+
|
87
|
+
# set / update logger
|
88
|
+
if opts[:logger]
|
89
|
+
@logger = opts[:logger]
|
90
|
+
elsif opts[:log_file]
|
91
|
+
@logger = Logger.new(opts[:log_file])
|
92
|
+
end
|
93
|
+
@logger = Logger.new(STDOUT) if !@logger
|
94
|
+
@logger.level = opts[:log_level] if opts[:log_level] # expects integer
|
95
|
+
|
96
|
+
@opts = opts
|
97
|
+
end
|
98
|
+
|
99
|
+
def logger
|
100
|
+
@logger
|
101
|
+
end
|
102
|
+
|
103
|
+
def dump_cmd
|
104
|
+
raise BadConfig.new "#{self.class} has not defined dump_cmd!"
|
105
|
+
end
|
106
|
+
|
107
|
+
# DUMPING + EXPORTING
|
108
|
+
|
109
|
+
def remote_destination?
|
110
|
+
@destination_host && @destination_host != "localhost"
|
111
|
+
end
|
112
|
+
|
113
|
+
# creates @dumpfile
|
114
|
+
# pipes :dump_cmd to gzip, rather than write the file to disk twice
|
115
|
+
# if @destination is defined, it then moves the dump to the @destination, which can be a remote host:path
|
116
|
+
def dump
|
117
|
+
logger.debug("begin dump")
|
118
|
+
dumpfn = @dumpfile + (@gzip ? ".gz" : "")
|
119
|
+
Util.with_lockfile("localhost", dumpfn, @opts[:force]) do
|
120
|
+
|
121
|
+
logger.debug "preparing dump..."
|
122
|
+
if !File.exists?(File.dirname(dumpfn))
|
123
|
+
run "mkdir -p #{File.dirname(dumpfn)}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# avoid overwriting dump files..
|
127
|
+
if File.exists?(dumpfn)
|
128
|
+
if @opts[:force]
|
129
|
+
logger.warn "#{dumpfn} exists, moving it to #{dumpfn}.1"
|
130
|
+
#run "rm -f #{dumpfn}.1;"
|
131
|
+
run "mv #{dumpfn} #{dumpfn}.1"
|
132
|
+
else
|
133
|
+
logger.warn "#{dumpfn} already exists!"
|
134
|
+
raise DumpFileExists.new "#{dumpfn} already exists!"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
logger.debug "dumping..."
|
139
|
+
if @gzip
|
140
|
+
run "#{dump_cmd} | gzip #{gzip_options} > #{dumpfn}"
|
141
|
+
else
|
142
|
+
run "#{dump_cmd} > #{dumpfn}"
|
143
|
+
end
|
144
|
+
dumpsize = Util.human_file_size("localhost", dumpfn)
|
145
|
+
logger.info("generated #{dumpfn} (#{dumpsize})")
|
146
|
+
|
147
|
+
if @destination
|
148
|
+
if remote_destination?
|
149
|
+
logger.debug "exporting to #{@destination_host}..."
|
150
|
+
Util.with_lockfile(@destination_host, @destination_dumpfile, @opts[:force]) do
|
151
|
+
run "scp #{dumpfn} #{@destination_host}:#{@destination_dumpfile}#{@gzip ? '.gz' : ''}"
|
152
|
+
end
|
153
|
+
elsif @destination_dumpfile && @destination_dumpfile+(@gzip ? '.gz' : '') != dumpfn
|
154
|
+
logger.debug "exporting..."
|
155
|
+
destdir = File.dirname(@destination_dumpfile)
|
156
|
+
run "mkdir -p #{destdir}" if !Util.dir_exists?("localhost", destdir)
|
157
|
+
Util.with_lockfile("localhost", @destination_dumpfile, @opts[:force]) do
|
158
|
+
run "mv #{dumpfn} #{@destination_dumpfile}#{@gzip ? '.gz' : ''}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end # with_lockfile
|
164
|
+
logger.debug("end dump")
|
165
|
+
end
|
166
|
+
|
167
|
+
# IMPORTING
|
168
|
+
|
169
|
+
def import_cmd
|
170
|
+
raise BadConfig.new "#{self.class} has not defined import_cmd!"
|
171
|
+
end
|
172
|
+
|
173
|
+
def decompress
|
174
|
+
if File.exists?(@dumpfile + ".gz")
|
175
|
+
if File.exists?(@dumpfile) && !@opts[:force]
|
176
|
+
logger.warn "skipping decompress because #{@dumpfile} already exists."
|
177
|
+
else
|
178
|
+
logger.debug "decompressing..."
|
179
|
+
run "gzip -d -f #{@dumpfile}.gz"
|
180
|
+
end
|
181
|
+
else
|
182
|
+
logger.warn "decompress failed. #{@dumpfile}.gz does not exist!"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def import
|
187
|
+
Util.with_lockfile("localhost", @dumpfile, @opts[:force]) do
|
188
|
+
decompress if @gzip
|
189
|
+
|
190
|
+
if !File.exists?(@dumpfile)
|
191
|
+
raise "Cannot import #{@dumpfile} because it does not exist!"
|
192
|
+
else
|
193
|
+
dumpsize = Util.human_file_size("localhost", @dumpfile)
|
194
|
+
logger.info("importing #{@dumpfile} (#{dumpsize})")
|
195
|
+
run import_cmd
|
196
|
+
end
|
197
|
+
|
198
|
+
end # with_lockfile
|
199
|
+
end
|
200
|
+
|
201
|
+
protected
|
202
|
+
|
203
|
+
def scrub_cmd(cmd)
|
204
|
+
cmd.gsub(/password=[^\s]+/, 'password=xxxxxx')
|
205
|
+
end
|
206
|
+
|
207
|
+
def run(cmd)
|
208
|
+
start_time = Time.now
|
209
|
+
logger.info "running command: #{scrub_cmd cmd}"
|
210
|
+
stdout = `#{cmd}`
|
211
|
+
took_sec = (Time.now - start_time).round()
|
212
|
+
if $?.success?
|
213
|
+
logger.info "finished (took #{took_sec}s)"
|
214
|
+
else
|
215
|
+
logger.error "failed (took #{took_sec}s) status: #{$?.exitstatus}"
|
216
|
+
raise CommandFailure.new("Aborting because the following command failed: #{scrub_cmd cmd}")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'dumpr/driver'
|
2
|
+
module Dumpr
|
3
|
+
module Driver
|
4
|
+
class Mysql < Base
|
5
|
+
|
6
|
+
def port
|
7
|
+
@port || 3306
|
8
|
+
end
|
9
|
+
|
10
|
+
def dump_options
|
11
|
+
@dump_options || "--single-transaction --quick"
|
12
|
+
end
|
13
|
+
|
14
|
+
def dump_cmd
|
15
|
+
if @all_databases
|
16
|
+
"mysqldump -u #{user} --password=#{password} -h #{host} -P #{port} --all-databases #{dump_options}"
|
17
|
+
elsif @databases
|
18
|
+
"mysqldump -u #{user} --password=#{password} -h #{host} -P #{port} --databases #{databases.join(' ')} #{dump_options}"
|
19
|
+
else
|
20
|
+
"mysqldump -u #{user} --password=#{password} -h #{host} -P #{port} #{database} #{tables ? tables.join(' ') : ''} #{dump_options}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def import_cmd
|
25
|
+
"mysql -u #{user} --password=#{password} -h #{host} -P #{port} #{database} < #{dumpfile}"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dumpr/util.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#TODO: Refactor this
|
2
|
+
# Utility methods for touching/reading local and remote files
|
3
|
+
module Dumpr
|
4
|
+
module Util
|
5
|
+
|
6
|
+
def self.file_exists?(h, fn)
|
7
|
+
if h == "localhost"
|
8
|
+
File.exists?(fn)
|
9
|
+
else
|
10
|
+
`ssh #{h} test -f '#{fn}' &> /dev/null`
|
11
|
+
$?.success?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.dir_exists?(h, fn)
|
16
|
+
if h == "localhost"
|
17
|
+
File.exists?(fn)
|
18
|
+
else
|
19
|
+
`ssh #{h} test -d '#{fn}' &> /dev/null`
|
20
|
+
$?.success?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# touch a file and optionally overwrite it's content with msg
|
25
|
+
def self.touch_file(h, fn, msg=nil)
|
26
|
+
cmd = "touch #{fn}" + (msg ? " && echo '#{msg}' > #{fn}" : '')
|
27
|
+
if h == "localhost"
|
28
|
+
system(cmd)
|
29
|
+
else
|
30
|
+
system("ssh #{h} '#{cmd}'")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# return contents of a file
|
35
|
+
def self.cat_file(h, fn)
|
36
|
+
cmd = "cat #{fn}"
|
37
|
+
if h == "localhost"
|
38
|
+
`#{cmd}`.strip
|
39
|
+
else
|
40
|
+
`ssh #{h} #{cmd}`.strip
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.remove_file(h, fn)
|
45
|
+
cmd = "rm #{fn}"
|
46
|
+
if h == "localhost"
|
47
|
+
system(cmd)
|
48
|
+
else
|
49
|
+
system("ssh #{h} #{cmd}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# return the human readable size of a file like 10MB
|
54
|
+
def self.human_file_size(h, fn)
|
55
|
+
cmd = "du -h #{fn} | cut -f 1"
|
56
|
+
if h == "localhost"
|
57
|
+
`#{cmd}`.strip
|
58
|
+
else
|
59
|
+
`ssh #{h} #{cmd}`.strip
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.process_running?(h, pid)
|
64
|
+
cmd = "ps -p #{pid}"
|
65
|
+
if h == "localhost"
|
66
|
+
system(cmd)
|
67
|
+
else
|
68
|
+
system("ssh #{h} #{cmd}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.with_lockfile(h, fn, remove_dead_locks=false)
|
73
|
+
fn = fn.chomp('.dumpr.lock') + '.dumpr.lock'
|
74
|
+
mylock = nil
|
75
|
+
if file_exists?(h, fn)
|
76
|
+
pid = cat_file(h, fn)
|
77
|
+
mylock = Process.pid == pid
|
78
|
+
if mylock
|
79
|
+
# my own lock.. proceed
|
80
|
+
elsif process_running?(h, pid)
|
81
|
+
raise BusyDumping.new "Lockfile '#{fn}' exists for another process (#{pid})!"
|
82
|
+
else
|
83
|
+
if remove_dead_locks
|
84
|
+
puts "Removing lockfile '#{fn}' for dead process (#{pid})"
|
85
|
+
remove_file(h, fn)
|
86
|
+
else
|
87
|
+
raise BusyDumping.new "Lockfile '#{fn}' exists for dead process (#{pid}) ! You may want to investigate the reason why, or use --force"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
begin
|
92
|
+
touch_file(h, fn, Process.pid)
|
93
|
+
yield
|
94
|
+
rescue => e
|
95
|
+
raise e
|
96
|
+
ensure
|
97
|
+
remove_file(h, fn)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dumpr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Dickson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Dumpr provides an easy way to dump and import databases.
|
14
|
+
email:
|
15
|
+
- jdickson@bcap.com
|
16
|
+
executables:
|
17
|
+
- csv2sqlinsert
|
18
|
+
- dumpr
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files:
|
21
|
+
- README.md
|
22
|
+
files:
|
23
|
+
- ".gitignore"
|
24
|
+
- Gemfile
|
25
|
+
- LICENSE
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- bin/csv2sqlinsert
|
29
|
+
- bin/dumpr
|
30
|
+
- dumpr.gemspec
|
31
|
+
- lib/dumpr.rb
|
32
|
+
- lib/dumpr/chunkpipe.rb
|
33
|
+
- lib/dumpr/cli.rb
|
34
|
+
- lib/dumpr/driver.rb
|
35
|
+
- lib/dumpr/driver/mysql.rb
|
36
|
+
- lib/dumpr/util.rb
|
37
|
+
- lib/dumpr/version.rb
|
38
|
+
homepage: http://github.com/sixjameses/dumpr
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 2.2.1
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 2.7.6
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Dump and load databases.
|
62
|
+
test_files: []
|