dumpr 1.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.
- 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: []
|