aspera-cli 4.27.0 → 4.27.2
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +46 -1
- data/CONTRIBUTING.md +1 -7
- data/TODO.md +50 -0
- data/docs/README.md +709 -489
- data/lib/aspera/agent/base.rb +10 -2
- data/lib/aspera/agent/connect.rb +7 -7
- data/lib/aspera/agent/desktop.rb +3 -3
- data/lib/aspera/agent/direct.rb +48 -19
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/agent/transferd.rb +12 -12
- data/lib/aspera/api/aoc.rb +28 -28
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +2 -2
- data/lib/aspera/api/httpgw.rb +20 -20
- data/lib/aspera/api/node.rb +31 -53
- data/lib/aspera/ascmd.rb +13 -13
- data/lib/aspera/ascp/installation.rb +43 -36
- data/lib/aspera/ascp/management.rb +6 -6
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +7 -7
- data/lib/aspera/cli/async_transfer_store.rb +49 -5
- data/lib/aspera/cli/bootstrapper.rb +9 -9
- data/lib/aspera/cli/command_registry.rb +1 -1
- data/lib/aspera/cli/command_spec.rb +6 -3
- data/lib/aspera/cli/context.rb +3 -3
- data/lib/aspera/cli/extended_value.rb +26 -26
- data/lib/aspera/cli/formatter.rb +76 -54
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +22 -29
- data/lib/aspera/cli/mailer.rb +7 -7
- data/lib/aspera/cli/mcp_tool.rb +31 -4
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +273 -3
- data/lib/aspera/cli/parser.rb +539 -280
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +303 -314
- data/lib/aspera/cli/plugins/ats.rb +25 -21
- data/lib/aspera/cli/plugins/base.rb +357 -186
- data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
- data/lib/aspera/cli/plugins/config.rb +76 -73
- data/lib/aspera/cli/plugins/console.rb +8 -8
- data/lib/aspera/cli/plugins/cos.rb +4 -3
- data/lib/aspera/cli/plugins/factory.rb +2 -2
- data/lib/aspera/cli/plugins/faspex5.rb +259 -248
- data/lib/aspera/cli/plugins/faspio.rb +7 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/mcp.rb +11 -11
- data/lib/aspera/cli/plugins/node.rb +227 -283
- data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
- data/lib/aspera/cli/plugins/preview.rb +61 -64
- data/lib/aspera/cli/plugins/server.rb +36 -31
- data/lib/aspera/cli/plugins/shares.rb +174 -135
- data/lib/aspera/cli/preset_actions.rb +39 -19
- data/lib/aspera/cli/preset_manager.rb +74 -32
- data/lib/aspera/cli/result.rb +18 -16
- data/lib/aspera/cli/runner.rb +75 -39
- data/lib/aspera/cli/secret_finder.rb +1 -1
- data/lib/aspera/cli/sync_actions.rb +36 -35
- data/lib/aspera/cli/terminal_formatter.rb +2 -2
- data/lib/aspera/cli/transfer_actions.rb +17 -10
- data/lib/aspera/cli/transfer_agent.rb +44 -44
- data/lib/aspera/cli/transfer_progress.rb +7 -7
- data/lib/aspera/cli/vault_manager.rb +34 -12
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +17 -20
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +17 -16
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/dot_container.rb +8 -8
- data/lib/aspera/environment.rb +13 -13
- data/lib/aspera/faspex_gw.rb +4 -4
- data/lib/aspera/faspex_postproc.rb +14 -9
- data/lib/aspera/graphql.rb +4 -2
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/json_rpc/client.rb +1 -1
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +17 -5
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +13 -13
- data/lib/aspera/markdown.rb +5 -5
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +33 -27
- data/lib/aspera/oauth/base.rb +4 -5
- data/lib/aspera/oauth/boot.rb +3 -3
- data/lib/aspera/oauth/factory.rb +5 -5
- data/lib/aspera/oauth/jwt.rb +5 -7
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +8 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +49 -46
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +65 -18
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -10
- data/lib/aspera/rest.rb +149 -115
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_list.rb +10 -9
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +17 -11
- data/lib/aspera/schema/reader.rb +78 -4
- data/lib/aspera/schema/registry.rb +26 -7
- data/lib/aspera/secret_hider.rb +5 -5
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +3 -3
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +13 -13
- data/lib/aspera/temp_file_manager.rb +24 -4
- data/lib/aspera/transfer/faux_file.rb +2 -2
- data/lib/aspera/transfer/parameters.rb +15 -15
- data/lib/aspera/transfer/resumer.rb +9 -9
- data/lib/aspera/transfer/spec.rb +1 -1
- data/lib/aspera/transfer/spec.schema.yaml +1 -0
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +6 -5
- data/lib/aspera/web_server_simple.rb +9 -9
- data/lib/aspera/yaml.rb +4 -4
- data.tar.gz.sig +0 -0
- metadata +10 -4
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/plugins/faspex.rb +0 -576
- data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/rest.rb
CHANGED
|
@@ -87,7 +87,7 @@ module Aspera
|
|
|
87
87
|
# @param query [Hash] A key can have Array value and result will use PHP format: a[]=1&a[]=2
|
|
88
88
|
# @return [Hash] The query parameters.
|
|
89
89
|
def php_style(query)
|
|
90
|
-
Aspera.assert_type(query, Hash){'query'}
|
|
90
|
+
Aspera.assert_type(query, Hash) { 'query' }
|
|
91
91
|
query[:x_array_php_style] = true
|
|
92
92
|
query
|
|
93
93
|
end
|
|
@@ -100,7 +100,7 @@ module Aspera
|
|
|
100
100
|
# @return [URI] The built URI.
|
|
101
101
|
def build_uri(url, query)
|
|
102
102
|
uri = URI.parse(url)
|
|
103
|
-
Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
|
|
103
|
+
Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
|
|
104
104
|
return uri if query.nil? || query.respond_to?(:empty?) && query.empty?
|
|
105
105
|
Log.dump(:query, query)
|
|
106
106
|
uri.query =
|
|
@@ -110,9 +110,9 @@ module Aspera
|
|
|
110
110
|
when Hash
|
|
111
111
|
URI.encode_www_form(h_to_query_array(query))
|
|
112
112
|
when Array
|
|
113
|
-
Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}, 'Query must be array of arrays of 2 elements')
|
|
113
|
+
Aspera.assert(query.all? { |i| i.is_a?(Array) && i.length.eql?(2) }, 'Query must be array of arrays of 2 elements')
|
|
114
114
|
URI.encode_www_form(query) # remove nil values
|
|
115
|
-
else Aspera.error_unexpected_value(query.class){'query type'}
|
|
115
|
+
else Aspera.error_unexpected_value(query.class) { 'query type' }
|
|
116
116
|
end.gsub('%5B%5D=', '[]=')
|
|
117
117
|
# [] is allowed in url parameters
|
|
118
118
|
uri
|
|
@@ -165,7 +165,7 @@ module Aspera
|
|
|
165
165
|
# @return [Net::HTTP] A started HTTP session
|
|
166
166
|
def start_http_session(base_url)
|
|
167
167
|
uri = URI.parse(base_url)
|
|
168
|
-
Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
|
|
168
|
+
Aspera.assert_values(uri.scheme, %w[http https]) { 'URI scheme' }
|
|
169
169
|
# This honors http_proxy env var
|
|
170
170
|
http_session = Net::HTTP.new(uri.host, uri.port)
|
|
171
171
|
http_session.use_ssl = uri.scheme.eql?('https')
|
|
@@ -185,7 +185,7 @@ module Aspera
|
|
|
185
185
|
Aspera.assert_type(http_session, Net::HTTP)
|
|
186
186
|
# Net::BufferedIO in net/protocol.rb
|
|
187
187
|
result = http_session.instance_variable_get(:@socket)
|
|
188
|
-
Aspera.assert(!result.nil?){"no socket for #{http_session}"}
|
|
188
|
+
Aspera.assert(!result.nil?) { "no socket for #{http_session}" }
|
|
189
189
|
return result
|
|
190
190
|
end
|
|
191
191
|
|
|
@@ -290,7 +290,7 @@ module Aspera
|
|
|
290
290
|
# remove trailing port if it is 443 and scheme is https
|
|
291
291
|
@base_url = @base_url.gsub(/:443$/, '') if @base_url.start_with?('https://')
|
|
292
292
|
@base_url = @base_url.gsub(/:80$/, '') if @base_url.start_with?('http://')
|
|
293
|
-
Log.log.debug{"Rest.new(#{@base_url})"}
|
|
293
|
+
Log.log.debug { "Rest.new(#{@base_url})" }
|
|
294
294
|
# default is no auth
|
|
295
295
|
@auth_params = auth
|
|
296
296
|
Aspera.assert_type(@auth_params, Hash)
|
|
@@ -312,7 +312,7 @@ module Aspera
|
|
|
312
312
|
def oauth
|
|
313
313
|
if @oauth.nil?
|
|
314
314
|
Aspera.assert(@auth_params[:type].eql?(:oauth2), 'no OAuth defined')
|
|
315
|
-
oauth_parameters = @auth_params.reject{ |k, _v| k.eql?(:type)}
|
|
315
|
+
oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
|
|
316
316
|
Log.dump(:oauth_parameters, oauth_parameters)
|
|
317
317
|
@oauth = OAuth::Factory.instance.create(**oauth_parameters)
|
|
318
318
|
end
|
|
@@ -326,7 +326,7 @@ module Aspera
|
|
|
326
326
|
# @param content_type [String, nil] Type of body parameters (one of MIME_*) and serialization, else use headers
|
|
327
327
|
# @param body [Hash, String, nil] Body parameters
|
|
328
328
|
# @param headers [Hash{String => String}] Additional headers (override Content-Type)
|
|
329
|
-
# @param
|
|
329
|
+
# @param save_to [String, IO, nil] File path or IO object to save response body; progress bar is used when set
|
|
330
330
|
# @param exception [Boolean] Whether to raise an exception on HTTP error
|
|
331
331
|
# @param ret [Symbol] One of :data, :resp, :both - controls return value
|
|
332
332
|
# @return [Array(Hash, Net::HTTPResponse)] When `ret` is :both
|
|
@@ -340,42 +340,20 @@ module Aspera
|
|
|
340
340
|
content_type: nil,
|
|
341
341
|
body: nil,
|
|
342
342
|
headers: nil,
|
|
343
|
-
|
|
343
|
+
save_to: nil,
|
|
344
344
|
exception: true,
|
|
345
345
|
ret: :data
|
|
346
346
|
)
|
|
347
347
|
subpath = subpath.to_s if subpath.is_a?(Symbol)
|
|
348
348
|
subpath = '' if subpath.nil?
|
|
349
|
-
Log.log.debug{"call #{operation} [#{subpath}]".red.bold.bg_green}
|
|
349
|
+
Log.log.debug { "call #{operation} [#{subpath}]".red.bold.bg_green }
|
|
350
350
|
Log.dump(:body, body, level: :trace1)
|
|
351
351
|
Log.dump(:query, query, level: :trace1)
|
|
352
352
|
Log.dump(:headers, headers, level: :trace1)
|
|
353
353
|
Aspera.assert_type(subpath, String)
|
|
354
354
|
# We must have a way to check return code
|
|
355
355
|
Aspera.assert(exception || !ret.eql?(:data), 'ret: :data requires exception handler')
|
|
356
|
-
|
|
357
|
-
headers = @headers.clone
|
|
358
|
-
else
|
|
359
|
-
h = headers
|
|
360
|
-
headers = @headers.clone
|
|
361
|
-
headers.merge!(h)
|
|
362
|
-
end
|
|
363
|
-
Aspera.assert_type(headers, Hash)
|
|
364
|
-
case @auth_params[:type]
|
|
365
|
-
when :none
|
|
366
|
-
# no auth
|
|
367
|
-
when :basic
|
|
368
|
-
Log.log.debug('using Basic auth')
|
|
369
|
-
# done in build_req
|
|
370
|
-
when :oauth2
|
|
371
|
-
headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
|
|
372
|
-
when :url
|
|
373
|
-
query ||= {}
|
|
374
|
-
@auth_params[:url_query].each do |key, value|
|
|
375
|
-
query[key] = value
|
|
376
|
-
end
|
|
377
|
-
else Aspera.error_unexpected_value(@auth_params[:type])
|
|
378
|
-
end
|
|
356
|
+
headers, query = prepare_call(headers, query)
|
|
379
357
|
result_http = nil
|
|
380
358
|
result_data = nil
|
|
381
359
|
# initialize with number of initial retries allowed, nil gives zero
|
|
@@ -383,37 +361,7 @@ module Aspera
|
|
|
383
361
|
# start a block to be able to retry the actual HTTP request in case of OAuth token expiration
|
|
384
362
|
begin
|
|
385
363
|
Log.log.debug("send request (retries=#{tries_remain_redirect})")
|
|
386
|
-
|
|
387
|
-
# URI.escape()
|
|
388
|
-
separator = ['', '/'].include?(subpath) ? '' : '/'
|
|
389
|
-
uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", query)
|
|
390
|
-
Log.log.debug{"URI=#{uri}"}
|
|
391
|
-
begin
|
|
392
|
-
# instantiate request object based on string name
|
|
393
|
-
req = Net::HTTP.const_get(operation.capitalize).new(uri)
|
|
394
|
-
rescue NameError
|
|
395
|
-
raise "unsupported operation : #{operation}"
|
|
396
|
-
end
|
|
397
|
-
case content_type
|
|
398
|
-
when nil # ignore
|
|
399
|
-
when Mime::JSON
|
|
400
|
-
req.body = JSON.generate(body) # , ascii_only: true
|
|
401
|
-
req['Content-Type'] = Mime::JSON
|
|
402
|
-
when Mime::WWW
|
|
403
|
-
req.body = URI.encode_www_form(body)
|
|
404
|
-
req['Content-Type'] = Mime::WWW
|
|
405
|
-
when Mime::TEXT
|
|
406
|
-
req.body = body
|
|
407
|
-
req['Content-Type'] = Mime::TEXT
|
|
408
|
-
else Aspera.error_unexpected_value(content_type){'body type'}
|
|
409
|
-
end
|
|
410
|
-
# set headers
|
|
411
|
-
headers.each do |key, value|
|
|
412
|
-
req[key] = value
|
|
413
|
-
end
|
|
414
|
-
# :type = :basic
|
|
415
|
-
req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
|
|
416
|
-
Log.dump(:req_body, req.body, level: :trace1)
|
|
364
|
+
req = build_request(operation, subpath, query, content_type, body, headers)
|
|
417
365
|
# we try the call, and will retry on some error types
|
|
418
366
|
error_tries ||= 1 + RestParameters.instance.retry_max
|
|
419
367
|
result_mime = nil
|
|
@@ -422,58 +370,21 @@ module Aspera
|
|
|
422
370
|
http_session.request(req) do |response|
|
|
423
371
|
result_http = response
|
|
424
372
|
result_mime = self.class.parse_header(result_http['Content-Type'] || Mime::TEXT)[:type]
|
|
425
|
-
Log.log.debug{"response: code=#{result_http.code}, mime=#{result_mime}, content-type=#{response['Content-Type']}"}
|
|
373
|
+
Log.log.debug { "response: code=#{result_http.code}, mime=#{result_mime}, content-type=#{response['Content-Type']}" }
|
|
426
374
|
# JSON data needs to be parsed, in case it contains an error code
|
|
427
|
-
|
|
428
|
-
result_http.code.to_s.start_with?('2') &&
|
|
429
|
-
!Mime.json?(result_mime)
|
|
430
|
-
total_size = result_http['Content-Length']&.to_i
|
|
431
|
-
Log.log.debug('before write file')
|
|
432
|
-
target_file = save_to_file
|
|
433
|
-
# override user's path to path in header
|
|
434
|
-
if !response['Content-Disposition'].nil?
|
|
435
|
-
disposition = self.class.parse_header(response['Content-Disposition'])
|
|
436
|
-
target_file = File.join(File.dirname(target_file), disposition[:parameters][:filename]) if disposition[:parameters].key?(:filename) && !disposition[:parameters][:filename].eql?('.')
|
|
437
|
-
end
|
|
438
|
-
# download with temp filename
|
|
439
|
-
target_file_tmp = "#{target_file}#{RestParameters.instance.download_partial_suffix}"
|
|
440
|
-
Log.log.debug{"saving to: #{target_file}"}
|
|
441
|
-
written_size = 0
|
|
442
|
-
session_id = SecureRandom.uuid.freeze
|
|
443
|
-
RestParameters.instance.progress_bar&.event(:session_start, session_id: session_id)
|
|
444
|
-
RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size) if total_size
|
|
445
|
-
FileUtils.mkdir_p(File.dirname(target_file_tmp))
|
|
446
|
-
limiter = TimerLimiter.new(0.5)
|
|
447
|
-
File.open(target_file_tmp, 'wb') do |file|
|
|
448
|
-
result_http.read_body do |fragment|
|
|
449
|
-
file.write(fragment)
|
|
450
|
-
written_size += fragment.length
|
|
451
|
-
RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
RestParameters.instance.progress_bar&.event(:session_end, session_id: session_id)
|
|
455
|
-
RestParameters.instance.progress_bar&.event(:end)
|
|
456
|
-
# rename at the end
|
|
457
|
-
File.rename(target_file_tmp, target_file)
|
|
458
|
-
file_saved = true
|
|
459
|
-
end
|
|
375
|
+
file_saved = save_response(response, result_http, result_mime, save_to)
|
|
460
376
|
end
|
|
461
|
-
Log.log.debug{"result: code=#{result_http.code} mime=#{result_mime}"}
|
|
377
|
+
Log.log.debug { "result: code=#{result_http.code} mime=#{result_mime}" }
|
|
462
378
|
# sometimes there is a UTF8 char (e.g. (c) )
|
|
463
379
|
# TODO : related to mime type encoding ?
|
|
464
380
|
# result_http.body.force_encoding('UTF-8') if result_http.body.is_a?(String)
|
|
465
381
|
# Log.log.debug{"result: body=#{result_http.body}"}
|
|
466
|
-
result_data = result_http
|
|
467
|
-
Log.dump(:result_data_raw, result_data, level: :trace1)
|
|
468
|
-
# TODO: Remove next 2 lines when bug in async node api is fixed. (Aspera/core/issues/4490)
|
|
469
|
-
node_api_bug = result_data&.index('}HTTP/1.1 400 Bad Request') if result_data.is_a?(String)
|
|
470
|
-
result_data = result_data[0..node_api_bug] if node_api_bug
|
|
471
|
-
result_data = JSON.parse(result_data) if Mime.json?(result_mime) && !result_data.nil? && !result_data.empty?
|
|
472
|
-
Log.dump(:result_data, result_data)
|
|
382
|
+
result_data = parse_response(result_http, result_mime)
|
|
473
383
|
RestErrorAnalyzer.instance.raise_on_error(req, result_data, result_http)
|
|
474
|
-
unless file_saved ||
|
|
475
|
-
|
|
476
|
-
File.
|
|
384
|
+
unless file_saved || save_to.nil?
|
|
385
|
+
raise 'save_to: IO object requires a streaming response' if save_to.respond_to?(:write)
|
|
386
|
+
FileUtils.mkdir_p(File.dirname(save_to))
|
|
387
|
+
File.write(save_to, result_http.body, binmode: true)
|
|
477
388
|
end
|
|
478
389
|
rescue RestCallError => e
|
|
479
390
|
do_retry = false
|
|
@@ -494,7 +405,7 @@ module Aspera
|
|
|
494
405
|
# regenerate a brand new token
|
|
495
406
|
req['Authorization'] = oauth.authorization(cache: false)
|
|
496
407
|
end
|
|
497
|
-
Log.log.debug
|
|
408
|
+
Log.log.debug('using new token')
|
|
498
409
|
do_retry ||= true
|
|
499
410
|
end
|
|
500
411
|
if do_retry && (error_tries -= 1).positive?
|
|
@@ -509,7 +420,7 @@ module Aspera
|
|
|
509
420
|
# special case: relative redirect
|
|
510
421
|
if URI.parse(new_url).host.nil?
|
|
511
422
|
# we don't manage relative redirects with non-absolute path
|
|
512
|
-
Aspera.assert(new_url.start_with?('/')){"redirect location is relative: #{new_url}, but does not start with /."}
|
|
423
|
+
Aspera.assert(new_url.start_with?('/')) { "redirect location is relative: #{new_url}, but does not start with /." }
|
|
513
424
|
new_url = "#{current_uri.scheme}://#{current_uri.host}#{new_url}"
|
|
514
425
|
end
|
|
515
426
|
# forwards the request to the new location
|
|
@@ -522,7 +433,7 @@ module Aspera
|
|
|
522
433
|
query: query,
|
|
523
434
|
body: body,
|
|
524
435
|
content_type: content_type,
|
|
525
|
-
|
|
436
|
+
save_to: save_to,
|
|
526
437
|
exception: exception,
|
|
527
438
|
headers: headers,
|
|
528
439
|
ret: ret
|
|
@@ -531,15 +442,138 @@ module Aspera
|
|
|
531
442
|
# raise exception if could not retry and not return error in result
|
|
532
443
|
raise e if exception
|
|
533
444
|
end
|
|
534
|
-
Log.log.debug{"result=http:#{result_http}, data:#{result_data.class}"}
|
|
445
|
+
Log.log.debug { "result=http:#{result_http}, data:#{result_data.class}" }
|
|
535
446
|
return case ret
|
|
536
447
|
when :data then result_data
|
|
537
448
|
when :resp then result_http
|
|
538
449
|
when :both then [result_data, result_http]
|
|
539
|
-
else Aspera.error_unexpected_value(ret){'Type of result for REST'}
|
|
450
|
+
else Aspera.error_unexpected_value(ret) { 'Type of result for REST' }
|
|
540
451
|
end
|
|
541
452
|
end
|
|
542
453
|
|
|
454
|
+
private
|
|
455
|
+
|
|
456
|
+
def prepare_call(headers, query)
|
|
457
|
+
if headers.nil?
|
|
458
|
+
headers = @headers.clone
|
|
459
|
+
else
|
|
460
|
+
h = headers
|
|
461
|
+
headers = @headers.clone
|
|
462
|
+
headers.merge!(h)
|
|
463
|
+
end
|
|
464
|
+
Aspera.assert_type(headers, Hash)
|
|
465
|
+
case @auth_params[:type]
|
|
466
|
+
when :none
|
|
467
|
+
# no auth
|
|
468
|
+
when :basic
|
|
469
|
+
Log.log.debug('using Basic auth')
|
|
470
|
+
# done in build_req
|
|
471
|
+
when :oauth2
|
|
472
|
+
headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
|
|
473
|
+
when :url
|
|
474
|
+
query ||= {}
|
|
475
|
+
@auth_params[:url_query].each do |key, value|
|
|
476
|
+
query[key] = value
|
|
477
|
+
end
|
|
478
|
+
else Aspera.error_unexpected_value(@auth_params[:type])
|
|
479
|
+
end
|
|
480
|
+
[headers, query]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def build_request(operation, subpath, query, content_type, body, headers)
|
|
484
|
+
# TODO: shall we percent encode subpath (spaces) test with access key delete with space in id
|
|
485
|
+
# URI.escape()
|
|
486
|
+
separator = ['', '/'].include?(subpath) ? '' : '/'
|
|
487
|
+
uri = self.class.build_uri("#{@base_url}#{separator}#{subpath}", query)
|
|
488
|
+
Log.log.debug { "URI=#{uri}" }
|
|
489
|
+
begin
|
|
490
|
+
# instantiate request object based on string name
|
|
491
|
+
req = Net::HTTP.const_get(operation.capitalize).new(uri)
|
|
492
|
+
rescue NameError
|
|
493
|
+
raise "unsupported operation : #{operation}"
|
|
494
|
+
end
|
|
495
|
+
case content_type
|
|
496
|
+
when nil # ignore
|
|
497
|
+
when Mime::JSON
|
|
498
|
+
req.body = JSON.generate(body) # , ascii_only: true
|
|
499
|
+
req['Content-Type'] = Mime::JSON
|
|
500
|
+
when Mime::WWW
|
|
501
|
+
req.body = URI.encode_www_form(body)
|
|
502
|
+
req['Content-Type'] = Mime::WWW
|
|
503
|
+
when Mime::TEXT
|
|
504
|
+
req.body = body
|
|
505
|
+
req['Content-Type'] = Mime::TEXT
|
|
506
|
+
else Aspera.error_unexpected_value(content_type) { 'body type' }
|
|
507
|
+
end
|
|
508
|
+
# set headers
|
|
509
|
+
headers.each do |key, value|
|
|
510
|
+
req[key] = value
|
|
511
|
+
end
|
|
512
|
+
# :type = :basic
|
|
513
|
+
req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
|
|
514
|
+
Log.dump(:req_body, req.body, level: :trace1)
|
|
515
|
+
req
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def save_response(response, result_http, result_mime, save_to)
|
|
519
|
+
return false unless !save_to.nil? && result_http.code.to_s.start_with?('2') && !Mime.json?(result_mime)
|
|
520
|
+
|
|
521
|
+
total_size = result_http['Content-Length']&.to_i
|
|
522
|
+
Log.log.debug('before write file')
|
|
523
|
+
target_file = save_to
|
|
524
|
+
# override user's path to path in header
|
|
525
|
+
unless response['Content-Disposition'].nil?
|
|
526
|
+
disposition = self.class.parse_header(response['Content-Disposition'])
|
|
527
|
+
if disposition[:parameters].key?(:filename) && !disposition[:parameters][:filename].eql?('.')
|
|
528
|
+
# Use only the basename to prevent path traversal via a server-controlled Content-Disposition header
|
|
529
|
+
safe_filename = File.basename(disposition[:parameters][:filename])
|
|
530
|
+
target_file = File.join(File.dirname(target_file), safe_filename) unless safe_filename.empty?
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
Log.log.debug { "saving to: #{target_file}" }
|
|
534
|
+
written_size = 0
|
|
535
|
+
session_id = SecureRandom.uuid.freeze
|
|
536
|
+
RestParameters.instance.progress_bar&.event(:session_start, session_id: session_id)
|
|
537
|
+
RestParameters.instance.progress_bar&.event(:session_size, session_id: session_id, info: total_size) if total_size
|
|
538
|
+
limiter = TimerLimiter.new(0.5)
|
|
539
|
+
if target_file.respond_to?(:write)
|
|
540
|
+
# IO object: stream directly into it
|
|
541
|
+
result_http.read_body do |fragment|
|
|
542
|
+
target_file.write(fragment)
|
|
543
|
+
written_size += fragment.length
|
|
544
|
+
RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
|
|
545
|
+
end
|
|
546
|
+
else
|
|
547
|
+
# file path: download to partial name first, then rename atomically
|
|
548
|
+
target_file_tmp = "#{target_file}#{RestParameters.instance.download_partial_suffix}"
|
|
549
|
+
FileUtils.mkdir_p(File.dirname(target_file_tmp))
|
|
550
|
+
File.open(target_file_tmp, 'wb') do |file|
|
|
551
|
+
result_http.read_body do |fragment|
|
|
552
|
+
file.write(fragment)
|
|
553
|
+
written_size += fragment.length
|
|
554
|
+
RestParameters.instance.progress_bar&.event(:transfer, session_id: session_id, info: written_size) if limiter.trigger?
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
File.rename(target_file_tmp, target_file)
|
|
558
|
+
end
|
|
559
|
+
RestParameters.instance.progress_bar&.event(:session_end, session_id: session_id)
|
|
560
|
+
RestParameters.instance.progress_bar&.event(:end)
|
|
561
|
+
true
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def parse_response(result_http, result_mime)
|
|
565
|
+
result_data = result_http.body
|
|
566
|
+
Log.dump(:result_data_raw, result_data, level: :trace1)
|
|
567
|
+
# TODO: Remove next 2 lines when bug in async node api is fixed. (Aspera/core/issues/4490)
|
|
568
|
+
node_api_bug = result_data&.index('}HTTP/1.1 400 Bad Request') if result_data.is_a?(String)
|
|
569
|
+
result_data = result_data[0..node_api_bug] if node_api_bug
|
|
570
|
+
result_data = JSON.parse(result_data) if Mime.json?(result_mime) && !result_data.nil? && !result_data.empty?
|
|
571
|
+
Log.dump(:result_data, result_data)
|
|
572
|
+
result_data
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
public
|
|
576
|
+
|
|
543
577
|
#
|
|
544
578
|
# CRUD simplified methods here
|
|
545
579
|
# If specific elements are needed, then use the full `call` method
|
|
@@ -36,7 +36,7 @@ module Aspera
|
|
|
36
36
|
# @param data [Object] the parsed response body
|
|
37
37
|
# @param http [Net::HTTPResponse] the raw HTTP response
|
|
38
38
|
def raise_on_error(req, data, http)
|
|
39
|
-
Log.log.debug{"raise_on_error #{req.method} #{req.path} #{http.code}"}
|
|
39
|
+
Log.log.debug { "raise_on_error #{req.method} #{req.path} #{http.code}" }
|
|
40
40
|
context = {
|
|
41
41
|
messages: [],
|
|
42
42
|
request: req,
|
|
@@ -49,7 +49,7 @@ module Aspera
|
|
|
49
49
|
@error_handlers.each do |handler|
|
|
50
50
|
handler[:block].call(handler[:name], context)
|
|
51
51
|
rescue StandardError => e
|
|
52
|
-
Log.log.error{"ERROR in handler:\n#{e.message}\n#{e.backtrace}"}
|
|
52
|
+
Log.log.error { "ERROR in handler:\n#{e.message}\n#{e.backtrace}" }
|
|
53
53
|
end
|
|
54
54
|
raise RestCallError, context unless context[:messages].empty?
|
|
55
55
|
end
|
|
@@ -97,7 +97,7 @@ module Aspera
|
|
|
97
97
|
# @param message [String] one error message to add to list
|
|
98
98
|
def add_error(context, type, message)
|
|
99
99
|
context[:messages].push(message)
|
|
100
|
-
Log.log.trace1{"Found error: #{type}: #{message}"}
|
|
100
|
+
Log.log.trace1 { "Found error: #{type}: #{message}" }
|
|
101
101
|
log_file = instance.log_file
|
|
102
102
|
# log error for further analysis (file must exist to activate)
|
|
103
103
|
return if log_file.nil? || !File.exist?(log_file)
|
data/lib/aspera/rest_list.rb
CHANGED
|
@@ -17,8 +17,8 @@ module Aspera
|
|
|
17
17
|
# @param query [Hash] Optional additional search query parameters
|
|
18
18
|
# @return [Hash] A single entity matching the search, or an exception if not found or multiple found
|
|
19
19
|
def lookup_with_q(entity, value:, field: 'name', query: {})
|
|
20
|
-
Aspera.assert_type(query, Hash){'query'}
|
|
21
|
-
Aspera.assert_type(field, String){'field'}
|
|
20
|
+
Aspera.assert_type(query, Hash) { 'query' }
|
|
21
|
+
Aspera.assert_type(field, String) { 'field' }
|
|
22
22
|
# returns entities matching the query (it matches against several fields in case insensitive way)
|
|
23
23
|
# We don't do paging, as anyway, we look for only one match
|
|
24
24
|
matching_items = read(entity, query.merge({'q' => value}))
|
|
@@ -32,11 +32,11 @@ module Aspera
|
|
|
32
32
|
else
|
|
33
33
|
# multiple case insensitive partial matches, try case insensitive full match
|
|
34
34
|
# (anyway AoC does not allow creation of 2 entities with same case insensitive field value)
|
|
35
|
-
value_matches = matching_items.select{ |i| i[field].casecmp?(value)}
|
|
35
|
+
value_matches = matching_items.select { |i| i[field].casecmp?(value) }
|
|
36
36
|
case value_matches.length
|
|
37
37
|
when 1 then return value_matches.first
|
|
38
|
-
when 0 then raise Error, "#{entity}: Multiple case insensitive partial match for: \"#{value}\" in #{matching_items.map{ |i| i[field]}.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}."
|
|
39
|
-
else raise Error, "Two entities cannot have the same case insensitive #{field}: #{value_matches.map{ |i| i[field]}}"
|
|
38
|
+
when 0 then raise Error, "#{entity}: Multiple case insensitive partial match for: \"#{value}\" in #{matching_items.map { |i| i[field] }.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}."
|
|
39
|
+
else raise Error, "Two entities cannot have the same case insensitive #{field}: #{value_matches.map { |i| i[field] }}"
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -60,7 +60,8 @@ module Aspera
|
|
|
60
60
|
Aspera.assert_type(entity, String)
|
|
61
61
|
Aspera.assert_type(items_key, String)
|
|
62
62
|
Aspera.assert_type(query, Hash)
|
|
63
|
-
Log.log.debug{"list_entities t=#{entity} k=#{items_key}
|
|
63
|
+
Log.log.debug { "list_entities t=#{entity} k=#{items_key}" }
|
|
64
|
+
Log.dump(:query, query)
|
|
64
65
|
result = []
|
|
65
66
|
offset = 0
|
|
66
67
|
max_items = query.delete(MAX_ITEMS)
|
|
@@ -104,10 +105,10 @@ module Aspera
|
|
|
104
105
|
# @param query [Hash] Additional query parameters (Default: `:default`)
|
|
105
106
|
def lookup_entity_by_field(entity:, value:, field: 'name', items_key: nil, query: :default)
|
|
106
107
|
if query.eql?(:default)
|
|
107
|
-
Aspera.assert_values(field, ['name']){'Default query field'}
|
|
108
|
+
Aspera.assert_values(field, ['name']) { 'Default query field' }
|
|
108
109
|
query = {'q'=> value}
|
|
109
110
|
end
|
|
110
|
-
lookup_entity_generic(entity: entity, field: field, value: value){list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query).first}
|
|
111
|
+
lookup_entity_generic(entity: entity, field: field, value: value) { list_entities_limit_offset_total_count(entity: entity, items_key: items_key, query: query).first }
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
# Lookup entity by field and value.
|
|
@@ -123,7 +124,7 @@ module Aspera
|
|
|
123
124
|
Aspera.assert(block_given?, 'block required for lookup_entity_generic')
|
|
124
125
|
found = yield
|
|
125
126
|
Aspera.assert_array_all(found, Hash)
|
|
126
|
-
found = found.select{ |i| i[field].eql?(value)}
|
|
127
|
+
found = found.select { |i| i[field].eql?(value) }
|
|
127
128
|
return found.first if found.length.eql?(1)
|
|
128
129
|
raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
|
|
129
130
|
end
|