ricardochimal-taps 0.2.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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Ricardo Chimal, Jr
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.rdoc ADDED
@@ -0,0 +1,29 @@
1
+ = Taps -- simple database import/export app
2
+
3
+ A simple database agnostic import/export app to transfer data to/from a remote database.
4
+
5
+ == Usage: Server
6
+
7
+ $ taps server postgres://localdbuser:localdbpass@localhost/dbname user password
8
+
9
+ == Usage: Client
10
+
11
+ When you want to pull down a database from a taps server
12
+
13
+ $ taps pull postgres://dbuser:dbpassword@localhost/dbname http://user:password@example.com:5000
14
+
15
+ or when you want to push a local database to a taps server
16
+
17
+ $ taps push postgres://dbuser:dbpassword@localhost/dbname http://user:password@example.com:5000
18
+
19
+ == Meta
20
+
21
+ Maintained by Ricardo Chimal, Jr. (ricardo at heroku dot com)
22
+
23
+ Written by Ricardo Chimal, Jr. (ricardo at heroku dot com) and Adam Wiggins (adam at heroku dot com)
24
+
25
+ Early research and inspiration by Blake Mizerany
26
+
27
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
28
+
29
+ http://github.com/ricardochimal/taps
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "taps"
5
+ s.summary = %Q{simple database import/export app}
6
+ s.email = "ricardo@heroku.com"
7
+ s.homepage = "http://github.com/ricardochimal/taps"
8
+ s.description = "A simple database agnostic import/export app to transfer data to/from a remote database."
9
+ s.authors = ["Ricardo Chimal, Jr.", "Adam Wiggins"]
10
+
11
+ s.add_dependency 'sinatra', '~> 0.9.0'
12
+ s.add_dependency 'activerecord', '= 2.2.2'
13
+ s.add_dependency 'thor', '= 0.9.9'
14
+ s.add_dependency 'rest-client', '~> 0.9.0'
15
+ s.add_dependency 'sequel', '~> 2.10.0'
16
+
17
+ s.rubyforge_project = "taps"
18
+ s.rubygems_version = '1.3.1'
19
+
20
+ s.files = FileList['spec/*.rb'] + FileList['lib/**/*.rb'] + ['README.rdoc', 'LICENSE', 'VERSION.yml', 'Rakefile']
21
+ s.executables = ['taps', 'schema']
22
+ end
23
+ rescue LoadError
24
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
25
+ end
26
+
27
+ require 'rake/rdoctask'
28
+ Rake::RDocTask.new do |rdoc|
29
+ rdoc.rdoc_dir = 'rdoc'
30
+ rdoc.title = 'taps'
31
+ rdoc.options << '--line-numbers' << '--inline-source'
32
+ rdoc.rdoc_files.include('README*')
33
+ rdoc.rdoc_files.include('lib/**/*.rb')
34
+ end
35
+
36
+ begin
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |t|
39
+ t.libs << 'spec'
40
+ t.test_files = FileList['spec/*_spec.rb']
41
+ t.verbose = true
42
+ end
43
+ rescue LoadError
44
+ puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
+ end
46
+
47
+ desc "Run all specs"
48
+ task :spec do
49
+ system "bacon #{File.dirname(__FILE__)}/spec/*_spec.rb"
50
+ end
51
+
52
+ task :default => :spec
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :patch: 0
data/bin/schema ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ gem 'activerecord', '= 2.2.2'
5
+
6
+ require File.dirname(__FILE__) + '/../lib/taps/schema'
7
+
8
+ cmd = ARGV.shift.strip rescue ''
9
+ database_url = ARGV.shift.strip rescue ''
10
+
11
+ def show_usage_and_exit
12
+ puts <<EOTXT
13
+ schema dump <database_url>
14
+ schema indexes <database_url>
15
+ schema reset_db_sequences <database_url>
16
+ schema load <database_url> <schema_file>
17
+ schema load_indexes <database_url> <indexes_file>
18
+ EOTXT
19
+ exit(1)
20
+ end
21
+
22
+ if cmd == 'dump'
23
+ puts Taps::Schema.dump_without_indexes(database_url)
24
+ elsif cmd == 'indexes'
25
+ puts Taps::Schema.indexes(database_url)
26
+ elsif cmd == 'load_indexes'
27
+ filename = ARGV.shift.strip rescue ''
28
+ indexes = File.read(filename) rescue show_usage_and_exit
29
+ Taps::Schema.load_indexes(database_url, indexes)
30
+ elsif cmd == 'load'
31
+ filename = ARGV.shift.strip rescue ''
32
+ schema = File.read(filename) rescue show_usage_and_exit
33
+ Taps::Schema.load(database_url, schema)
34
+ elsif cmd == 'reset_db_sequences'
35
+ Taps::Schema.reset_db_sequences(database_url)
36
+ else
37
+ show_usage_and_exit
38
+ end
data/bin/taps ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ gem 'activerecord', '= 2.2.2'
5
+ gem 'thor', '= 0.9.9'
6
+ gem 'rest-client', '~> 0.9.0'
7
+ gem 'sinatra', '~> 0.9.0'
8
+ gem 'sequel', '~> 2.10.0'
9
+
10
+ require File.dirname(__FILE__) + '/../lib/taps/cli'
11
+
12
+ Taps::Cli.start
data/lib/taps/cli.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'thor'
2
+ require File.dirname(__FILE__) + '/config'
3
+
4
+ Taps::Config.taps_database_url = 'sqlite://taps.db'
5
+
6
+ module Taps
7
+ class Cli < Thor
8
+ desc "server <local_database_url> <login> <password>", "Start a taps database import/export server"
9
+ method_options(:port => :numeric)
10
+ def server(database_url, login, password)
11
+ Taps::Config.database_url = database_url
12
+ Taps::Config.login = login
13
+ Taps::Config.password = password
14
+
15
+ port = options[:port] || 5000
16
+
17
+ Taps::Config.verify_database_url
18
+
19
+ require File.dirname(__FILE__) + '/server'
20
+ Taps::Server.run!({
21
+ :port => port,
22
+ :environment => :production,
23
+ :logging => true
24
+ })
25
+ end
26
+
27
+ desc "pull <local_database_url> <remote_url>", "Pull a database from a taps server"
28
+ method_options(:chunksize => :numeric)
29
+ def pull(database_url, remote_url)
30
+ clientxfer(:cmd_receive, database_url, remote_url)
31
+ end
32
+
33
+ desc "push <local_database_url> <remote_url>", "Push a database to a taps server"
34
+ method_options(:chunksize => :numeric)
35
+ def push(database_url, remote_url)
36
+ clientxfer(:cmd_send, database_url, remote_url)
37
+ end
38
+
39
+ def clientxfer(method, database_url, remote_url)
40
+ if options[:chunksize]
41
+ Taps::Config.chunksize = options[:chunksize] < 100 ? 100 : options[:chunksize]
42
+ else
43
+ Taps::Config.chunksize = 1000
44
+ end
45
+ Taps::Config.database_url = database_url
46
+ Taps::Config.remote_url = remote_url
47
+
48
+ Taps::Config.verify_database_url
49
+
50
+ require File.dirname(__FILE__) + '/client_session'
51
+
52
+ Taps::ClientSession.quickstart do |session|
53
+ session.send(method)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,236 @@
1
+ require 'rest_client'
2
+ require 'sequel'
3
+ require 'zlib'
4
+
5
+ require File.dirname(__FILE__) + '/progress_bar'
6
+ require File.dirname(__FILE__) + '/config'
7
+ require File.dirname(__FILE__) + '/utils'
8
+
9
+ module Taps
10
+ class ClientSession
11
+ attr_reader :database_url, :remote_url, :default_chunksize
12
+
13
+ def initialize(database_url, remote_url, default_chunksize)
14
+ @database_url = database_url
15
+ @remote_url = remote_url
16
+ @default_chunksize = default_chunksize
17
+ end
18
+
19
+ def self.start(database_url, remote_url, default_chunksize, &block)
20
+ s = new(database_url, remote_url, default_chunksize)
21
+ yield s
22
+ s.close_session
23
+ end
24
+
25
+ def self.quickstart(&block)
26
+ start(Taps::Config.database_url, Taps::Config.remote_url, Taps::Config.chunksize) do |s|
27
+ yield s
28
+ end
29
+ end
30
+
31
+ def db
32
+ @db ||= Sequel.connect(database_url)
33
+ end
34
+
35
+ def server
36
+ @server ||= RestClient::Resource.new(remote_url)
37
+ end
38
+
39
+ def session_resource
40
+ @session_resource ||= open_session
41
+ end
42
+
43
+ def open_session
44
+ uri = server['sessions'].post('', :taps_version => Taps::VERSION)
45
+ server[uri]
46
+ end
47
+
48
+ def close_session
49
+ @session_resource.delete(:taps_version => Taps::VERSION) if @session_resource
50
+ end
51
+
52
+ def cmd_send
53
+ verify_server
54
+ cmd_send_schema
55
+ cmd_send_data
56
+ cmd_send_indexes
57
+ cmd_send_reset_sequences
58
+ end
59
+
60
+ def cmd_send_indexes
61
+ puts "Sending schema indexes to remote taps server #{remote_url} from local database #{database_url}"
62
+
63
+ index_data = `#{File.dirname(__FILE__)}/../../bin/schema indexes #{database_url}`
64
+ session_resource['indexes'].post(index_data, :taps_version => Taps::VERSION)
65
+ end
66
+
67
+ def cmd_send_schema
68
+ puts "Sending schema to remote taps server #{remote_url} from local database #{database_url}"
69
+
70
+ schema_data = `#{File.dirname(__FILE__)}/../../bin/schema dump #{database_url}`
71
+ session_resource['schema'].post(schema_data, :taps_version => Taps::VERSION)
72
+ end
73
+
74
+ def cmd_send_reset_sequences
75
+ puts "Resetting db sequences in remote taps server at #{remote_url}"
76
+
77
+ session_resource["reset_sequences"].post('', :taps_version => Taps::VERSION)
78
+ end
79
+
80
+ def cmd_send_data
81
+ puts "Sending schema and data from local database #{database_url} to remote taps server at #{remote_url}"
82
+
83
+ db.tables.each do |table_name|
84
+ table = db[table_name]
85
+ count = table.count
86
+ columns = table.columns
87
+ order = columns.include?(:id) ? :id : columns.first
88
+ chunksize = self.default_chunksize
89
+
90
+ progress = ProgressBar.new(table_name.to_s, count)
91
+
92
+ offset = 0
93
+ loop do
94
+ rows = Taps::Utils.format_data(table.order(order).limit(chunksize, offset).all)
95
+ break if rows == { }
96
+
97
+ gzip_data = Taps::Utils.gzip(Marshal.dump(rows))
98
+
99
+ chunksize = Taps::Utils.calculate_chunksize(chunksize) do
100
+ begin
101
+ session_resource["tables/#{table_name}"].post(gzip_data,
102
+ :taps_version => Taps::VERSION,
103
+ :content_type => 'application/octet-stream',
104
+ :taps_checksum => Taps::Utils.checksum(gzip_data).to_s)
105
+ rescue RestClient::RequestFailed => e
106
+ # retry the same data, it got corrupted somehow.
107
+ if e.http_code == 412
108
+ next
109
+ end
110
+ raise
111
+ end
112
+ end
113
+
114
+ progress.inc(rows[:data].size)
115
+ offset += rows[:data].size
116
+ end
117
+
118
+ progress.finish
119
+ end
120
+ end
121
+
122
+ def cmd_receive
123
+ verify_server
124
+ cmd_receive_schema
125
+ cmd_receive_data
126
+ cmd_receive_indexes
127
+ cmd_reset_sequences
128
+ end
129
+
130
+ def cmd_receive_data
131
+ puts "Receiving data from remote taps server #{remote_url} into local database #{database_url}"
132
+
133
+ tables_with_counts, record_count = fetch_tables_info
134
+
135
+ puts "#{tables_with_counts.size} tables, #{format_number(record_count)} records"
136
+
137
+ tables_with_counts.each do |table_name, count|
138
+ table = db[table_name.to_sym]
139
+ chunksize = default_chunksize
140
+
141
+ progress = ProgressBar.new(table_name.to_s, count)
142
+
143
+ offset = 0
144
+ loop do
145
+ begin
146
+ chunksize, rows = fetch_table_rows(table_name, chunksize, offset)
147
+ rescue CorruptedData
148
+ next
149
+ end
150
+ break if rows == { }
151
+
152
+ table.multi_insert(rows[:header], rows[:data])
153
+
154
+ progress.inc(rows[:data].size)
155
+ offset += rows[:data].size
156
+ end
157
+
158
+ progress.finish
159
+ end
160
+ end
161
+
162
+ class CorruptedData < Exception; end
163
+
164
+ def fetch_table_rows(table_name, chunksize, offset)
165
+ response = nil
166
+ chunksize = Taps::Utils.calculate_chunksize(chunksize) do
167
+ response = session_resource["tables/#{table_name}/#{chunksize}?offset=#{offset}"].get(:taps_version => Taps::VERSION)
168
+ end
169
+ raise CorruptedData unless Taps::Utils.valid_data?(response.to_s, response.headers[:taps_checksum])
170
+
171
+ rows = Marshal.load(Taps::Utils.gunzip(response.to_s))
172
+ [chunksize, rows]
173
+ end
174
+
175
+ def fetch_tables_info
176
+ retries = 0
177
+ max_retries = 1
178
+ begin
179
+ tables_with_counts = Marshal.load(session_resource['tables'].get(:taps_version => Taps::VERSION))
180
+ record_count = tables_with_counts.values.inject(0) { |a,c| a += c }
181
+ rescue RestClient::Exception
182
+ retries += 1
183
+ retry if retries <= max_retries
184
+ puts "Unable to fetch tables information from #{remote_url}. Please check the server log."
185
+ exit(1)
186
+ end
187
+
188
+ [ tables_with_counts, record_count ]
189
+ end
190
+
191
+ def cmd_receive_schema
192
+ puts "Receiving schema from remote taps server #{remote_url} into local database #{database_url}"
193
+
194
+ schema_data = session_resource['schema'].get(:taps_version => Taps::VERSION)
195
+ puts Taps::Utils.load_schema(database_url, schema_data)
196
+ end
197
+
198
+ def cmd_receive_indexes
199
+ puts "Receiving schema indexes from remote taps server #{remote_url} into local database #{database_url}"
200
+
201
+ index_data = session_resource['indexes'].get(:taps_version => Taps::VERSION)
202
+
203
+ puts Taps::Utils.load_indexes(database_url, index_data)
204
+ end
205
+
206
+ def cmd_reset_sequences
207
+ puts "Resetting db sequences in #{database_url}"
208
+
209
+ puts `#{File.dirname(__FILE__)}/../../bin/schema reset_db_sequences #{database_url}`
210
+ end
211
+
212
+ def format_number(num)
213
+ num.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
214
+ end
215
+
216
+ def verify_server
217
+ begin
218
+ server['/'].get(:taps_version => Taps::VERSION)
219
+ rescue RestClient::RequestFailed => e
220
+ if e.http_code == 417
221
+ puts "#{remote_url} is running a different version of taps."
222
+ puts "#{e.response.body}"
223
+ exit(1)
224
+ else
225
+ raise
226
+ end
227
+ rescue RestClient::Unauthorized
228
+ puts "Bad credentials given for #{remote_url}"
229
+ exit(1)
230
+ rescue Errno::ECONNREFUSED
231
+ puts "Can't connect to #{remote_url}. Please check that it's running"
232
+ exit(1)
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,23 @@
1
+ require 'sequel'
2
+
3
+ module Taps
4
+
5
+ VERSION = '0.2.0'
6
+
7
+ class Config
8
+ class << self
9
+ attr_accessor :taps_database_url
10
+ attr_accessor :login, :password, :database_url, :remote_url
11
+ attr_accessor :chunksize
12
+
13
+ def verify_database_url
14
+ db = Sequel.connect(self.database_url)
15
+ db.tables
16
+ db.disconnect
17
+ rescue Object => e
18
+ puts "Failed to connect to database:\n #{e.class} -> #{e}"
19
+ exit 1
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ Sequel::Model.db = Sequel.connect(Taps::Config.taps_database_url)
2
+
3
+ class DbSession < Sequel::Model
4
+ set_schema do
5
+ primary_key :id
6
+ text :key
7
+ text :database_url
8
+ timestamp :started_at
9
+ timestamp :last_access
10
+ end
11
+
12
+ def connection
13
+ @@connections ||= {}
14
+ @@connections[key] ||= Sequel.connect(database_url)
15
+ end
16
+
17
+ def disconnect
18
+ if defined? @@connections and @@connections[key]
19
+ @@connections[key].disconnect
20
+ @@connections.delete key
21
+ end
22
+ end
23
+ end
24
+
25
+ DbSession.create_table! unless DbSession.table_exists?
@@ -0,0 +1,236 @@
1
+ #
2
+ # Ruby/ProgressBar - a text progress bar library
3
+ #
4
+ # Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
5
+ # All rights reserved.
6
+ # This is free software with ABSOLUTELY NO WARRANTY.
7
+ #
8
+ # You can redistribute it and/or modify it under the terms
9
+ # of Ruby's license.
10
+ #
11
+
12
+ class ProgressBar
13
+ VERSION = "0.9"
14
+
15
+ def initialize (title, total, out = STDERR)
16
+ @title = title
17
+ @total = total
18
+ @out = out
19
+ @terminal_width = 80
20
+ @bar_mark = "o"
21
+ @current = 0
22
+ @previous = 0
23
+ @finished_p = false
24
+ @start_time = Time.now
25
+ @previous_time = @start_time
26
+ @title_width = 14
27
+ @format = "%-#{@title_width}s %3d%% %s %s"
28
+ @format_arguments = [:title, :percentage, :bar, :stat]
29
+ clear
30
+ show
31
+ end
32
+ attr_reader :title
33
+ attr_reader :current
34
+ attr_reader :total
35
+ attr_accessor :start_time
36
+
37
+ private
38
+ def fmt_bar
39
+ bar_width = do_percentage * @terminal_width / 100
40
+ sprintf("|%s%s|",
41
+ @bar_mark * bar_width,
42
+ " " * (@terminal_width - bar_width))
43
+ end
44
+
45
+ def fmt_percentage
46
+ do_percentage
47
+ end
48
+
49
+ def fmt_stat
50
+ if @finished_p then elapsed else eta end
51
+ end
52
+
53
+ def fmt_stat_for_file_transfer
54
+ if @finished_p then
55
+ sprintf("%s %s %s", bytes, transfer_rate, elapsed)
56
+ else
57
+ sprintf("%s %s %s", bytes, transfer_rate, eta)
58
+ end
59
+ end
60
+
61
+ def fmt_title
62
+ @title[0,(@title_width - 1)] + ":"
63
+ end
64
+
65
+ def convert_bytes (bytes)
66
+ if bytes < 1024
67
+ sprintf("%6dB", bytes)
68
+ elsif bytes < 1024 * 1000 # 1000kb
69
+ sprintf("%5.1fKB", bytes.to_f / 1024)
70
+ elsif bytes < 1024 * 1024 * 1000 # 1000mb
71
+ sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
72
+ else
73
+ sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
74
+ end
75
+ end
76
+
77
+ def transfer_rate
78
+ bytes_per_second = @current.to_f / (Time.now - @start_time)
79
+ sprintf("%s/s", convert_bytes(bytes_per_second))
80
+ end
81
+
82
+ def bytes
83
+ convert_bytes(@current)
84
+ end
85
+
86
+ def format_time (t)
87
+ t = t.to_i
88
+ sec = t % 60
89
+ min = (t / 60) % 60
90
+ hour = t / 3600
91
+ sprintf("%02d:%02d:%02d", hour, min, sec);
92
+ end
93
+
94
+ # ETA stands for Estimated Time of Arrival.
95
+ def eta
96
+ if @current == 0
97
+ "ETA: --:--:--"
98
+ else
99
+ elapsed = Time.now - @start_time
100
+ eta = elapsed * @total / @current - elapsed;
101
+ sprintf("ETA: %s", format_time(eta))
102
+ end
103
+ end
104
+
105
+ def elapsed
106
+ elapsed = Time.now - @start_time
107
+ sprintf("Time: %s", format_time(elapsed))
108
+ end
109
+
110
+ def eol
111
+ if @finished_p then "\n" else "\r" end
112
+ end
113
+
114
+ def do_percentage
115
+ if @total.zero?
116
+ 100
117
+ else
118
+ @current * 100 / @total
119
+ end
120
+ end
121
+
122
+ def get_width
123
+ # FIXME: I don't know how portable it is.
124
+ default_width = 80
125
+ begin
126
+ tiocgwinsz = 0x5413
127
+ data = [0, 0, 0, 0].pack("SSSS")
128
+ if @out.ioctl(tiocgwinsz, data) >= 0 then
129
+ rows, cols, xpixels, ypixels = data.unpack("SSSS")
130
+ if cols >= 0 then cols else default_width end
131
+ else
132
+ default_width
133
+ end
134
+ rescue Exception
135
+ default_width
136
+ end
137
+ end
138
+
139
+ def show
140
+ arguments = @format_arguments.map {|method|
141
+ method = sprintf("fmt_%s", method)
142
+ send(method)
143
+ }
144
+ line = sprintf(@format, *arguments)
145
+
146
+ width = get_width
147
+ if line.length == width - 1
148
+ @out.print(line + eol)
149
+ @out.flush
150
+ elsif line.length >= width
151
+ @terminal_width = [@terminal_width - (line.length - width + 1), 0].max
152
+ if @terminal_width == 0 then @out.print(line + eol) else show end
153
+ else # line.length < width - 1
154
+ @terminal_width += width - line.length + 1
155
+ show
156
+ end
157
+ @previous_time = Time.now
158
+ end
159
+
160
+ def show_if_needed
161
+ if @total.zero?
162
+ cur_percentage = 100
163
+ prev_percentage = 0
164
+ else
165
+ cur_percentage = (@current * 100 / @total).to_i
166
+ prev_percentage = (@previous * 100 / @total).to_i
167
+ end
168
+
169
+ # Use "!=" instead of ">" to support negative changes
170
+ if cur_percentage != prev_percentage ||
171
+ Time.now - @previous_time >= 1 || @finished_p
172
+ show
173
+ end
174
+ end
175
+
176
+ public
177
+ def clear
178
+ @out.print "\r"
179
+ @out.print(" " * (get_width - 1))
180
+ @out.print "\r"
181
+ end
182
+
183
+ def finish
184
+ @current = @total
185
+ @finished_p = true
186
+ show
187
+ end
188
+
189
+ def finished?
190
+ @finished_p
191
+ end
192
+
193
+ def file_transfer_mode
194
+ @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
195
+ end
196
+
197
+ def format= (format)
198
+ @format = format
199
+ end
200
+
201
+ def format_arguments= (arguments)
202
+ @format_arguments = arguments
203
+ end
204
+
205
+ def halt
206
+ @finished_p = true
207
+ show
208
+ end
209
+
210
+ def inc (step = 1)
211
+ @current += step
212
+ @current = @total if @current > @total
213
+ show_if_needed
214
+ @previous = @current
215
+ end
216
+
217
+ def set (count)
218
+ if count < 0 || count > @total
219
+ raise "invalid count: #{count} (total: #{@total})"
220
+ end
221
+ @current = count
222
+ show_if_needed
223
+ @previous = @current
224
+ end
225
+
226
+ def inspect
227
+ "#<ProgressBar:#{@current}/#{@total}>"
228
+ end
229
+ end
230
+
231
+ class ReversedProgressBar < ProgressBar
232
+ def do_percentage
233
+ 100 - super
234
+ end
235
+ end
236
+
@@ -0,0 +1,82 @@
1
+ require 'active_record'
2
+ require 'active_support'
3
+ require 'stringio'
4
+ require 'uri'
5
+
6
+ module Taps
7
+ module Schema
8
+ extend self
9
+
10
+ def create_config(url)
11
+ uri = URI.parse(url)
12
+ adapter = uri.scheme
13
+ adapter = 'postgresql' if adapter == 'postgres'
14
+ adapter = 'sqlite3' if adapter == 'sqlite'
15
+ config = {
16
+ 'adapter' => adapter,
17
+ 'database' => uri.path.blank? ? uri.host : uri.path.split('/')[1],
18
+ 'username' => uri.user,
19
+ 'password' => uri.password,
20
+ 'host' => uri.host,
21
+ }
22
+ end
23
+
24
+ def connection(database_url)
25
+ config = create_config(database_url)
26
+ ActiveRecord::Base.establish_connection(config)
27
+ end
28
+
29
+ def dump(database_url)
30
+ connection(database_url)
31
+
32
+ stream = StringIO.new
33
+ ActiveRecord::SchemaDumper.ignore_tables = []
34
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
35
+ stream.string
36
+ end
37
+
38
+ def dump_without_indexes(database_url)
39
+ schema = dump(database_url)
40
+ schema.split("\n").collect do |line|
41
+ if line =~ /^\s+add_index/
42
+ line = "##{line}"
43
+ end
44
+ line
45
+ end.join("\n")
46
+ end
47
+
48
+ def indexes(database_url)
49
+ schema = dump(database_url)
50
+ schema.split("\n").collect do |line|
51
+ line if line =~ /^\s+add_index/
52
+ end.uniq.join("\n")
53
+ end
54
+
55
+ def load(database_url, schema)
56
+ connection(database_url)
57
+ eval(schema)
58
+ ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations") rescue nil
59
+ end
60
+
61
+ def load_indexes(database_url, indexes)
62
+ connection(database_url)
63
+
64
+ schema =<<EORUBY
65
+ ActiveRecord::Schema.define do
66
+ #{indexes}
67
+ end
68
+ EORUBY
69
+ eval(schema)
70
+ end
71
+
72
+ def reset_db_sequences(database_url)
73
+ connection(database_url)
74
+
75
+ if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
76
+ ActiveRecord::Base.connection.tables.each do |table|
77
+ ActiveRecord::Base.connection.reset_pk_sequence!(table)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,141 @@
1
+ require 'sinatra/base'
2
+ require File.dirname(__FILE__) + '/config'
3
+ require File.dirname(__FILE__) + '/utils'
4
+ require File.dirname(__FILE__) + '/db_session'
5
+
6
+ module Taps
7
+ class Server < Sinatra::Base
8
+ use Rack::Auth::Basic do |login, password|
9
+ login == Taps::Config.login && password == Taps::Config.password
10
+ end
11
+
12
+ error do
13
+ e = request.env['sinatra.error']
14
+ puts e.to_s
15
+ puts e.backtrace.join("\n")
16
+ "Application error"
17
+ end
18
+
19
+ before do
20
+ unless request.env['HTTP_TAPS_VERSION'] == Taps::VERSION
21
+ halt 417, "Taps version #{Taps::VERSION} is required for this server"
22
+ end
23
+ end
24
+
25
+ get '/' do
26
+ "hello"
27
+ end
28
+
29
+ post '/sessions' do
30
+ key = rand(9999999999).to_s
31
+ database_url = Taps::Config.database_url || request.body.string
32
+
33
+ DbSession.create(:key => key, :database_url => database_url, :started_at => Time.now, :last_access => Time.now)
34
+
35
+ "/sessions/#{key}"
36
+ end
37
+
38
+ post '/sessions/:key/tables/:table' do
39
+ session = DbSession.filter(:key => params[:key]).first
40
+ halt 404 unless session
41
+
42
+ gzip_data = request.body.read
43
+ halt 412 unless Taps::Utils.valid_data?(gzip_data, request.env['HTTP_TAPS_CHECKSUM'])
44
+
45
+ rows = Marshal.load(Taps::Utils.gunzip(gzip_data))
46
+
47
+ db = session.connection
48
+ table = db[params[:table].to_sym]
49
+ table.multi_insert(rows[:header], rows[:data])
50
+
51
+ "#{rows[:data].size}"
52
+ end
53
+
54
+ post '/sessions/:key/reset_sequences' do
55
+ session = DbSession.filter(:key => params[:key]).first
56
+ halt 404 unless session
57
+
58
+ schema_app = File.dirname(__FILE__) + '/../../bin/schema'
59
+ `#{schema_app} reset_db_sequences #{session.database_url}`
60
+ end
61
+
62
+ post '/sessions/:key/schema' do
63
+ session = DbSession.filter(:key => params[:key]).first
64
+ halt 404 unless session
65
+
66
+ schema_data = request.body.read
67
+ Taps::Utils.load_schema(session.database_url, schema_data)
68
+ end
69
+
70
+ post '/sessions/:key/indexes' do
71
+ session = DbSession.filter(:key => params[:key]).first
72
+ halt 404 unless session
73
+
74
+ index_data = request.body.read
75
+ Taps::Utils.load_indexes(session.database_url, index_data)
76
+ end
77
+
78
+ get '/sessions/:key/schema' do
79
+ session = DbSession.filter(:key => params[:key]).first
80
+ halt 404 unless session
81
+
82
+ schema_app = File.dirname(__FILE__) + '/../../bin/schema'
83
+ `#{schema_app} dump #{session.database_url}`
84
+ end
85
+
86
+ get '/sessions/:key/indexes' do
87
+ session = DbSession.filter(:key => params[:key]).first
88
+ halt 404 unless session
89
+
90
+ schema_app = File.dirname(__FILE__) + '/../../bin/schema'
91
+ `#{schema_app} indexes #{session.database_url}`
92
+ end
93
+
94
+ get '/sessions/:key/tables' do
95
+ session = DbSession.filter(:key => params[:key]).first
96
+ halt 404 unless session
97
+
98
+ db = session.connection
99
+ tables = db.tables
100
+
101
+ tables_with_counts = tables.inject({}) do |accum, table|
102
+ accum[table] = db[table].count
103
+ accum
104
+ end
105
+
106
+ Marshal.dump(tables_with_counts)
107
+ end
108
+
109
+ get '/sessions/:key/tables/:table/:chunk' do
110
+ session = DbSession.filter(:key => params[:key]).first
111
+ halt 404 unless session
112
+
113
+ chunk = params[:chunk].to_i
114
+ chunk = 500 if chunk < 1
115
+
116
+ offset = params[:offset].to_i
117
+ offset = 0 if offset < 0
118
+
119
+ db = session.connection
120
+ table = db[params[:table].to_sym]
121
+ columns = table.columns
122
+ order = columns.include?(:id) ? :id : columns.first
123
+ raw_data = Marshal.dump(Taps::Utils.format_data(table.order(order).limit(chunk, offset).all))
124
+ gzip_data = Taps::Utils.gzip(raw_data)
125
+ response['Taps-Checksum'] = Taps::Utils.checksum(gzip_data).to_s
126
+ response['Content-Type'] = "application/octet-stream"
127
+ gzip_data
128
+ end
129
+
130
+ delete '/sessions/:key' do
131
+ session = DbSession.filter(:key => params[:key]).first
132
+ halt 404 unless session
133
+
134
+ session.disconnect
135
+ session.destroy
136
+
137
+ "ok"
138
+ end
139
+
140
+ end
141
+ end
data/lib/taps/utils.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+ require 'time'
4
+ require 'tempfile'
5
+
6
+ module Taps
7
+ module Utils
8
+ extend self
9
+
10
+ def checksum(data)
11
+ Zlib.crc32(data)
12
+ end
13
+
14
+ def valid_data?(data, crc32)
15
+ Zlib.crc32(data) == crc32.to_i
16
+ end
17
+
18
+ def gzip(data)
19
+ io = StringIO.new
20
+ gz = Zlib::GzipWriter.new(io)
21
+ gz.write data
22
+ gz.close
23
+ io.string
24
+ end
25
+
26
+ def gunzip(gzip_data)
27
+ io = StringIO.new(gzip_data)
28
+ gz = Zlib::GzipReader.new(io)
29
+ data = gz.read
30
+ gz.close
31
+ data
32
+ end
33
+
34
+ def format_data(data)
35
+ return {} if data.size == 0
36
+ header = data[0].keys
37
+ only_data = data.collect do |row|
38
+ header.collect { |h| row[h] }
39
+ end
40
+ { :header => header, :data => only_data }
41
+ end
42
+
43
+ def calculate_chunksize(old_chunksize)
44
+ t1 = Time.now
45
+ yield
46
+ t2 = Time.now
47
+
48
+ diff = t2 - t1
49
+ new_chunksize = if diff > 3.0
50
+ (old_chunksize / 3).ceil
51
+ elsif diff > 1.1
52
+ old_chunksize - 100
53
+ elsif diff < 0.8
54
+ old_chunksize * 2
55
+ else
56
+ old_chunksize + 100
57
+ end
58
+ new_chunksize = 100 if new_chunksize < 100
59
+ new_chunksize
60
+ end
61
+
62
+ def load_schema(database_url, schema_data)
63
+ Tempfile.open('taps') do |tmp|
64
+ File.open(tmp.path, 'w') { |f| f.write(schema_data) }
65
+ `#{File.dirname(__FILE__)}/../../bin/schema load #{database_url} #{tmp.path}`
66
+ end
67
+ end
68
+
69
+ def load_indexes(database_url, index_data)
70
+ Tempfile.open('taps') do |tmp|
71
+ File.open(tmp.path, 'w') { |f| f.write(index_data) }
72
+ `#{File.dirname(__FILE__)}/../../bin/schema load_indexes #{database_url} #{tmp.path}`
73
+ end
74
+ end
75
+ end
76
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'mocha'
4
+
5
+ class Bacon::Context
6
+ include Mocha::Standalone
7
+
8
+ alias_method :old_it, :it
9
+ def it(description)
10
+ old_it(description) do
11
+ mocha_setup
12
+ yield
13
+ mocha_verify
14
+ mocha_teardown
15
+ end
16
+ end
17
+ end
18
+
19
+ require File.dirname(__FILE__) + '/../lib/taps/config'
20
+ Taps::Config.taps_database_url = 'sqlite://test.db'
21
+ Sequel.connect(Taps::Config.taps_database_url)
@@ -0,0 +1,82 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/client_session'
3
+
4
+ describe Taps::ClientSession do
5
+ before do
6
+ @client = Taps::ClientSession.new('sqlite://my.db', 'http://example.com:3000', 1000)
7
+ @client.stubs(:session_resource).returns(mock('session resource'))
8
+ end
9
+
10
+ it "starts a session and yields the session object to the block" do
11
+ Taps::ClientSession.start('x', 'y', 1000) do |session|
12
+ session.database_url.should == 'x'
13
+ session.remote_url.should == 'y'
14
+ session.default_chunksize.should == 1000
15
+ end
16
+ end
17
+
18
+ it "opens the local db connection via sequel and the database url" do
19
+ Sequel.expects(:connect).with('sqlite://my.db').returns(:con)
20
+ @client.db.should == :con
21
+ end
22
+
23
+ it "creates a restclient resource to the remote server" do
24
+ @client.server.url.should == 'http://example.com:3000'
25
+ end
26
+
27
+ it "verifies the db version, receive the schema, data, indexes, then reset the sequences" do
28
+ @client.expects(:verify_server)
29
+ @client.expects(:cmd_receive_schema)
30
+ @client.expects(:cmd_receive_data)
31
+ @client.expects(:cmd_receive_indexes)
32
+ @client.expects(:cmd_reset_sequences)
33
+ @client.cmd_receive.should.be.nil
34
+ end
35
+
36
+ it "checks the version of the server by seeing if it has access" do
37
+ @client.stubs(:server).returns(mock('server'))
38
+ @request = mock('request')
39
+ @client.server.expects(:[]).with('/').returns(@request)
40
+ @request.expects(:get).with({:taps_version => Taps::VERSION})
41
+
42
+ lambda { @client.verify_server }.should.not.raise
43
+ end
44
+
45
+ it "receives data from a remote taps server" do
46
+ @client.stubs(:puts)
47
+ @progressbar = mock('progressbar')
48
+ ProgressBar.stubs(:new).with('mytable', 2).returns(@progressbar)
49
+ @progressbar.stubs(:inc)
50
+ @progressbar.stubs(:finish)
51
+ @mytable = mock('mytable')
52
+ @client.expects(:fetch_tables_info).returns([ { :mytable => 2 }, 2 ])
53
+ @client.stubs(:db).returns(mock('db'))
54
+ @client.db.stubs(:[]).with(:mytable).returns(@mytable)
55
+ @client.expects(:fetch_table_rows).with(:mytable, 1000, 0).returns([ 1000, { :header => [:x, :y], :data => [[1, 2], [3, 4]] } ])
56
+ @client.expects(:fetch_table_rows).with(:mytable, 1000, 2).returns([ 1000, { }])
57
+ @mytable.expects(:multi_insert).with([:x, :y], [[1, 2], [3, 4]])
58
+
59
+ lambda { @client.cmd_receive_data }.should.not.raise
60
+ end
61
+
62
+ it "fetches tables info from taps server" do
63
+ @marshal_data = Marshal.dump({ :mytable => 2 })
64
+ @client.session_resource.stubs(:[]).with('tables').returns(mock('tables'))
65
+ @client.session_resource['tables'].stubs(:get).with(:taps_version => Taps::VERSION).returns(@marshal_data)
66
+ @client.fetch_tables_info.should == [ { :mytable => 2 }, 2 ]
67
+ end
68
+
69
+ it "fetches table rows given a chunksize and offset from taps server" do
70
+ @data = { :header => [ :x, :y ], :data => [ [1, 2], [3, 4] ] }
71
+ @gzip_data = Taps::Utils.gzip(Marshal.dump(@data))
72
+ Taps::Utils.stubs(:calculate_chunksize).with(1000).yields.returns(1000)
73
+
74
+ @response = mock('response')
75
+ @client.session_resource.stubs(:[]).with('tables/mytable/1000?offset=0').returns(mock('table resource'))
76
+ @client.session_resource['tables/mytable/1000?offset=0'].expects(:get).with(:taps_version => Taps::VERSION).returns(@response)
77
+ @response.stubs(:to_s).returns(@gzip_data)
78
+ @response.stubs(:headers).returns({ :taps_checksum => Taps::Utils.checksum(@gzip_data) })
79
+ @client.fetch_table_rows('mytable', 1000, 0).should == [ 1000, { :header => [:x, :y], :data => [[1, 2], [3, 4]] } ]
80
+ end
81
+ end
82
+
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/schema'
3
+
4
+ describe Taps::Schema do
5
+ before do
6
+ @connection = mock("AR connection")
7
+ ActiveRecord::Base.stubs(:connection).returns(@connection)
8
+ end
9
+
10
+ it "parses a database url and returns a config hash for activerecord" do
11
+ Taps::Schema.create_config("postgres://myuser:mypass@localhost/mydb").should == {
12
+ 'adapter' => 'postgresql',
13
+ 'database' => 'mydb',
14
+ 'username' => 'myuser',
15
+ 'password' => 'mypass',
16
+ 'host' => 'localhost'
17
+ }
18
+ end
19
+
20
+ it "translates sqlite in the database url to sqlite3" do
21
+ Taps::Schema.create_config("sqlite://myuser:mypass@localhost/mydb")['adapter'].should == 'sqlite3'
22
+ end
23
+
24
+ it "connects activerecord to the database" do
25
+ Taps::Schema.expects(:create_config).with("postgres://myuser:mypass@localhost/mydb").returns("db config")
26
+ ActiveRecord::Base.expects(:establish_connection).with("db config").returns(true)
27
+ Taps::Schema.connection("postgres://myuser:mypass@localhost/mydb").should == true
28
+ end
29
+
30
+ it "resets the db tables' primary keys" do
31
+ Taps::Schema.stubs(:connection)
32
+ ActiveRecord::Base.connection.expects(:respond_to?).with(:reset_pk_sequence!).returns(true)
33
+ ActiveRecord::Base.connection.stubs(:tables).returns(['table1'])
34
+ ActiveRecord::Base.connection.expects(:reset_pk_sequence!).with('table1')
35
+ should.not.raise { Taps::Schema.reset_db_sequences("postgres://myuser:mypass@localhost/mydb") }
36
+ end
37
+ end
38
+
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require 'sinatra'
3
+ require 'sinatra/test/bacon'
4
+
5
+ require File.dirname(__FILE__) + '/../lib/taps/server'
6
+
7
+ require 'pp'
8
+
9
+ describe Taps::Server do
10
+ before do
11
+ Taps::Config.login = 'taps'
12
+ Taps::Config.password = 'tpass'
13
+
14
+ @app = Taps::Server
15
+ @auth_header = "Basic " + ["taps:tpass"].pack("m*")
16
+ end
17
+
18
+ it "asks for http basic authentication" do
19
+ get '/'
20
+ status.should == 401
21
+ end
22
+
23
+ it "verifies the client taps version" do
24
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => Taps::VERSION })
25
+ status.should == 200
26
+ end
27
+
28
+ it "yells loudly if the client taps version doesn't match" do
29
+ get('/', { }, { 'HTTP_AUTHORIZATION' => @auth_header, 'HTTP_TAPS_VERSION' => '0.0.1' })
30
+ status.should == 417
31
+ end
32
+ end
33
+
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/../lib/taps/utils'
3
+
4
+ describe Taps::Utils do
5
+ it "gunzips a string" do
6
+ @hello_world = "\037\213\b\000R\261\207I\000\003\313H\315\311\311W(\317/\312I\001\000\205\021J\r\v\000\000\000"
7
+ Taps::Utils.gunzip(@hello_world).should == "hello world"
8
+ end
9
+
10
+ it "gzips and gunzips a string and returns the same string" do
11
+ Taps::Utils.gunzip(Taps::Utils.gzip("hello world")).should == "hello world"
12
+ end
13
+
14
+ it "generates a checksum using crc32" do
15
+ Taps::Utils.checksum("hello world").should == Zlib.crc32("hello world")
16
+ end
17
+
18
+ it "formats a data hash into one hash that contains an array of headers and an array of array of data" do
19
+ Taps::Utils.format_data([ { :x => 1, :y => 1 }, { :x => 2, :y => 2 } ]).should == { :header => [ :x, :y ], :data => [ [1, 1], [2, 2] ] }
20
+ end
21
+
22
+ it "scales chunksize down slowly when the time delta of the block is just over a second" do
23
+ Time.stubs(:now).returns(10.0).returns(11.5)
24
+ Taps::Utils.calculate_chunksize(1000) { }.should == 900
25
+ end
26
+
27
+ it "scales chunksize down fast when the time delta of the block is over 3 seconds" do
28
+ Time.stubs(:now).returns(10.0).returns(15.0)
29
+ Taps::Utils.calculate_chunksize(3000) { }.should == 1000
30
+ end
31
+
32
+ it "scales up chunksize fast when the time delta of the block is under 0.8 seconds" do
33
+ Time.stubs(:now).returns(10.0).returns(10.7)
34
+ Taps::Utils.calculate_chunksize(1000) { }.should == 2000
35
+ end
36
+
37
+ it "scales up chunksize slow when the time delta of the block is between 0.8 and 1.1 seconds" do
38
+ Time.stubs(:now).returns(10.0).returns(10.8)
39
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
40
+
41
+ Time.stubs(:now).returns(10.0).returns(11.1)
42
+ Taps::Utils.calculate_chunksize(1000) { }.should == 1100
43
+ end
44
+ end
45
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ricardochimal-taps
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Ricardo Chimal, Jr.
8
+ - Adam Wiggins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-02-06 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: sinatra
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ version_requirement:
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.2.2
33
+ version:
34
+ - !ruby/object:Gem::Dependency
35
+ name: thor
36
+ version_requirement:
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "="
40
+ - !ruby/object:Gem::Version
41
+ version: 0.9.9
42
+ version:
43
+ - !ruby/object:Gem::Dependency
44
+ name: rest-client
45
+ version_requirement:
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ~>
49
+ - !ruby/object:Gem::Version
50
+ version: 0.9.0
51
+ version:
52
+ - !ruby/object:Gem::Dependency
53
+ name: sequel
54
+ version_requirement:
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: 2.10.0
60
+ version:
61
+ description: A simple database agnostic import/export app to transfer data to/from a remote database.
62
+ email: ricardo@heroku.com
63
+ executables:
64
+ - taps
65
+ - schema
66
+ extensions: []
67
+
68
+ extra_rdoc_files: []
69
+
70
+ files:
71
+ - spec/base.rb
72
+ - spec/client_session_spec.rb
73
+ - spec/schema_spec.rb
74
+ - spec/server_spec.rb
75
+ - spec/utils_spec.rb
76
+ - lib/taps/config.rb
77
+ - lib/taps/db_session.rb
78
+ - lib/taps/progress_bar.rb
79
+ - lib/taps/server.rb
80
+ - lib/taps/utils.rb
81
+ - lib/taps/schema.rb
82
+ - lib/taps/client_session.rb
83
+ - lib/taps/cli.rb
84
+ - README.rdoc
85
+ - LICENSE
86
+ - VERSION.yml
87
+ - Rakefile
88
+ - bin/taps
89
+ - bin/schema
90
+ has_rdoc: true
91
+ homepage: http://github.com/ricardochimal/taps
92
+ post_install_message:
93
+ rdoc_options:
94
+ - --inline-source
95
+ - --charset=UTF-8
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ version:
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: "0"
109
+ version:
110
+ requirements: []
111
+
112
+ rubyforge_project: taps
113
+ rubygems_version: 1.2.0
114
+ signing_key:
115
+ specification_version: 2
116
+ summary: simple database import/export app
117
+ test_files: []
118
+