dumpr 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []