td-client 1.0.0-java
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/data/ca-bundle.crt +3448 -0
- data/lib/td-client.rb +1 -0
- data/lib/td/client.rb +606 -0
- data/lib/td/client/api.rb +707 -0
- data/lib/td/client/api/access_control.rb +74 -0
- data/lib/td/client/api/account.rb +45 -0
- data/lib/td/client/api/bulk_import.rb +184 -0
- data/lib/td/client/api/bulk_load.rb +172 -0
- data/lib/td/client/api/database.rb +50 -0
- data/lib/td/client/api/export.rb +38 -0
- data/lib/td/client/api/import.rb +38 -0
- data/lib/td/client/api/job.rb +390 -0
- data/lib/td/client/api/partial_delete.rb +27 -0
- data/lib/td/client/api/result.rb +46 -0
- data/lib/td/client/api/schedule.rb +120 -0
- data/lib/td/client/api/server_status.rb +21 -0
- data/lib/td/client/api/table.rb +132 -0
- data/lib/td/client/api/user.rb +134 -0
- data/lib/td/client/api_error.rb +37 -0
- data/lib/td/client/compat_gzip_reader.rb +22 -0
- data/lib/td/client/model.rb +816 -0
- data/lib/td/client/version.rb +5 -0
- data/lib/td/core_ext/openssl/ssl/sslcontext/set_params.rb +18 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/td/client/access_control_api_spec.rb +37 -0
- data/spec/td/client/account_api_spec.rb +34 -0
- data/spec/td/client/api_error_spec.rb +77 -0
- data/spec/td/client/api_spec.rb +269 -0
- data/spec/td/client/api_ssl_connection_spec.rb +109 -0
- data/spec/td/client/bulk_import_spec.rb +199 -0
- data/spec/td/client/bulk_load_spec.rb +401 -0
- data/spec/td/client/db_api_spec.rb +123 -0
- data/spec/td/client/export_api_spec.rb +51 -0
- data/spec/td/client/import_api_spec.rb +148 -0
- data/spec/td/client/job_api_spec.rb +833 -0
- data/spec/td/client/model_job_spec.rb +136 -0
- data/spec/td/client/model_schedule_spec.rb +26 -0
- data/spec/td/client/model_schema_spec.rb +134 -0
- data/spec/td/client/partial_delete_api_spec.rb +58 -0
- data/spec/td/client/result_api_spec.rb +77 -0
- data/spec/td/client/sched_api_spec.rb +109 -0
- data/spec/td/client/server_status_api_spec.rb +25 -0
- data/spec/td/client/spec_resources.rb +99 -0
- data/spec/td/client/table_api_spec.rb +226 -0
- data/spec/td/client/user_api_spec.rb +118 -0
- data/spec/td/client_sched_spec.rb +79 -0
- data/spec/td/client_spec.rb +46 -0
- metadata +271 -0
@@ -0,0 +1,707 @@
|
|
1
|
+
require 'td/client/api_error'
|
2
|
+
require 'td/client/version'
|
3
|
+
require 'td/client/api/access_control'
|
4
|
+
require 'td/client/api/account'
|
5
|
+
require 'td/client/api/bulk_import'
|
6
|
+
require 'td/client/api/bulk_load'
|
7
|
+
require 'td/client/api/database'
|
8
|
+
require 'td/client/api/export'
|
9
|
+
require 'td/client/api/import'
|
10
|
+
require 'td/client/api/job'
|
11
|
+
require 'td/client/api/partial_delete'
|
12
|
+
require 'td/client/api/result'
|
13
|
+
require 'td/client/api/schedule'
|
14
|
+
require 'td/client/api/server_status'
|
15
|
+
require 'td/client/api/table'
|
16
|
+
require 'td/client/api/user'
|
17
|
+
|
18
|
+
# For disabling SSLv3 connection in favor of POODLE Attack protection
|
19
|
+
require 'td/core_ext/openssl/ssl/sslcontext/set_params'
|
20
|
+
|
21
|
+
module TreasureData
|
22
|
+
|
23
|
+
class API
|
24
|
+
include API::AccessControl
|
25
|
+
include API::Account
|
26
|
+
include API::BulkImport
|
27
|
+
include API::BulkLoad
|
28
|
+
include API::Database
|
29
|
+
include API::Export
|
30
|
+
include API::Import
|
31
|
+
include API::Job
|
32
|
+
include API::PartialDelete
|
33
|
+
include API::Result
|
34
|
+
include API::Schedule
|
35
|
+
include API::ServerStatus
|
36
|
+
include API::Table
|
37
|
+
include API::User
|
38
|
+
|
39
|
+
DEFAULT_ENDPOINT = 'api.treasuredata.com'
|
40
|
+
DEFAULT_IMPORT_ENDPOINT = 'api-import.treasuredata.com'
|
41
|
+
|
42
|
+
# Deprecated. Use DEFAULT_ENDPOINT and DEFAULT_IMPORT_ENDPOINT instead
|
43
|
+
NEW_DEFAULT_ENDPOINT = DEFAULT_ENDPOINT
|
44
|
+
NEW_DEFAULT_IMPORT_ENDPOINT = DEFAULT_IMPORT_ENDPOINT
|
45
|
+
OLD_ENDPOINT = 'api.treasure-data.com'
|
46
|
+
|
47
|
+
class IncompleteError < APIError; end
|
48
|
+
|
49
|
+
# @param [String] apikey
|
50
|
+
# @param [Hash] opts
|
51
|
+
# for backward compatibility
|
52
|
+
def initialize(apikey, opts={})
|
53
|
+
require 'json'
|
54
|
+
require 'time'
|
55
|
+
require 'uri'
|
56
|
+
require 'net/http'
|
57
|
+
require 'net/https'
|
58
|
+
require 'time'
|
59
|
+
#require 'faraday' # faraday doesn't support streaming upload with httpclient yet so now disabled
|
60
|
+
require 'httpclient'
|
61
|
+
require 'zlib'
|
62
|
+
require 'stringio'
|
63
|
+
require 'cgi'
|
64
|
+
require 'msgpack'
|
65
|
+
|
66
|
+
@apikey = apikey
|
67
|
+
@user_agent = "TD-Client-Ruby: #{TreasureData::Client::VERSION}"
|
68
|
+
@user_agent = "#{opts[:user_agent]}; " + @user_agent if opts.has_key?(:user_agent)
|
69
|
+
|
70
|
+
endpoint = opts[:endpoint] || ENV['TD_API_SERVER'] || DEFAULT_ENDPOINT
|
71
|
+
uri = URI.parse(endpoint)
|
72
|
+
|
73
|
+
@connect_timeout = opts[:connect_timeout] || 60
|
74
|
+
@read_timeout = opts[:read_timeout] || 600
|
75
|
+
@send_timeout = opts[:send_timeout] || 600
|
76
|
+
@retry_post_requests = opts[:retry_post_requests] || false
|
77
|
+
@retry_delay = opts[:retry_delay] || 5
|
78
|
+
@max_cumul_retry_delay = opts[:max_cumul_retry_delay] || 600
|
79
|
+
|
80
|
+
case uri.scheme
|
81
|
+
when 'http', 'https'
|
82
|
+
@host = uri.host
|
83
|
+
@port = uri.port
|
84
|
+
# the opts[:ssl] option is ignored here, it's value
|
85
|
+
# overridden by the scheme of the endpoint URI
|
86
|
+
@ssl = (uri.scheme == 'https')
|
87
|
+
@base_path = uri.path.to_s
|
88
|
+
|
89
|
+
else
|
90
|
+
if uri.port
|
91
|
+
# invalid URI
|
92
|
+
raise "Invalid endpoint: #{endpoint}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# generic URI
|
96
|
+
@host, @port = endpoint.split(':', 2)
|
97
|
+
@port = @port.to_i
|
98
|
+
if opts[:ssl] === false || @host == TreasureData::API::OLD_ENDPOINT
|
99
|
+
# for backward compatibility, old endpoint specified without ssl option, use http
|
100
|
+
#
|
101
|
+
# opts[:ssl] would be nil if user doesn't specify ssl options,
|
102
|
+
# but connecting to https is the new default behavior (since 0.9)
|
103
|
+
# so check ssl option by `if opts[:ssl] === false` instead of `if opts[:ssl]`
|
104
|
+
# that means if user desire to use http, give `:ssl => false` for initializer such as API.new("APIKEY", :ssl => false)
|
105
|
+
@port = 80 if @port == 0
|
106
|
+
@ssl = false
|
107
|
+
else
|
108
|
+
@port = 443 if @port == 0
|
109
|
+
@ssl = true
|
110
|
+
end
|
111
|
+
@base_path = ''
|
112
|
+
end
|
113
|
+
|
114
|
+
@http_proxy = opts[:http_proxy] || ENV['HTTP_PROXY']
|
115
|
+
@headers = opts[:headers] || {}
|
116
|
+
@api = api_client("#{@ssl ? 'https' : 'http'}://#{@host}:#{@port}")
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO error check & raise appropriate errors
|
120
|
+
|
121
|
+
# @!attribute [r] apikey
|
122
|
+
attr_reader :apikey
|
123
|
+
|
124
|
+
MSGPACK_INT64_MAX = 2 ** 64 - 1
|
125
|
+
MSGPACK_INT64_MIN = -1 * (2 ** 63) # it's just same with -2**63, but for readability
|
126
|
+
|
127
|
+
# @param [Hash] record
|
128
|
+
# @param [IO] out
|
129
|
+
def self.normalized_msgpack(record, out = nil)
|
130
|
+
record.keys.each { |k|
|
131
|
+
v = record[k]
|
132
|
+
if v.kind_of?(Integer) && (v > MSGPACK_INT64_MAX || v < MSGPACK_INT64_MIN)
|
133
|
+
record[k] = v.to_s
|
134
|
+
end
|
135
|
+
}
|
136
|
+
if out
|
137
|
+
out << record.to_msgpack
|
138
|
+
out
|
139
|
+
else
|
140
|
+
record.to_msgpack
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param [String] target
|
145
|
+
# @param [Fixnum] min_len
|
146
|
+
# @param [Fixnum] max_len
|
147
|
+
# @param [String] name
|
148
|
+
def self.validate_name(target, min_len, max_len, name)
|
149
|
+
if !target.instance_of?(String) || target.empty?
|
150
|
+
raise ParameterValidationError,
|
151
|
+
"A valid target name is required"
|
152
|
+
end
|
153
|
+
|
154
|
+
name = name.to_s
|
155
|
+
if max_len
|
156
|
+
if name.length < min_len || name.length > max_len
|
157
|
+
raise ParameterValidationError,
|
158
|
+
"#{target.capitalize} name must be between #{min_len} and #{max_len} characters long. Got #{name.length} " +
|
159
|
+
(name.length == 1 ? "character" : "characters") + "."
|
160
|
+
end
|
161
|
+
else
|
162
|
+
if min_len == 1
|
163
|
+
if name.empty?
|
164
|
+
raise ParameterValidationError,
|
165
|
+
"Empty #{target} name is not allowed"
|
166
|
+
end
|
167
|
+
else
|
168
|
+
if name.length < min_len
|
169
|
+
raise ParameterValidationError,
|
170
|
+
"#{target.capitalize} name must be longer than #{min_len} characters. Got #{name.length} " +
|
171
|
+
(name.length == 1 ? "character" : "characters") + "."
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
unless name =~ /^([a-z0-9_]+)$/
|
177
|
+
raise ParameterValidationError,
|
178
|
+
"#{target.capitalize} name must only consist of lower-case alpha-numeric characters and '_'."
|
179
|
+
end
|
180
|
+
|
181
|
+
name
|
182
|
+
end
|
183
|
+
|
184
|
+
# @param [String] name
|
185
|
+
def self.validate_database_name(name)
|
186
|
+
validate_name("database", 3, 255, name)
|
187
|
+
end
|
188
|
+
|
189
|
+
# @param [String] name
|
190
|
+
def self.validate_table_name(name)
|
191
|
+
validate_name("table", 3, 255, name)
|
192
|
+
end
|
193
|
+
|
194
|
+
# @param [String] name
|
195
|
+
def self.validate_result_set_name(name)
|
196
|
+
validate_name("result set", 3, 255, name)
|
197
|
+
end
|
198
|
+
|
199
|
+
# @param [String] name
|
200
|
+
def self.validate_column_name(name)
|
201
|
+
target = 'column'
|
202
|
+
name = name.to_s
|
203
|
+
if name.empty?
|
204
|
+
raise ParameterValidationError,
|
205
|
+
"Empty #{target} name is not allowed"
|
206
|
+
end
|
207
|
+
name
|
208
|
+
end
|
209
|
+
|
210
|
+
# @param [String] name
|
211
|
+
def self.validate_sql_alias_name(name)
|
212
|
+
validate_name("sql_alias", 1, nil, name)
|
213
|
+
end
|
214
|
+
|
215
|
+
# @param [String] name
|
216
|
+
def self.normalize_database_name(name)
|
217
|
+
name = name.to_s
|
218
|
+
if name.empty?
|
219
|
+
raise "Empty name is not allowed"
|
220
|
+
end
|
221
|
+
if name.length < 3
|
222
|
+
name += "_" * (3 - name.length)
|
223
|
+
end
|
224
|
+
if 255 < name.length
|
225
|
+
name = name[0, 253] + "__"
|
226
|
+
end
|
227
|
+
name = name.downcase
|
228
|
+
name = name.gsub(/[^a-z0-9_]/, '_')
|
229
|
+
name
|
230
|
+
end
|
231
|
+
|
232
|
+
# @param [String] name
|
233
|
+
def self.normalize_table_name(name)
|
234
|
+
normalize_database_name(name)
|
235
|
+
end
|
236
|
+
|
237
|
+
# for fluent-plugin-td / td command to check table existence with import onlt user
|
238
|
+
# @return [String]
|
239
|
+
def self.create_empty_gz_data
|
240
|
+
io = StringIO.new
|
241
|
+
Zlib::GzipWriter.new(io).close
|
242
|
+
io.string
|
243
|
+
end
|
244
|
+
|
245
|
+
# @param [String] ssl_ca_file
|
246
|
+
def ssl_ca_file=(ssl_ca_file)
|
247
|
+
@ssl_ca_file = ssl_ca_file
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
# @param [String] url
|
253
|
+
# @param [Hash] params
|
254
|
+
# @yield [response]
|
255
|
+
def get(url, params=nil, &block)
|
256
|
+
guard_no_sslv3 do
|
257
|
+
do_get(url, params, &block)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# @param [String] url
|
262
|
+
# @param [Hash] params
|
263
|
+
# @yield [response]
|
264
|
+
def do_get(url, params=nil, &block)
|
265
|
+
client, header = new_client
|
266
|
+
client.send_timeout = @send_timeout
|
267
|
+
client.receive_timeout = @read_timeout
|
268
|
+
|
269
|
+
header['Accept-Encoding'] = 'deflate, gzip'
|
270
|
+
|
271
|
+
target = build_endpoint(url, @host)
|
272
|
+
|
273
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
274
|
+
puts "DEBUG: REST GET call:"
|
275
|
+
puts "DEBUG: header: " + header.to_s
|
276
|
+
puts "DEBUG: path: " + target.to_s
|
277
|
+
puts "DEBUG: params: " + params.to_s
|
278
|
+
end
|
279
|
+
|
280
|
+
# up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
|
281
|
+
retry_delay = @retry_delay
|
282
|
+
cumul_retry_delay = 0
|
283
|
+
|
284
|
+
# for both exceptions and 500+ errors retrying is enabled by default.
|
285
|
+
# The total number of retries cumulatively should not exceed 10 minutes / 600 seconds
|
286
|
+
response = nil
|
287
|
+
begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
|
288
|
+
begin
|
289
|
+
if block
|
290
|
+
current_total_chunk_size = 0
|
291
|
+
response = client.get(target, params, header) {|res, chunk|
|
292
|
+
current_total_chunk_size += chunk.bytesize
|
293
|
+
block.call(res, chunk, current_total_chunk_size)
|
294
|
+
}
|
295
|
+
|
296
|
+
# XXX ext/openssl raises EOFError in case where underlying connection causes an error,
|
297
|
+
# and msgpack-ruby that used in block handles it as an end of stream == no exception.
|
298
|
+
# Therefor, check content size.
|
299
|
+
validate_content_length!(response, current_total_chunk_size) if @ssl
|
300
|
+
else
|
301
|
+
response = client.get(target, params, header)
|
302
|
+
|
303
|
+
validate_content_length!(response, response.body.bytesize) if @ssl
|
304
|
+
end
|
305
|
+
|
306
|
+
status = response.code
|
307
|
+
# retry if the HTTP error code is 500 or higher and we did not run out of retrying attempts
|
308
|
+
if !block_given? && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
|
309
|
+
$stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
|
310
|
+
sleep retry_delay
|
311
|
+
cumul_retry_delay += retry_delay
|
312
|
+
retry_delay *= 2
|
313
|
+
redo # restart from beginning of do-while loop
|
314
|
+
end
|
315
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError, IncompleteError, HTTPClient::TimeoutError => e
|
316
|
+
if block_given?
|
317
|
+
raise e
|
318
|
+
end
|
319
|
+
$stderr.print "#{e.class}: #{e.message}. "
|
320
|
+
if cumul_retry_delay < @max_cumul_retry_delay
|
321
|
+
$stderr.puts "Retrying after #{retry_delay} seconds..."
|
322
|
+
sleep retry_delay
|
323
|
+
cumul_retry_delay += retry_delay
|
324
|
+
retry_delay *= 2
|
325
|
+
retry
|
326
|
+
else
|
327
|
+
$stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
|
328
|
+
raise e
|
329
|
+
end
|
330
|
+
rescue => e
|
331
|
+
raise e
|
332
|
+
end
|
333
|
+
end while false
|
334
|
+
|
335
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
336
|
+
puts "DEBUG: REST GET response:"
|
337
|
+
puts "DEBUG: header: " + response.header.to_s
|
338
|
+
puts "DEBUG: status: " + response.code.to_s
|
339
|
+
puts "DEBUG: body: " + response.body.to_s
|
340
|
+
end
|
341
|
+
|
342
|
+
body = block ? response.body : inflate_body(response)
|
343
|
+
|
344
|
+
return [response.code.to_s, body, response]
|
345
|
+
end
|
346
|
+
|
347
|
+
def validate_content_length!(response, body_size)
|
348
|
+
content_length = response.header['Content-Length'].first
|
349
|
+
raise IncompleteError if @ssl && content_length && content_length.to_i != body_size
|
350
|
+
end
|
351
|
+
|
352
|
+
def inflate_body(response)
|
353
|
+
return response.body if (ce = response.header['Content-Encoding']).empty?
|
354
|
+
|
355
|
+
if ce.include?('gzip')
|
356
|
+
infl = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
|
357
|
+
begin
|
358
|
+
infl.inflate(response.body)
|
359
|
+
ensure
|
360
|
+
infl.close
|
361
|
+
end
|
362
|
+
else
|
363
|
+
# NOTE maybe for content-encoding is msgpack.gz ?
|
364
|
+
Zlib::Inflate.inflate(response.body)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# @param [String] url
|
369
|
+
# @param [Hash] params
|
370
|
+
def post(url, params=nil, &block)
|
371
|
+
guard_no_sslv3 do
|
372
|
+
do_post(url, params, &block)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# @param [String] url
|
377
|
+
# @param [Hash] params
|
378
|
+
def do_post(url, params=nil, &block)
|
379
|
+
target = build_endpoint(url, @host)
|
380
|
+
|
381
|
+
client, header = new_client
|
382
|
+
client.send_timeout = @send_timeout
|
383
|
+
client.receive_timeout = @read_timeout
|
384
|
+
header['Accept-Encoding'] = 'gzip'
|
385
|
+
|
386
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
387
|
+
puts "DEBUG: REST POST call:"
|
388
|
+
puts "DEBUG: header: " + header.to_s
|
389
|
+
puts "DEBUG: path: " + target.to_s
|
390
|
+
puts "DEBUG: params: " + params.to_s
|
391
|
+
end
|
392
|
+
|
393
|
+
# up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
|
394
|
+
retry_delay = @retry_delay
|
395
|
+
cumul_retry_delay = 0
|
396
|
+
|
397
|
+
# for both exceptions and 500+ errors retrying can be enabled by initialization
|
398
|
+
# parameter 'retry_post_requests'. The total number of retries cumulatively
|
399
|
+
# should not exceed 10 minutes / 600 seconds
|
400
|
+
response = nil
|
401
|
+
begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
|
402
|
+
begin
|
403
|
+
response = client.post(target, params || {}, header)
|
404
|
+
|
405
|
+
# if the HTTP error code is 500 or higher and the user requested retrying
|
406
|
+
# on post request, attempt a retry
|
407
|
+
status = response.code.to_i
|
408
|
+
if @retry_post_requests && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
|
409
|
+
$stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
|
410
|
+
sleep retry_delay
|
411
|
+
cumul_retry_delay += retry_delay
|
412
|
+
retry_delay *= 2
|
413
|
+
redo # restart from beginning of do-while loop
|
414
|
+
end
|
415
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
|
416
|
+
$stderr.print "#{e.class}: #{e.message}. "
|
417
|
+
if @retry_post_requests && cumul_retry_delay < @max_cumul_retry_delay
|
418
|
+
$stderr.puts "Retrying after #{retry_delay} seconds..."
|
419
|
+
sleep retry_delay
|
420
|
+
cumul_retry_delay += retry_delay
|
421
|
+
retry_delay *= 2
|
422
|
+
retry
|
423
|
+
else
|
424
|
+
if @retry_post_requests
|
425
|
+
$stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
|
426
|
+
else
|
427
|
+
$stderr.puts ""
|
428
|
+
end
|
429
|
+
raise e
|
430
|
+
end
|
431
|
+
rescue => e
|
432
|
+
raise e
|
433
|
+
end
|
434
|
+
end while false
|
435
|
+
|
436
|
+
body = block ? response.body : inflate_body(response)
|
437
|
+
|
438
|
+
begin
|
439
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
440
|
+
puts "DEBUG: REST POST response:"
|
441
|
+
puts "DEBUG: header: " + response.header.to_s
|
442
|
+
puts "DEBUG: status: " + response.code.to_s
|
443
|
+
puts "DEBUG: body: <omitted>"
|
444
|
+
end
|
445
|
+
return [response.code.to_s, body, response]
|
446
|
+
ensure
|
447
|
+
# Disconnect keep-alive connection explicitly here, not by GC.
|
448
|
+
client.reset(target) rescue nil
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# @param [String] url
|
453
|
+
# @param [String, StringIO] stream
|
454
|
+
# @param [Fixnum] size
|
455
|
+
# @param [Hash] opts
|
456
|
+
def put(url, stream, size, opts = {})
|
457
|
+
client, header = new_client(opts)
|
458
|
+
client.send_timeout = @send_timeout
|
459
|
+
client.receive_timeout = @read_timeout
|
460
|
+
|
461
|
+
header['Content-Type'] = 'application/octet-stream'
|
462
|
+
header['Content-Length'] = size.to_s
|
463
|
+
|
464
|
+
body = if stream.class.name == 'StringIO'
|
465
|
+
stream.string
|
466
|
+
else
|
467
|
+
stream
|
468
|
+
end
|
469
|
+
target = build_endpoint(url, opts[:host] || @host)
|
470
|
+
|
471
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
472
|
+
puts "DEBUG: REST PUT call:"
|
473
|
+
puts "DEBUG: header: " + header.to_s
|
474
|
+
puts "DEBUG: target: " + target.to_s
|
475
|
+
puts "DEBUG: body: " + body.to_s
|
476
|
+
end
|
477
|
+
|
478
|
+
response = client.put(target, body, header)
|
479
|
+
begin
|
480
|
+
unless ENV['TD_CLIENT_DEBUG'].nil?
|
481
|
+
puts "DEBUG: REST PUT response:"
|
482
|
+
puts "DEBUG: header: " + response.header.to_s
|
483
|
+
puts "DEBUG: status: " + response.code.to_s
|
484
|
+
puts "DEBUG: body: <omitted>"
|
485
|
+
end
|
486
|
+
return [response.code.to_s, response.body, response]
|
487
|
+
ensure
|
488
|
+
# Disconnect keep-alive connection explicitly here, not by GC.
|
489
|
+
client.reset(target) rescue nil
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# @param [String] url
|
494
|
+
# @param [String] host
|
495
|
+
# @return [String]
|
496
|
+
def build_endpoint(url, host)
|
497
|
+
schema = @ssl ? 'https' : 'http'
|
498
|
+
"#{schema}://#{host}:#{@port}#{@base_path + url}"
|
499
|
+
end
|
500
|
+
|
501
|
+
# @yield Disable SSLv3 in given block
|
502
|
+
def guard_no_sslv3
|
503
|
+
key = :SET_SSL_OP_NO_SSLv3
|
504
|
+
backup = Thread.current[key]
|
505
|
+
begin
|
506
|
+
# Disable SSLv3 connection: See Net::HTTP hack at the bottom
|
507
|
+
Thread.current[key] = true
|
508
|
+
yield
|
509
|
+
ensure
|
510
|
+
# backup could be nil, but assigning nil to TLS means 'delete'
|
511
|
+
Thread.current[key] = backup
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# @param [Hash] opts
|
516
|
+
# @return [HTTPClient, Hash]
|
517
|
+
def new_client(opts = {})
|
518
|
+
client = HTTPClient.new(@http_proxy, @user_agent)
|
519
|
+
client.connect_timeout = @connect_timeout
|
520
|
+
|
521
|
+
if @ssl
|
522
|
+
client.ssl_config.add_trust_ca(ssl_ca_file)
|
523
|
+
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
524
|
+
# Disable SSLv3 connection in favor of POODLE Attack protection
|
525
|
+
client.ssl_config.options |= OpenSSL::SSL::OP_NO_SSLv3
|
526
|
+
end
|
527
|
+
|
528
|
+
header = {}
|
529
|
+
if @apikey
|
530
|
+
header['Authorization'] = "TD1 #{apikey}"
|
531
|
+
end
|
532
|
+
header['Date'] = Time.now.rfc2822
|
533
|
+
|
534
|
+
header.merge!(@headers)
|
535
|
+
|
536
|
+
return client, header
|
537
|
+
end
|
538
|
+
|
539
|
+
# @return [String]
|
540
|
+
def api_client(endpoint)
|
541
|
+
header = {}.merge(@headers)
|
542
|
+
header['Authorization'] = "TD1 #{apikey}" if @apikey
|
543
|
+
header['Content-Type'] = 'application/json; charset=utf-8'
|
544
|
+
client = HTTPClient.new(:proxy => @http_proxy, :agent_name => @user_agent, :base_url => endpoint, :default_header => header)
|
545
|
+
client.connect_timeout = @connect_timeout
|
546
|
+
client.send_timeout = @send_timeout
|
547
|
+
client.receive_timeout = @read_timeout
|
548
|
+
client.transparent_gzip_decompression = true
|
549
|
+
client.debug_dev = STDOUT unless ENV['TD_CLIENT_DEBUG'].nil?
|
550
|
+
client
|
551
|
+
end
|
552
|
+
|
553
|
+
def api(opt = {:retry_request => true}, &block)
|
554
|
+
retry_request = opt[:retry_request]
|
555
|
+
# up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
|
556
|
+
retry_delay = @retry_delay
|
557
|
+
retry_times = 0
|
558
|
+
cumul_retry_delay = 0
|
559
|
+
|
560
|
+
# for both exceptions and 500+ errors retrying can be enabled by initialization
|
561
|
+
# parameter 'retry_post_requests'. The total number of retries cumulatively
|
562
|
+
# should not exceed 10 minutes / 600 seconds
|
563
|
+
begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
|
564
|
+
begin
|
565
|
+
response = @api.instance_eval &block
|
566
|
+
|
567
|
+
# if the HTTP error code is 500 or higher and the user requested retrying
|
568
|
+
# on post request, attempt a retry
|
569
|
+
status = response.code.to_i
|
570
|
+
if retry_request && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
|
571
|
+
$stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
|
572
|
+
sleep retry_delay
|
573
|
+
cumul_retry_delay += retry_delay
|
574
|
+
retry_delay *= 2
|
575
|
+
redo # restart from beginning of do-while loop
|
576
|
+
end
|
577
|
+
return response
|
578
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
|
579
|
+
$stderr.print "#{e.class}: #{e.message}. "
|
580
|
+
if retry_request
|
581
|
+
if cumul_retry_delay < @max_cumul_retry_delay
|
582
|
+
$stderr.puts "Retrying after #{retry_delay} seconds..."
|
583
|
+
sleep retry_delay
|
584
|
+
cumul_retry_delay += retry_delay
|
585
|
+
retry_delay *= 2
|
586
|
+
retry_times += 1
|
587
|
+
retry
|
588
|
+
else
|
589
|
+
$stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
|
590
|
+
e.message << " (Retried #{retry_times} times in #{cumul_retry_delay} seconds)"
|
591
|
+
end
|
592
|
+
else
|
593
|
+
$stderr.puts "No retry should be performed."
|
594
|
+
end
|
595
|
+
raise e
|
596
|
+
end
|
597
|
+
end while false
|
598
|
+
end
|
599
|
+
|
600
|
+
def ssl_ca_file
|
601
|
+
@ssl_ca_file ||= File.join(File.dirname(__FILE__), '..', '..', '..', 'data', 'ca-bundle.crt')
|
602
|
+
end
|
603
|
+
|
604
|
+
# @param [response] res
|
605
|
+
# @return [String]
|
606
|
+
def get_error(res)
|
607
|
+
parse_error_response(res)['message']
|
608
|
+
end
|
609
|
+
|
610
|
+
def parse_error_response(res)
|
611
|
+
begin
|
612
|
+
error = JSON.load(res.body)
|
613
|
+
if error
|
614
|
+
error['message'] = error['error'] unless error['message']
|
615
|
+
else
|
616
|
+
error = {'message' => res.reason}
|
617
|
+
end
|
618
|
+
rescue JSON::ParserError
|
619
|
+
error = {'message' => res.body}
|
620
|
+
end
|
621
|
+
|
622
|
+
error
|
623
|
+
end
|
624
|
+
|
625
|
+
# @param [String] msg
|
626
|
+
# @param [response] res
|
627
|
+
# @param [Class] klass
|
628
|
+
def raise_error(msg, res, klass=nil)
|
629
|
+
status_code = res.code.to_s
|
630
|
+
error = parse_error_response(res)
|
631
|
+
message = "#{msg}: #{error['message']}"
|
632
|
+
|
633
|
+
error_class = if klass
|
634
|
+
message = "#{status_code}: #{message}"
|
635
|
+
klass
|
636
|
+
else
|
637
|
+
case status_code
|
638
|
+
when "404"
|
639
|
+
NotFoundError
|
640
|
+
when "409"
|
641
|
+
message = "#{message}: conflicts_with job:#{error["details"]["conflicts_with"]}" if error["details"] && error["details"]["conflicts_with"]
|
642
|
+
AlreadyExistsError
|
643
|
+
when "401"
|
644
|
+
AuthError
|
645
|
+
when "403"
|
646
|
+
ForbiddenError
|
647
|
+
else
|
648
|
+
message = "#{status_code}: #{message}"
|
649
|
+
APIError
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
exc = nil
|
654
|
+
if error_class.method_defined?(:conflicts_with) && error["details"] && error["details"]["conflicts_with"]
|
655
|
+
exc = error_class.new(message, error['stacktrace'], error["details"]["conflicts_with"])
|
656
|
+
elsif error_class.method_defined?(:api_backtrace)
|
657
|
+
exc = error_class.new(message, error['stacktrace'])
|
658
|
+
else
|
659
|
+
exc = error_class.new(message)
|
660
|
+
end
|
661
|
+
raise exc
|
662
|
+
end
|
663
|
+
|
664
|
+
if ''.respond_to?(:encode)
|
665
|
+
# @param [String] s
|
666
|
+
# @return [String]
|
667
|
+
# * ' ' and '+' must be escaped into %20 and %2B because escaped text may be
|
668
|
+
# used as both URI and query.
|
669
|
+
# * '.' must be escaped as %2E because it may be cunfused with extension.
|
670
|
+
def e(s)
|
671
|
+
s = s.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
|
672
|
+
s.gsub!(/[^\-_!~*'()~0-9A-Z_a-z]/){|x|'%%%02X' % x.ord}
|
673
|
+
s
|
674
|
+
end
|
675
|
+
else
|
676
|
+
# @param [String] s
|
677
|
+
# @return [String]
|
678
|
+
# * ' ' and '+' must be escaped into %20 and %2B because escaped text may be
|
679
|
+
# used as both URI and query.
|
680
|
+
# * '.' must be escaped as %2E because it may be cunfused with extension.
|
681
|
+
def e(s)
|
682
|
+
s.to_s.gsub(/[^\-_!~*'()~0-9A-Z_a-z]/){|x|'%%%02X' % x.ord}
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
# @param [String] body
|
687
|
+
# @param [Array] required
|
688
|
+
def checked_json(body, required = [])
|
689
|
+
js = nil
|
690
|
+
begin
|
691
|
+
js = JSON.load(body)
|
692
|
+
rescue
|
693
|
+
raise "Unexpected API response: #{$!}"
|
694
|
+
end
|
695
|
+
unless js.is_a?(Hash)
|
696
|
+
raise "Unexpected API response: #{body}"
|
697
|
+
end
|
698
|
+
required.each {|k|
|
699
|
+
unless js[k]
|
700
|
+
raise "Unexpected API response: #{body}"
|
701
|
+
end
|
702
|
+
}
|
703
|
+
js
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
end # module TreasureData
|