tapsoob 0.1.10
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 +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/README.md +66 -0
- data/Rakefile +0 -0
- data/bin/schema +54 -0
- data/bin/tapsoob +6 -0
- data/lib/tapsoob.rb +9 -0
- data/lib/tapsoob/chunksize.rb +53 -0
- data/lib/tapsoob/cli.rb +145 -0
- data/lib/tapsoob/config.rb +33 -0
- data/lib/tapsoob/data_stream.rb +350 -0
- data/lib/tapsoob/errors.rb +16 -0
- data/lib/tapsoob/log.rb +16 -0
- data/lib/tapsoob/operation.rb +468 -0
- data/lib/tapsoob/progress_bar.rb +236 -0
- data/lib/tapsoob/railtie.rb +11 -0
- data/lib/tapsoob/schema.rb +83 -0
- data/lib/tapsoob/utils.rb +179 -0
- data/lib/tapsoob/version.rb +4 -0
- data/lib/tasks/tapsoob.rake +59 -0
- data/tapsoob.gemspec +30 -0
- metadata +138 -0
@@ -0,0 +1,236 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
#
|
3
|
+
# Ruby/ProgressBar - a text progress bar library
|
4
|
+
#
|
5
|
+
# Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
|
6
|
+
# All rights reserved.
|
7
|
+
# This is free software with ABSOLUTELY NO WARRANTY.
|
8
|
+
#
|
9
|
+
# You can redistribute it and/or modify it under the terms
|
10
|
+
# of Ruby's license.
|
11
|
+
#
|
12
|
+
|
13
|
+
class ProgressBar
|
14
|
+
VERSION = "0.9"
|
15
|
+
|
16
|
+
def initialize (title, total, out = STDERR)
|
17
|
+
@title = title
|
18
|
+
@total = total
|
19
|
+
@out = out
|
20
|
+
@terminal_width = 80
|
21
|
+
@bar_mark = "="
|
22
|
+
@current = 0
|
23
|
+
@previous = 0
|
24
|
+
@finished_p = false
|
25
|
+
@start_time = Time.now
|
26
|
+
@previous_time = @start_time
|
27
|
+
@title_width = 14
|
28
|
+
@format = "%-#{@title_width}s %3d%% %s %s"
|
29
|
+
@format_arguments = [:title, :percentage, :bar, :stat]
|
30
|
+
clear
|
31
|
+
show
|
32
|
+
end
|
33
|
+
attr_reader :title
|
34
|
+
attr_reader :current
|
35
|
+
attr_reader :total
|
36
|
+
attr_accessor :start_time
|
37
|
+
|
38
|
+
private
|
39
|
+
def fmt_bar
|
40
|
+
bar_width = do_percentage * @terminal_width / 100
|
41
|
+
sprintf("|%s%s|",
|
42
|
+
@bar_mark * bar_width,
|
43
|
+
" " * (@terminal_width - bar_width))
|
44
|
+
end
|
45
|
+
|
46
|
+
def fmt_percentage
|
47
|
+
do_percentage
|
48
|
+
end
|
49
|
+
|
50
|
+
def fmt_stat
|
51
|
+
if @finished_p then elapsed else eta end
|
52
|
+
end
|
53
|
+
|
54
|
+
def fmt_stat_for_file_transfer
|
55
|
+
if @finished_p then
|
56
|
+
sprintf("%s %s %s", bytes, transfer_rate, elapsed)
|
57
|
+
else
|
58
|
+
sprintf("%s %s %s", bytes, transfer_rate, eta)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def fmt_title
|
63
|
+
@title[0,(@title_width - 1)] + ":"
|
64
|
+
end
|
65
|
+
|
66
|
+
def convert_bytes (bytes)
|
67
|
+
if bytes < 1024
|
68
|
+
sprintf("%6dB", bytes)
|
69
|
+
elsif bytes < 1024 * 1000 # 1000kb
|
70
|
+
sprintf("%5.1fKB", bytes.to_f / 1024)
|
71
|
+
elsif bytes < 1024 * 1024 * 1000 # 1000mb
|
72
|
+
sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
|
73
|
+
else
|
74
|
+
sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def transfer_rate
|
79
|
+
bytes_per_second = @current.to_f / (Time.now - @start_time)
|
80
|
+
sprintf("%s/s", convert_bytes(bytes_per_second))
|
81
|
+
end
|
82
|
+
|
83
|
+
def bytes
|
84
|
+
convert_bytes(@current)
|
85
|
+
end
|
86
|
+
|
87
|
+
def format_time (t)
|
88
|
+
t = t.to_i
|
89
|
+
sec = t % 60
|
90
|
+
min = (t / 60) % 60
|
91
|
+
hour = t / 3600
|
92
|
+
sprintf("%02d:%02d:%02d", hour, min, sec);
|
93
|
+
end
|
94
|
+
|
95
|
+
# ETA stands for Estimated Time of Arrival.
|
96
|
+
def eta
|
97
|
+
if @current == 0
|
98
|
+
"ETA: --:--:--"
|
99
|
+
else
|
100
|
+
elapsed = Time.now - @start_time
|
101
|
+
eta = elapsed * @total / @current - elapsed;
|
102
|
+
sprintf("ETA: %s", format_time(eta))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def elapsed
|
107
|
+
elapsed = Time.now - @start_time
|
108
|
+
sprintf("Time: %s", format_time(elapsed))
|
109
|
+
end
|
110
|
+
|
111
|
+
def eol
|
112
|
+
if @finished_p then "\n" else "\r" end
|
113
|
+
end
|
114
|
+
|
115
|
+
def do_percentage
|
116
|
+
if @total.zero?
|
117
|
+
100
|
118
|
+
else
|
119
|
+
@current * 100 / @total
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_width
|
124
|
+
# FIXME: I don't know how portable it is.
|
125
|
+
default_width = 80
|
126
|
+
begin
|
127
|
+
tiocgwinsz = 0x5413
|
128
|
+
data = [0, 0, 0, 0].pack("SSSS")
|
129
|
+
if @out.ioctl(tiocgwinsz, data) >= 0 then
|
130
|
+
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
131
|
+
if cols > 0 then cols else default_width end
|
132
|
+
else
|
133
|
+
default_width
|
134
|
+
end
|
135
|
+
rescue Exception
|
136
|
+
default_width
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def show
|
141
|
+
arguments = @format_arguments.map {|method|
|
142
|
+
method = sprintf("fmt_%s", method)
|
143
|
+
send(method)
|
144
|
+
}
|
145
|
+
line = sprintf(@format, *arguments)
|
146
|
+
|
147
|
+
width = get_width
|
148
|
+
if line.length == width - 1
|
149
|
+
@out.print(line + eol)
|
150
|
+
@out.flush
|
151
|
+
elsif line.length >= width
|
152
|
+
@terminal_width = [@terminal_width - (line.length - width + 1), 0].max
|
153
|
+
if @terminal_width == 0 then @out.print(line + eol) else show end
|
154
|
+
else # line.length < width - 1
|
155
|
+
@terminal_width += width - line.length + 1
|
156
|
+
show
|
157
|
+
end
|
158
|
+
@previous_time = Time.now
|
159
|
+
end
|
160
|
+
|
161
|
+
def show_if_needed
|
162
|
+
if @total.zero?
|
163
|
+
cur_percentage = 100
|
164
|
+
prev_percentage = 0
|
165
|
+
else
|
166
|
+
cur_percentage = (@current * 100 / @total).to_i
|
167
|
+
prev_percentage = (@previous * 100 / @total).to_i
|
168
|
+
end
|
169
|
+
|
170
|
+
# Use "!=" instead of ">" to support negative changes
|
171
|
+
if cur_percentage != prev_percentage ||
|
172
|
+
Time.now - @previous_time >= 1 || @finished_p
|
173
|
+
show
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
public
|
178
|
+
def clear
|
179
|
+
@out.print "\r"
|
180
|
+
@out.print(" " * (get_width - 1))
|
181
|
+
@out.print "\r"
|
182
|
+
end
|
183
|
+
|
184
|
+
def finish
|
185
|
+
@current = @total
|
186
|
+
@finished_p = true
|
187
|
+
show
|
188
|
+
end
|
189
|
+
|
190
|
+
def finished?
|
191
|
+
@finished_p
|
192
|
+
end
|
193
|
+
|
194
|
+
def file_transfer_mode
|
195
|
+
@format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
|
196
|
+
end
|
197
|
+
|
198
|
+
def format= (format)
|
199
|
+
@format = format
|
200
|
+
end
|
201
|
+
|
202
|
+
def format_arguments= (arguments)
|
203
|
+
@format_arguments = arguments
|
204
|
+
end
|
205
|
+
|
206
|
+
def halt
|
207
|
+
@finished_p = true
|
208
|
+
show
|
209
|
+
end
|
210
|
+
|
211
|
+
def inc (step = 1)
|
212
|
+
@current += step
|
213
|
+
@current = @total if @current > @total
|
214
|
+
show_if_needed
|
215
|
+
@previous = @current
|
216
|
+
end
|
217
|
+
|
218
|
+
def set (count)
|
219
|
+
if count < 0 || count > @total
|
220
|
+
raise "invalid count: #{count} (total: #{@total})"
|
221
|
+
end
|
222
|
+
@current = count
|
223
|
+
show_if_needed
|
224
|
+
@previous = @current
|
225
|
+
end
|
226
|
+
|
227
|
+
def inspect
|
228
|
+
"#<ProgressBar:#{@current}/#{@total}>"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class ReversedProgressBar < ProgressBar
|
233
|
+
def do_percentage
|
234
|
+
100 - super
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'sequel'
|
3
|
+
require 'sequel/extensions/schema_dumper'
|
4
|
+
require 'sequel/extensions/migration'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Tapsoob
|
8
|
+
module Schema
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def dump(database_url)
|
12
|
+
db = Sequel.connect(database_url)
|
13
|
+
db.dump_schema_migration(:indexes => false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def dump_table(database_url, table)
|
17
|
+
table = table.to_sym
|
18
|
+
Sequel.connect(database_url) do |db|
|
19
|
+
<<END_MIG
|
20
|
+
Class.new(Sequel::Migration) do
|
21
|
+
def up
|
22
|
+
#{db.dump_table_schema(table.identifier, :indexes => false)}
|
23
|
+
end
|
24
|
+
|
25
|
+
def down
|
26
|
+
drop_table("#{table}") if @db.table_exists?("#{table}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
END_MIG
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def indexes(database_url)
|
34
|
+
db = Sequel.connect(database_url)
|
35
|
+
db.dump_indexes_migration
|
36
|
+
end
|
37
|
+
|
38
|
+
def indexes_individual(database_url)
|
39
|
+
idxs = {}
|
40
|
+
Sequel.connect(database_url) do |db|
|
41
|
+
tables = db.tables
|
42
|
+
tables.each do |table|
|
43
|
+
idxs[table] = db.send(:dump_table_indexes, table, :add_index, {}).split("\n")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
idxs.each do |table, indexes|
|
48
|
+
idxs[table] = indexes.map do |idx|
|
49
|
+
<<END_MIG
|
50
|
+
Class.new(Sequel::Migration) do
|
51
|
+
def up
|
52
|
+
#{idx}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
END_MIG
|
56
|
+
end
|
57
|
+
end
|
58
|
+
JSON.generate(idxs)
|
59
|
+
end
|
60
|
+
|
61
|
+
def load(database_url, schema)
|
62
|
+
Sequel.connect(database_url) do |db|
|
63
|
+
klass = eval(schema)
|
64
|
+
klass.apply(db, :down)
|
65
|
+
klass.apply(db, :up)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def load_indexes(database_url, indexes)
|
70
|
+
Sequel.connect(database_url) do |db|
|
71
|
+
eval(indexes).apply(db, :up)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset_db_sequences(database_url)
|
76
|
+
db = Sequel.connect(database_url)
|
77
|
+
return unless db.respond_to?(:reset_primary_key_sequence)
|
78
|
+
db.tables.each do |table|
|
79
|
+
db.reset_primary_key_sequence(table)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
require 'tapsoob/errors'
|
5
|
+
require 'tapsoob/chunksize'
|
6
|
+
require 'tapsoob/schema'
|
7
|
+
|
8
|
+
module Tapsoob
|
9
|
+
module Utils
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def windows?
|
13
|
+
return @windows if defined?(@windows)
|
14
|
+
require 'rbconfig'
|
15
|
+
@windows = !!(::RbConfig::CONFIG['host_os'] =~ /mswin|mingw/)
|
16
|
+
end
|
17
|
+
|
18
|
+
def bin(cmd)
|
19
|
+
cmd = "#{cmd}.cmd" if windows?
|
20
|
+
cmd
|
21
|
+
end
|
22
|
+
|
23
|
+
def checksum(data)
|
24
|
+
Zlib.crc32(data)
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_data?(data, crc32)
|
28
|
+
Zlib.crc32(data) == crc32.to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
def base64encode(data)
|
32
|
+
[data].pack("m")
|
33
|
+
end
|
34
|
+
|
35
|
+
def base64decode(data)
|
36
|
+
data.unpack("m").first
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_data(data, opts = {})
|
40
|
+
return {} if data.size == 0
|
41
|
+
string_columns = opts[:string_columns] || []
|
42
|
+
schema = opts[:schema] || []
|
43
|
+
table = opts[:table]
|
44
|
+
|
45
|
+
max_lengths = schema.inject({}) do |hash, (column, meta)|
|
46
|
+
if meta[:db_type] =~ /^varchar\((\d+)\)/
|
47
|
+
hash.update(column => $1.to_i)
|
48
|
+
end
|
49
|
+
hash
|
50
|
+
end
|
51
|
+
|
52
|
+
header = data[0].keys
|
53
|
+
only_data = data.collect do |row|
|
54
|
+
row = blobs_to_string(row, string_columns)
|
55
|
+
row.each do |column, data|
|
56
|
+
if data.to_s.length > (max_lengths[column] || data.to_s.length)
|
57
|
+
raise Tapsoob::InvalidData.new(<<-ERROR)
|
58
|
+
Detected data that exceeds the length limitation of its column. This is
|
59
|
+
generally due to the fact that SQLite does not enforce length restrictions.
|
60
|
+
|
61
|
+
Table : #{table}
|
62
|
+
Column : #{column}
|
63
|
+
Type : #{schema.detect{|s| s.first == column}.last[:db_type]}
|
64
|
+
Data : #{data}
|
65
|
+
ERROR
|
66
|
+
end
|
67
|
+
end
|
68
|
+
header.collect { |h| row[h] }
|
69
|
+
end
|
70
|
+
{ :header => header, :data => only_data }
|
71
|
+
end
|
72
|
+
|
73
|
+
# mysql text and blobs fields are handled the same way internally
|
74
|
+
# this is not true for other databases so we must check if the field is
|
75
|
+
# actually text and manually convert it back to a string
|
76
|
+
def incorrect_blobs(db, table)
|
77
|
+
return [] if (db.url =~ /mysql:\/\//).nil?
|
78
|
+
|
79
|
+
columns = []
|
80
|
+
db.schema(table).each do |data|
|
81
|
+
column, cdata = data
|
82
|
+
columns << column if cdata[:db_type] =~ /text/
|
83
|
+
end
|
84
|
+
columns
|
85
|
+
end
|
86
|
+
|
87
|
+
def blobs_to_string(row, columns)
|
88
|
+
return row if columns.size == 0
|
89
|
+
columns.each do |c|
|
90
|
+
row[c] = row[c].to_s if row[c].kind_of?(Sequel::SQL::Blob)
|
91
|
+
end
|
92
|
+
row
|
93
|
+
end
|
94
|
+
|
95
|
+
def calculate_chunksize(old_chunksize)
|
96
|
+
c = Tapsoob::Chunksize.new(old_chunksize)
|
97
|
+
|
98
|
+
begin
|
99
|
+
c.start_time = Time.now
|
100
|
+
c.time_in_db = yield c
|
101
|
+
rescue Errno::EPIPE
|
102
|
+
c.retries += 1
|
103
|
+
raise if c.retries > 2
|
104
|
+
|
105
|
+
# we got disconnected, the chunksize could be too large
|
106
|
+
# reset the chunksize based on the number of retries
|
107
|
+
c.reset_chunksize
|
108
|
+
retry
|
109
|
+
end
|
110
|
+
|
111
|
+
c.end_time = Time.now
|
112
|
+
c.calc_new_chunksize
|
113
|
+
end
|
114
|
+
|
115
|
+
def export_schema(dump_path, table, schema_data)
|
116
|
+
File.open(File.join(dump_path, "schemas", "#{table}.rb"), 'w') do |file|
|
117
|
+
file.write(schema_data)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def export_indexes(dump_path, table, index_data)
|
122
|
+
data = [index_data]
|
123
|
+
if File.exists?(File.join(dump_path, "indexes", "#{table}.json"))
|
124
|
+
previous_data = JSON.parse(File.read(File.join(dump_path, "indexes", "#{table}.json")))
|
125
|
+
data = data + previous_data
|
126
|
+
end
|
127
|
+
|
128
|
+
File.open(File.join(dump_path, "indexes", "#{table}.json"), 'w') do |file|
|
129
|
+
file.write(JSON.generate(data))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def export_rows(dump_path, table, row_data)
|
134
|
+
data = row_data
|
135
|
+
if File.exists?(File.join(dump_path, "data", "#{table}.json"))
|
136
|
+
previous_data = JSON.parse(File.read(File.join(dump_path, "data", "#{table}.json")))
|
137
|
+
data[:data] = previous_data["data"] + row_data[:data]
|
138
|
+
end
|
139
|
+
|
140
|
+
File.open(File.join(dump_path, "data", "#{table}.json"), 'w') do |file|
|
141
|
+
file.write(JSON.generate(data))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def load_schema(dump_path, database_url, table)
|
146
|
+
schema = File.join(dump_path, "schemas", "#{table}.rb")
|
147
|
+
schema_bin(:load, database_url, schema.to_s)
|
148
|
+
end
|
149
|
+
|
150
|
+
def load_indexes(database_url, index)
|
151
|
+
Tapsoob::Schema.load_indexes(database_url, index)
|
152
|
+
end
|
153
|
+
|
154
|
+
def schema_bin(*args)
|
155
|
+
bin_path = File.expand_path("#{File.dirname(__FILE__)}/../../bin/#{bin('schema')}")
|
156
|
+
`"#{bin_path}" #{args.map { |a| "'#{a}'" }.join(' ')}`
|
157
|
+
end
|
158
|
+
|
159
|
+
def primary_key(db, table)
|
160
|
+
db.schema(table).select { |c| c[1][:primary_key] }.map { |c| c[0] }
|
161
|
+
end
|
162
|
+
|
163
|
+
def single_integer_primary_key(db, table)
|
164
|
+
table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
|
165
|
+
keys = db.schema(table).select { |c| c[1][:primary_key] and c[1][:type] == :integer }
|
166
|
+
not keys.nil? and keys.size == 1
|
167
|
+
end
|
168
|
+
|
169
|
+
def order_by(db, table)
|
170
|
+
pkey = primary_key(db, table)
|
171
|
+
if pkey
|
172
|
+
pkey.kind_of?(Array) ? pkey : [pkey.to_sym]
|
173
|
+
else
|
174
|
+
table = table.to_sym.identifier unless table.kind_of?(Sequel::SQL::Identifier)
|
175
|
+
db[table].columns
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|