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.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dumpr.gemspec
4
+ gemspec
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.
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'dumpr/cli'
4
+ Dumpr::CLI.execute(ARGV)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Dumpr
2
+ Version = "1.0".freeze
3
+ 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: []