aspera-cli 4.24.1 → 4.24.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 +15 -2
- data/README.md +745 -436
- data/bin/ascli +20 -1
- data/bin/asession +23 -27
- data/lib/aspera/agent/base.rb +10 -21
- data/lib/aspera/agent/connect.rb +2 -3
- data/lib/aspera/agent/desktop.rb +2 -2
- data/lib/aspera/agent/direct.rb +49 -32
- data/lib/aspera/agent/factory.rb +31 -0
- data/lib/aspera/api/aoc.rb +79 -49
- data/lib/aspera/api/faspex.rb +212 -0
- data/lib/aspera/api/node.rb +99 -84
- data/lib/aspera/ascp/installation.rb +22 -21
- data/lib/aspera/ascp/management.rb +119 -23
- data/lib/aspera/assert.rb +14 -8
- data/lib/aspera/cli/extended_value.rb +15 -15
- data/lib/aspera/cli/formatter.rb +7 -5
- data/lib/aspera/cli/hints.rb +8 -0
- data/lib/aspera/cli/info.rb +4 -4
- data/lib/aspera/cli/main.rb +55 -70
- data/lib/aspera/cli/manager.rb +7 -4
- data/lib/aspera/cli/plugins/alee.rb +2 -1
- data/lib/aspera/cli/plugins/aoc.rb +110 -186
- data/lib/aspera/cli/plugins/ats.rb +4 -4
- data/lib/aspera/cli/plugins/base.rb +335 -0
- data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
- data/lib/aspera/cli/plugins/config.rb +249 -220
- data/lib/aspera/cli/plugins/console.rb +15 -15
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/factory.rb +78 -0
- data/lib/aspera/cli/plugins/faspex.rb +17 -20
- data/lib/aspera/cli/plugins/faspex5.rb +79 -193
- data/lib/aspera/cli/plugins/faspio.rb +14 -13
- data/lib/aspera/cli/plugins/httpgw.rb +13 -12
- data/lib/aspera/cli/plugins/node.rb +34 -32
- data/lib/aspera/cli/plugins/oauth.rb +48 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
- data/lib/aspera/cli/plugins/preview.rb +4 -4
- data/lib/aspera/cli/plugins/server.rb +15 -13
- data/lib/aspera/cli/plugins/shares.rb +18 -15
- data/lib/aspera/cli/sync_actions.rb +1 -1
- data/lib/aspera/cli/transfer_agent.rb +24 -20
- data/lib/aspera/cli/transfer_progress.rb +6 -6
- data/lib/aspera/cli/version.rb +3 -3
- data/lib/aspera/cli/wizard.rb +65 -53
- data/lib/aspera/colors.rb +6 -0
- data/lib/aspera/command_line_builder.rb +45 -50
- data/lib/aspera/command_line_converter.rb +2 -1
- data/lib/aspera/coverage.rb +1 -1
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +10 -7
- data/lib/aspera/faspex_gw.rb +6 -4
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +1 -1
- data/lib/aspera/log.rb +37 -9
- data/lib/aspera/nagios.rb +1 -1
- data/lib/aspera/oauth/base.rb +17 -10
- data/lib/aspera/oauth/factory.rb +8 -8
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/products/connect.rb +4 -3
- data/lib/aspera/products/desktop.rb +1 -4
- data/lib/aspera/products/other.rb +9 -1
- data/lib/aspera/products/transferd.rb +0 -1
- data/lib/aspera/rest.rb +126 -83
- data/lib/aspera/ssh.rb +3 -3
- data/lib/aspera/sync/args.schema.yaml +46 -3
- data/lib/aspera/sync/conf.schema.yaml +130 -94
- data/lib/aspera/sync/operations.rb +16 -16
- data/lib/aspera/temp_file_manager.rb +17 -5
- data/lib/aspera/transfer/error.rb +16 -7
- data/lib/aspera/transfer/parameters.rb +34 -20
- data/lib/aspera/transfer/resumer.rb +74 -0
- data/lib/aspera/transfer/spec.rb +4 -3
- data/lib/aspera/transfer/spec.schema.yaml +132 -51
- data/lib/aspera/transfer/spec_doc.rb +41 -35
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +6 -6
- data.tar.gz.sig +0 -0
- metadata +9 -7
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
- data/lib/aspera/cli/plugin.rb +0 -333
- data/lib/aspera/cli/plugin_factory.rb +0 -81
- data/lib/aspera/resumer.rb +0 -77
- data/lib/aspera/transfer/error_info.rb +0 -91
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Aspera
|
|
|
13
13
|
# OAuth 2.0 Authorization Framework: https://tools.ietf.org/html/rfc6749
|
|
14
14
|
# Bearer Token Usage: https://tools.ietf.org/html/rfc6750
|
|
15
15
|
class Base
|
|
16
|
+
Aspera.require_method!(:create_token)
|
|
16
17
|
# @param ** Parameters for REST
|
|
17
18
|
# @param client_id [String, nil]
|
|
18
19
|
# @param client_secret [String, nil]
|
|
@@ -31,8 +32,7 @@ module Aspera
|
|
|
31
32
|
cache_ids: nil,
|
|
32
33
|
**rest_params
|
|
33
34
|
)
|
|
34
|
-
|
|
35
|
-
# this is the OAuth API
|
|
35
|
+
# This is the OAuth API
|
|
36
36
|
@api = Rest.new(**rest_params)
|
|
37
37
|
@scope = nil
|
|
38
38
|
@token_cache_id = nil
|
|
@@ -43,6 +43,7 @@ module Aspera
|
|
|
43
43
|
@use_query = use_query
|
|
44
44
|
@base_cache_ids = cache_ids.nil? ? [] : cache_ids.clone
|
|
45
45
|
Aspera.assert_type(@base_cache_ids, Array)
|
|
46
|
+
# TODO: this shall be done in class, using cache_ids
|
|
46
47
|
@base_cache_ids.push(@api.auth_params[:username]) if @api.auth_params.key?(:username)
|
|
47
48
|
@base_cache_ids.compact!
|
|
48
49
|
@base_cache_ids.freeze
|
|
@@ -56,16 +57,20 @@ module Aspera
|
|
|
56
57
|
@token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
attr_reader :scope, :api, :path_token
|
|
60
|
+
attr_reader :scope, :api, :path_token, :client_id
|
|
60
61
|
|
|
61
62
|
# helper method to create token as per RFC
|
|
62
63
|
def create_token_call(creation_params)
|
|
63
64
|
Log.log.debug{'Generating a new token'.bg_green}
|
|
64
|
-
payload =
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
payload = if @use_query
|
|
66
|
+
{
|
|
67
|
+
query: creation_params
|
|
68
|
+
}
|
|
67
69
|
else
|
|
68
|
-
|
|
70
|
+
{
|
|
71
|
+
content_type: Rest::MIME_WWW,
|
|
72
|
+
body: creation_params
|
|
73
|
+
}
|
|
69
74
|
end
|
|
70
75
|
return @api.call(
|
|
71
76
|
operation: 'POST',
|
|
@@ -75,12 +80,13 @@ module Aspera
|
|
|
75
80
|
)
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
# @
|
|
83
|
+
# @param add_secret [Boolean] Add secret in default call parameters
|
|
84
|
+
# @return [Hash] Optional general parameters
|
|
79
85
|
def optional_scope_client_id(add_secret: false)
|
|
80
86
|
call_params = {}
|
|
81
87
|
call_params[:scope] = @scope unless @scope.nil?
|
|
82
88
|
call_params[:client_id] = @client_id unless @client_id.nil?
|
|
83
|
-
call_params[:client_secret] = @client_secret
|
|
89
|
+
call_params[:client_secret] = @client_secret unless !add_secret || @client_id.nil? || @client_secret.nil?
|
|
84
90
|
return call_params
|
|
85
91
|
end
|
|
86
92
|
|
|
@@ -106,6 +112,7 @@ module Aspera
|
|
|
106
112
|
# `direct` agent is equipped with refresh code
|
|
107
113
|
# an API was already called, but failed, we need to regenerate or refresh
|
|
108
114
|
if refresh || token_info[:expired]
|
|
115
|
+
Log.log.trace1{"refresh: #{refresh} expired: #{token_info[:expired]}"}
|
|
109
116
|
refresh_token = nil
|
|
110
117
|
if token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
|
|
111
118
|
# save possible refresh token, before deleting the cache
|
|
@@ -118,7 +125,7 @@ module Aspera
|
|
|
118
125
|
if !refresh_token.nil?
|
|
119
126
|
Log.log.info{"refresh=[#{refresh_token}]".bg_green}
|
|
120
127
|
# NOTE: AoC admin token has no refresh, and lives by default 1800secs
|
|
121
|
-
resp = create_token_call(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
|
128
|
+
resp = create_token_call(optional_scope_client_id(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
|
122
129
|
if resp[:http].code.start_with?('2')
|
|
123
130
|
# save only if success
|
|
124
131
|
json_data = resp[:http].body
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -63,10 +63,10 @@ module Aspera
|
|
|
63
63
|
@decoders = []
|
|
64
64
|
# default parameters, others can be added by handlers
|
|
65
65
|
@parameters = {
|
|
66
|
-
# tokens older than
|
|
67
|
-
|
|
68
|
-
# tokens valid for less than this duration will be regenerated
|
|
69
|
-
|
|
66
|
+
# tokens older than this duration in sec. will be discarded from cache
|
|
67
|
+
token_cache_max_age: 1800,
|
|
68
|
+
# tokens valid for less than this duration in sec. will be regenerated
|
|
69
|
+
token_refresh_threshold: 120
|
|
70
70
|
}
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -77,7 +77,7 @@ module Aspera
|
|
|
77
77
|
def persist_mgr=(manager)
|
|
78
78
|
@persist = manager
|
|
79
79
|
# cleanup expired tokens
|
|
80
|
-
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:
|
|
80
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def persist_mgr
|
|
@@ -108,7 +108,7 @@ module Aspera
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
#
|
|
111
|
+
# Get token information from cache
|
|
112
112
|
# @param id [String] identifier of token
|
|
113
113
|
# @return [Hash] token internal information , including Date object for `expiration_date`
|
|
114
114
|
def get_token_info(id)
|
|
@@ -118,7 +118,6 @@ module Aspera
|
|
|
118
118
|
Aspera.assert_type(token_data, Hash)
|
|
119
119
|
decoded_token = decode_token(token_data[TOKEN_FIELD])
|
|
120
120
|
info = {data: token_data}
|
|
121
|
-
Log.dump(:decoded_token, decoded_token)
|
|
122
121
|
if decoded_token.is_a?(Hash)
|
|
123
122
|
info[:decoded] = decoded_token
|
|
124
123
|
# TODO: move date decoding to token decoder ?
|
|
@@ -129,9 +128,10 @@ module Aspera
|
|
|
129
128
|
unless expiration_date.nil?
|
|
130
129
|
info[:expiration] = expiration_date
|
|
131
130
|
info[:ttl_sec] = expiration_date - Time.now
|
|
132
|
-
info[:expired] = info[:ttl_sec] < @parameters[:
|
|
131
|
+
info[:expired] = info[:ttl_sec] < @parameters[:token_refresh_threshold]
|
|
133
132
|
end
|
|
134
133
|
end
|
|
134
|
+
Log.dump(:token_info, info)
|
|
135
135
|
return info
|
|
136
136
|
end
|
|
137
137
|
|
data/lib/aspera/oauth/web.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Aspera
|
|
|
9
9
|
# Authentication using Web browser
|
|
10
10
|
class Web < Base
|
|
11
11
|
class << self
|
|
12
|
-
attr_accessor :
|
|
12
|
+
attr_accessor :additional_info
|
|
13
13
|
end
|
|
14
14
|
# @param redirect_uri url to receive the code after auth (to be exchanged for token)
|
|
15
15
|
# @param path_authorize path to login page on web app
|
|
@@ -37,7 +37,7 @@ module Aspera
|
|
|
37
37
|
# here, we need a human to authorize on a web page
|
|
38
38
|
Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
|
|
39
39
|
# start a web server to receive request code
|
|
40
|
-
web_server = WebAuth.new(@redirect_uri, self.class.
|
|
40
|
+
web_server = WebAuth.new(@redirect_uri, self.class.additional_info)
|
|
41
41
|
# start browser on login page
|
|
42
42
|
Environment.instance.open_uri(login_page_url)
|
|
43
43
|
# wait for code in request
|
|
@@ -48,11 +48,12 @@ module Aspera
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Base URL for CDN of Connect
|
|
51
52
|
def cdn_api
|
|
52
53
|
Rest.new(base_url: CDN_BASE_URL)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
#
|
|
56
|
+
# Retrieve structure from cloud (CDN) with all versions available
|
|
56
57
|
def versions
|
|
57
58
|
if @connect_versions.nil?
|
|
58
59
|
javascript = cdn_api.call(operation: 'GET', subpath: VERSION_INFO_FILE)
|
|
@@ -74,10 +75,10 @@ module Aspera
|
|
|
74
75
|
@connect_versions = nil
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
|
|
78
78
|
CDN_BASE_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
|
|
79
|
+
VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
|
|
79
80
|
|
|
80
|
-
private_constant :
|
|
81
|
+
private_constant :CDN_BASE_URL, :VERSION_INFO_FILE
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
end
|
|
@@ -8,6 +8,7 @@ module Aspera
|
|
|
8
8
|
class Desktop
|
|
9
9
|
APP_NAME = 'IBM Aspera for Desktop'
|
|
10
10
|
APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
|
|
11
|
+
LOG_FILENAME = 'ibm-aspera-desktop.log'
|
|
11
12
|
class << self
|
|
12
13
|
# standard folder locations
|
|
13
14
|
def locations
|
|
@@ -20,10 +21,6 @@ module Aspera
|
|
|
20
21
|
else []
|
|
21
22
|
end.map{ |i| i.merge({expected: APP_NAME})}
|
|
22
23
|
end
|
|
23
|
-
|
|
24
|
-
def log_file
|
|
25
|
-
File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER, 'ibm-aspera-desktop.log')
|
|
26
|
-
end
|
|
27
24
|
end
|
|
28
25
|
end
|
|
29
26
|
end
|
|
@@ -54,8 +54,14 @@ module Aspera
|
|
|
54
54
|
}]
|
|
55
55
|
end
|
|
56
56
|
class << self
|
|
57
|
+
# Find installed products and provide paths for it.
|
|
58
|
+
# @param scan_locations [Array] Array of Hash with keys: expected, app_root, sub_bin, ascp_path, name, version
|
|
59
|
+
# @return [Array] of products found, with filled missing fields
|
|
60
|
+
# @raise Exception if no installed product found
|
|
57
61
|
def find(scan_locations)
|
|
58
|
-
|
|
62
|
+
product_names = []
|
|
63
|
+
found = scan_locations.select do |item|
|
|
64
|
+
product_names.push(item[:expected]) unless product_names.include?(item[:expected])
|
|
59
65
|
# skip if not main folder
|
|
60
66
|
Log.log.trace1{"Checking #{item[:app_root]}"}
|
|
61
67
|
next false unless Dir.exist?(item[:app_root])
|
|
@@ -75,6 +81,8 @@ module Aspera
|
|
|
75
81
|
end
|
|
76
82
|
true # select this version
|
|
77
83
|
end
|
|
84
|
+
raise "Product: #{product_names.join(', ')} not found, please install." if found.empty?
|
|
85
|
+
found
|
|
78
86
|
end
|
|
79
87
|
end
|
|
80
88
|
end
|
data/lib/aspera/rest.rb
CHANGED
|
@@ -54,80 +54,91 @@ module Aspera
|
|
|
54
54
|
# rest call errors are raised as exception RestCallError
|
|
55
55
|
# and error are analyzed in RestErrorAnalyzer
|
|
56
56
|
class Rest
|
|
57
|
-
#
|
|
58
|
-
ARRAY_PARAMS = '[]'
|
|
59
|
-
|
|
60
|
-
private_constant :ARRAY_PARAMS
|
|
61
|
-
|
|
62
|
-
# error message when entity not found (TODO: use specific exception)
|
|
57
|
+
# Error message when entity not found (TODO: use specific exception)
|
|
63
58
|
ENTITY_NOT_FOUND = 'No such'
|
|
64
59
|
|
|
65
60
|
MIME_JSON = 'application/json'
|
|
66
61
|
MIME_WWW = 'application/x-www-form-urlencoded'
|
|
67
62
|
MIME_TEXT = 'text/plain'
|
|
68
63
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
# Special query parameter: max number of items for list command
|
|
65
|
+
MAX_ITEMS = 'max'
|
|
66
|
+
# Special query parameter: max number of pages for list command
|
|
67
|
+
MAX_PAGES = 'pmax'
|
|
73
68
|
|
|
74
69
|
class << self
|
|
75
70
|
# @return [String] Basic auth token
|
|
76
71
|
def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
|
|
77
72
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def array_params?(values)
|
|
85
|
-
return values.first.eql?(ARRAY_PARAMS)
|
|
73
|
+
# Indicate that the given Hash query uses php style for array parameters
|
|
74
|
+
# a[]=1&a[]=2
|
|
75
|
+
def php_style(query)
|
|
76
|
+
Aspera.assert_type(query, Hash){'query'}
|
|
77
|
+
query[:x_array_php_style] = true
|
|
78
|
+
query
|
|
86
79
|
end
|
|
87
80
|
|
|
88
81
|
# Build URI from URL and parameters and check it is http or https
|
|
89
|
-
#
|
|
90
|
-
# @param
|
|
91
|
-
|
|
82
|
+
# Check iof php style is specified
|
|
83
|
+
# @param url [String] The URL without query.
|
|
84
|
+
# @param query [Hash,Array,String] The query.
|
|
85
|
+
def build_uri(url, query)
|
|
92
86
|
uri = URI.parse(url)
|
|
93
|
-
Aspera.
|
|
87
|
+
Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
|
|
94
88
|
return uri if query.nil? || query.respond_to?(:empty?) && query.empty?
|
|
95
89
|
Log.dump(:query, query)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
uri.query =
|
|
91
|
+
case query
|
|
92
|
+
when String
|
|
93
|
+
query
|
|
94
|
+
when Hash
|
|
95
|
+
URI.encode_www_form(h_to_query_array(query))
|
|
96
|
+
when Array
|
|
97
|
+
Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays of 2 elements'}
|
|
98
|
+
URI.encode_www_form(query)
|
|
99
|
+
else Aspera.error_unexpected_value(query.class){'query type'}
|
|
100
|
+
end.gsub('%5B%5D=', '[]=')
|
|
101
|
+
# [] is allowed in url parameters
|
|
102
|
+
uri
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param query [Hash] HTTP query as hash
|
|
106
|
+
def h_to_query_array(query)
|
|
107
|
+
Aspera.assert_type(query, Hash)
|
|
108
|
+
# Support array for query parameter, there is no standard.
|
|
109
|
+
# Either p[]=1&p[]=2, or p=1&p=2
|
|
110
|
+
suffix = query.delete(:x_array_php_style) ? '[]' : nil
|
|
111
|
+
query.each_with_object([]) do |(k, v), query_array|
|
|
112
|
+
case v
|
|
113
|
+
when Array
|
|
114
|
+
v.each do |e|
|
|
115
|
+
query_array.push(["#{k}#{suffix}", e])
|
|
109
116
|
end
|
|
117
|
+
else
|
|
118
|
+
query_array.push([k, v])
|
|
110
119
|
end
|
|
111
|
-
when Array
|
|
112
|
-
Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays or 2 elements'}
|
|
113
|
-
query_array = query
|
|
114
|
-
else
|
|
115
|
-
raise "Query must be Hash or Array, not #{query.class}"
|
|
116
120
|
end
|
|
117
|
-
# [] is allowed in url parameters
|
|
118
|
-
uri.query = URI.encode_www_form(query_array).gsub('%5B%5D=', '[]=')
|
|
119
|
-
return uri
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
# Decode query string as Hash
|
|
123
|
-
#
|
|
124
|
+
# if parameter is only once, then it's a scalar
|
|
125
|
+
# if a parameter is several, then it's array
|
|
126
|
+
# if parameter has [] then it's an array, and [] is removed
|
|
127
|
+
# Support arrays in query string, e.g. PHP's way is p[]=1&p[]=2
|
|
124
128
|
# @param query [String] query string as in URI.query
|
|
125
129
|
# @return [Hash] decoded query
|
|
126
130
|
def query_to_h(query)
|
|
127
|
-
URI.decode_www_form(query).each_with_object({}) do |
|
|
128
|
-
key
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
URI.decode_www_form(query).each_with_object({}) do |(key, value), h|
|
|
132
|
+
if key.end_with?('[]')
|
|
133
|
+
key = key[..-3]
|
|
134
|
+
h[key] = [] unless h.key?(key)
|
|
135
|
+
end
|
|
136
|
+
if h.key?(key)
|
|
137
|
+
h[key] = [h[key]] if !h[key].is_a?(Array)
|
|
138
|
+
h[key].push(value)
|
|
139
|
+
else
|
|
140
|
+
h[key] = value
|
|
141
|
+
end
|
|
131
142
|
end
|
|
132
143
|
end
|
|
133
144
|
|
|
@@ -135,13 +146,14 @@ module Aspera
|
|
|
135
146
|
# @param base_url [String] base url of HTTP/S session
|
|
136
147
|
# @return [Net::HTTP] a started HTTP session
|
|
137
148
|
def start_http_session(base_url)
|
|
138
|
-
uri =
|
|
139
|
-
|
|
149
|
+
uri = URI.parse(base_url)
|
|
150
|
+
Aspera.assert_values(uri.scheme, %w[http https]){'URI scheme'}
|
|
151
|
+
# This honors http_proxy env var
|
|
140
152
|
http_session = Net::HTTP.new(uri.host, uri.port)
|
|
141
153
|
http_session.use_ssl = uri.scheme.eql?('https')
|
|
142
|
-
#
|
|
154
|
+
# Set http options in callback, such as timeout and cert. verification
|
|
143
155
|
RestParameters.instance.session_cb&.call(http_session)
|
|
144
|
-
#
|
|
156
|
+
# Manually start session for keep alive (if supported by server, else, session is closed every time)
|
|
145
157
|
http_session.start
|
|
146
158
|
return http_session
|
|
147
159
|
end
|
|
@@ -174,19 +186,31 @@ module Aspera
|
|
|
174
186
|
return result
|
|
175
187
|
end
|
|
176
188
|
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
189
|
+
# Parses an HTTP Content-Type header string into its media type and parameters
|
|
190
|
+
# according to RFC 9110 and RFC 6838.
|
|
191
|
+
# TODO: use gem: content_type
|
|
192
|
+
#
|
|
193
|
+
# @param header [String] The Content-Type header string, e.g., "application/json; charset=utf-8"
|
|
194
|
+
# @return [Hash] A hash with :type and :parameters keys.
|
|
195
|
+
# Example:
|
|
196
|
+
# {
|
|
197
|
+
# type: "application/json",
|
|
198
|
+
# parameters: {
|
|
199
|
+
# charset: "utf-8",
|
|
200
|
+
# version: "1.0"
|
|
201
|
+
# }
|
|
202
|
+
# }
|
|
181
203
|
def parse_header(header)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
parts = header.split(';').map(&:strip)
|
|
205
|
+
media_type = parts.shift.downcase
|
|
206
|
+
parameters = parts.filter_map do |param|
|
|
207
|
+
key, value = param.split('=', 2)
|
|
208
|
+
next unless key && value
|
|
209
|
+
key = key.strip.downcase.to_sym
|
|
210
|
+
value = value.strip.gsub(/\A"|"\z/, '')
|
|
211
|
+
[key, value]
|
|
188
212
|
end.to_h
|
|
189
|
-
{type:
|
|
213
|
+
{type: media_type, parameters: parameters}
|
|
190
214
|
end
|
|
191
215
|
end
|
|
192
216
|
|
|
@@ -200,8 +224,13 @@ module Aspera
|
|
|
200
224
|
|
|
201
225
|
public
|
|
202
226
|
|
|
203
|
-
|
|
227
|
+
# All original constructor parameters
|
|
204
228
|
attr_reader :auth_params
|
|
229
|
+
|
|
230
|
+
# The root URL for the API
|
|
231
|
+
attr_reader :base_url
|
|
232
|
+
|
|
233
|
+
# Base common headers of API
|
|
205
234
|
attr_reader :headers
|
|
206
235
|
|
|
207
236
|
# @return creation parameters
|
|
@@ -278,7 +307,7 @@ module Aspera
|
|
|
278
307
|
# @param body [Hash, String] body parameters
|
|
279
308
|
# @param headers [Hash] additional headers (override Content-Type)
|
|
280
309
|
# @param save_to_file (filepath)
|
|
281
|
-
# @param
|
|
310
|
+
# @param exception (bool) true, error raise exception
|
|
282
311
|
def call(
|
|
283
312
|
operation:,
|
|
284
313
|
subpath: nil,
|
|
@@ -287,12 +316,14 @@ module Aspera
|
|
|
287
316
|
body: nil,
|
|
288
317
|
headers: nil,
|
|
289
318
|
save_to_file: nil,
|
|
290
|
-
|
|
319
|
+
exception: true
|
|
291
320
|
)
|
|
292
321
|
subpath = subpath.to_s if subpath.is_a?(Symbol)
|
|
293
322
|
subpath = '' if subpath.nil?
|
|
294
323
|
Log.log.debug{"call #{operation} [#{subpath}]".red.bold.bg_green}
|
|
295
|
-
Log.dump(:body, body)
|
|
324
|
+
Log.dump(:body, body, level: :trace1)
|
|
325
|
+
Log.dump(:query, query, level: :trace1)
|
|
326
|
+
Log.dump(:headers, headers, level: :trace1)
|
|
296
327
|
Aspera.assert_type(subpath, String)
|
|
297
328
|
if headers.nil?
|
|
298
329
|
headers = @headers.clone
|
|
@@ -462,12 +493,12 @@ module Aspera
|
|
|
462
493
|
body: body,
|
|
463
494
|
content_type: content_type,
|
|
464
495
|
save_to_file: save_to_file,
|
|
465
|
-
|
|
496
|
+
exception: exception,
|
|
466
497
|
headers: headers
|
|
467
498
|
)
|
|
468
499
|
end
|
|
469
500
|
# raise exception if could not retry and not return error in result
|
|
470
|
-
raise e
|
|
501
|
+
raise e if exception
|
|
471
502
|
end
|
|
472
503
|
Log.log.debug{"result=http:#{result[:http]}, data:#{result[:data].class}"}
|
|
473
504
|
return result
|
|
@@ -478,31 +509,36 @@ module Aspera
|
|
|
478
509
|
# If specific elements are needed, then use the full `call` method
|
|
479
510
|
#
|
|
480
511
|
|
|
481
|
-
|
|
482
|
-
|
|
512
|
+
# Create: `POST`
|
|
513
|
+
def create(subpath, params, **kwargs)
|
|
514
|
+
return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)[:data]
|
|
483
515
|
end
|
|
484
516
|
|
|
485
|
-
|
|
486
|
-
|
|
517
|
+
# Read: `GET`
|
|
518
|
+
def read(subpath, query = nil, **kwargs)
|
|
519
|
+
return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query, **kwargs)[:data]
|
|
487
520
|
end
|
|
488
521
|
|
|
489
|
-
|
|
490
|
-
|
|
522
|
+
# Update: `PUT`
|
|
523
|
+
def update(subpath, params, **kwargs)
|
|
524
|
+
return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON, **kwargs)[:data]
|
|
491
525
|
end
|
|
492
526
|
|
|
493
|
-
|
|
494
|
-
|
|
527
|
+
# Delete: `DELETE`
|
|
528
|
+
def delete(subpath, params = nil, **kwargs)
|
|
529
|
+
return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params, **kwargs)[:data]
|
|
495
530
|
end
|
|
496
531
|
|
|
497
|
-
|
|
498
|
-
|
|
532
|
+
# Cancel: `CANCEL`
|
|
533
|
+
def cancel(subpath, **kwargs)
|
|
534
|
+
return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON}, **kwargs)[:data]
|
|
499
535
|
end
|
|
500
536
|
|
|
501
537
|
# Query entity by general search (read with parameter `q`)
|
|
502
538
|
# TODO: not generic enough ? move somewhere ? inheritance ?
|
|
503
|
-
# @param subpath
|
|
504
|
-
# @param search_name
|
|
505
|
-
# @param query
|
|
539
|
+
# @param subpath [String] Path of entity in API
|
|
540
|
+
# @param search_name [String] Name of searched entity
|
|
541
|
+
# @param query [Hash] Additional search query parameters
|
|
506
542
|
# @returns [Hash] A single entity matching the search, or an exception if not found or multiple found
|
|
507
543
|
def lookup_by_name(subpath, search_name, query: nil)
|
|
508
544
|
query = {} if query.nil?
|
|
@@ -520,10 +556,17 @@ module Aspera
|
|
|
520
556
|
name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
|
|
521
557
|
case name_matches.length
|
|
522
558
|
when 1 then return name_matches.first
|
|
523
|
-
when 0 then raise %Q(#{subpath}:
|
|
559
|
+
when 0 then raise %Q(#{subpath}: Multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{ |i| i['name']}} but no case insensitive full match. Please be more specific or give exact name.)
|
|
524
560
|
else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
|
|
525
561
|
end
|
|
526
562
|
end
|
|
527
563
|
end
|
|
564
|
+
|
|
565
|
+
# Content-Type that are JSON
|
|
566
|
+
JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
|
|
567
|
+
|
|
568
|
+
UNAVAILABLE_CODES = ['503']
|
|
569
|
+
|
|
570
|
+
private_constant :JSON_DECODE, :UNAVAILABLE_CODES
|
|
528
571
|
end
|
|
529
572
|
end
|
data/lib/aspera/ssh.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Aspera
|
|
|
17
17
|
$VERBOSE = nil
|
|
18
18
|
Net::SSH::Authentication::Session.class_eval do
|
|
19
19
|
define_method(:default_keys) do
|
|
20
|
-
%w[
|
|
20
|
+
%w[.ssh .ssh2].product(%w[rsa dsa ecdsa]).map{"~/#{_1}/id_#{_2}"}.freeze
|
|
21
21
|
end
|
|
22
22
|
private(:default_keys)
|
|
23
23
|
end rescue nil
|
|
@@ -81,6 +81,6 @@ module Aspera
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
Aspera::Ssh.disable_ed25519_keys if
|
|
84
|
+
# Deactivate ed25519 and ecdsa private keys from SSH identities, as it usually causes problems
|
|
85
|
+
Aspera::Ssh.disable_ed25519_keys if Gem::Specification.find_all_by_name('ed25519').none?
|
|
86
86
|
Aspera::Ssh.disable_ecd_sha2_algorithms if defined?(JRUBY_VERSION) && ENV.fetch('ASCLI_ENABLE_ECDSHA2', 'false').eql?('false')
|