aspera-cli 4.24.2 → 4.25.0.pre
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 +1064 -758
- data/CONTRIBUTING.md +43 -100
- data/README.md +671 -419
- data/lib/aspera/api/aoc.rb +71 -43
- data/lib/aspera/api/cos_node.rb +3 -2
- data/lib/aspera/api/faspex.rb +6 -5
- data/lib/aspera/api/node.rb +10 -12
- data/lib/aspera/ascmd.rb +1 -2
- data/lib/aspera/ascp/installation.rb +53 -39
- data/lib/aspera/assert.rb +25 -3
- data/lib/aspera/cli/error.rb +4 -2
- data/lib/aspera/cli/extended_value.rb +84 -60
- data/lib/aspera/cli/formatter.rb +55 -22
- data/lib/aspera/cli/main.rb +21 -14
- data/lib/aspera/cli/manager.rb +348 -247
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +70 -14
- data/lib/aspera/cli/plugins/base.rb +57 -49
- data/lib/aspera/cli/plugins/config.rb +69 -84
- data/lib/aspera/cli/plugins/console.rb +13 -8
- data/lib/aspera/cli/plugins/cos.rb +1 -1
- data/lib/aspera/cli/plugins/faspex.rb +32 -26
- data/lib/aspera/cli/plugins/faspex5.rb +45 -43
- data/lib/aspera/cli/plugins/faspio.rb +5 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +131 -120
- data/lib/aspera/cli/plugins/oauth.rb +1 -1
- data/lib/aspera/cli/plugins/orchestrator.rb +114 -32
- data/lib/aspera/cli/plugins/preview.rb +26 -46
- data/lib/aspera/cli/plugins/server.rb +6 -8
- data/lib/aspera/cli/plugins/shares.rb +27 -32
- data/lib/aspera/cli/sync_actions.rb +49 -38
- data/lib/aspera/cli/transfer_agent.rb +16 -34
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +8 -5
- data/lib/aspera/command_line_builder.rb +20 -17
- data/lib/aspera/coverage.rb +1 -1
- data/lib/aspera/environment.rb +41 -34
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -2
- data/lib/aspera/markdown.rb +31 -0
- data/lib/aspera/nagios.rb +6 -5
- data/lib/aspera/oauth/base.rb +17 -27
- data/lib/aspera/oauth/factory.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +2 -1
- data/lib/aspera/preview/file_types.rb +23 -37
- data/lib/aspera/products/connect.rb +3 -3
- data/lib/aspera/rest.rb +51 -39
- data/lib/aspera/rest_error_analyzer.rb +4 -4
- data/lib/aspera/ssh.rb +5 -2
- data/lib/aspera/ssl.rb +41 -0
- data/lib/aspera/sync/conf.schema.yaml +182 -34
- data/lib/aspera/sync/database.rb +2 -1
- data/lib/aspera/sync/operations.rb +125 -69
- data/lib/aspera/transfer/parameters.rb +3 -4
- data/lib/aspera/transfer/spec.rb +2 -3
- data/lib/aspera/transfer/spec.schema.yaml +48 -18
- data/lib/aspera/transfer/spec_doc.rb +14 -14
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/transferd_pb.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +19 -6
- metadata.gz.sig +3 -2
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -73,7 +73,7 @@ module Aspera
|
|
|
73
73
|
# split host of URL into organization and domain
|
|
74
74
|
def split_org_domain(uri)
|
|
75
75
|
Aspera.assert_type(uri, URI)
|
|
76
|
-
|
|
76
|
+
Aspera.assert(!uri.host.nil?){"No host found in URL. Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}"}
|
|
77
77
|
parts = uri.host.split('.', 2)
|
|
78
78
|
Aspera.assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
|
|
79
79
|
parts[0] = nil if parts[0].eql?('api')
|
|
@@ -89,7 +89,7 @@ module Aspera
|
|
|
89
89
|
# @param url [String] URL of AoC public link
|
|
90
90
|
# @return [Hash] information about public link, or nil if not a public link
|
|
91
91
|
def link_info(url)
|
|
92
|
-
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET')
|
|
92
|
+
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET', ret: :resp).uri
|
|
93
93
|
Log.dump(:final_uri, final_uri, level: :trace1)
|
|
94
94
|
org_domain = split_org_domain(final_uri)
|
|
95
95
|
if (m = final_uri.path.match(%r{/oauth2/([^/]+)/login$}))
|
|
@@ -97,7 +97,7 @@ module Aspera
|
|
|
97
97
|
else
|
|
98
98
|
Log.log.debug{"path=#{final_uri.path} does not end with /login"}
|
|
99
99
|
end
|
|
100
|
-
|
|
100
|
+
Aspera.assert(!final_uri.query.nil?, 'AoC shall redirect to login page with a query', type: Error)
|
|
101
101
|
query = Rest.query_to_h(final_uri.query)
|
|
102
102
|
Log.dump(:query, query, level: :trace1)
|
|
103
103
|
# is that a public link ?
|
|
@@ -141,9 +141,10 @@ module Aspera
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# Call block with same query using paging and response information.
|
|
144
|
-
# Block must return
|
|
144
|
+
# Block must return an Array with data and http response
|
|
145
145
|
# @return [Hash] {items: , total: }
|
|
146
|
-
def call_paging(query:
|
|
146
|
+
def call_paging(query: nil, formatter: nil)
|
|
147
|
+
query = {} if query.nil?
|
|
147
148
|
Aspera.assert_type(query, Hash){'query'}
|
|
148
149
|
Aspera.assert(block_given?)
|
|
149
150
|
# set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
|
|
@@ -158,33 +159,62 @@ module Aspera
|
|
|
158
159
|
loop do
|
|
159
160
|
new_query = query.clone
|
|
160
161
|
new_query['page'] = current_page
|
|
161
|
-
|
|
162
|
-
Aspera.assert(
|
|
163
|
-
|
|
164
|
-
total_count = result[:http]['X-Total-Count']
|
|
162
|
+
result_data, result_http = yield(new_query)
|
|
163
|
+
Aspera.assert(result_http)
|
|
164
|
+
total_count = result_http['X-Total-Count']&.to_i
|
|
165
165
|
page_count += 1
|
|
166
166
|
current_page += 1
|
|
167
|
-
add_items =
|
|
167
|
+
add_items = result_data
|
|
168
|
+
break if add_items.nil?
|
|
168
169
|
break if add_items.empty?
|
|
169
170
|
# append new items to full list
|
|
170
171
|
item_list += add_items
|
|
171
172
|
break if !max_items.nil? && item_list.count >= max_items
|
|
172
173
|
break if !max_pages.nil? && page_count >= max_pages
|
|
174
|
+
break if total_count&.<=(item_list.count)
|
|
173
175
|
formatter&.long_operation_running("#{item_list.count} / #{total_count}") unless total_count.eql?(item_list.count.to_s)
|
|
174
176
|
end
|
|
175
177
|
formatter&.long_operation_terminated
|
|
176
178
|
item_list = item_list[0..max_items - 1] if !max_items.nil? && item_list.count > max_items
|
|
177
179
|
return {items: item_list, total: total_count}
|
|
178
180
|
end
|
|
181
|
+
|
|
182
|
+
# @param id [String] Identifier or workspace
|
|
183
|
+
# @return [Hash] suitable for permission filtering
|
|
184
|
+
def workspace_access(id)
|
|
185
|
+
{
|
|
186
|
+
'access_type' => 'user',
|
|
187
|
+
'access_id' => "#{ID_AK_ADMIN}_WS_#{id}"
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param permission [Hash] Shared folder information
|
|
192
|
+
# @return [Boolean] `true` if internal access
|
|
193
|
+
def workspace_access?(permission)
|
|
194
|
+
permission['access_id'].start_with?("#{ID_AK_ADMIN}_WS_")
|
|
195
|
+
end
|
|
179
196
|
end
|
|
180
197
|
|
|
181
198
|
attr_reader :private_link
|
|
182
199
|
|
|
183
|
-
def initialize(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
def initialize(
|
|
201
|
+
url:,
|
|
202
|
+
auth:,
|
|
203
|
+
subpath: API_V1,
|
|
204
|
+
client_id: nil,
|
|
205
|
+
client_secret: nil,
|
|
206
|
+
scope: nil,
|
|
207
|
+
redirect_uri: nil,
|
|
208
|
+
private_key: nil,
|
|
209
|
+
passphrase: nil,
|
|
210
|
+
username: nil,
|
|
211
|
+
password: nil,
|
|
212
|
+
workspace: nil,
|
|
213
|
+
secret_finder: nil
|
|
214
|
+
)
|
|
215
|
+
# Test here because link may set url
|
|
216
|
+
Aspera.assert(url, 'Missing mandatory option: url', type: ParameterError)
|
|
217
|
+
Aspera.assert(scope, 'Missing mandatory option: scope', type: ParameterError)
|
|
188
218
|
# default values for client id
|
|
189
219
|
client_id, client_secret = self.class.get_client_info if client_id.nil?
|
|
190
220
|
# access key secrets are provided out of band to get node api access
|
|
@@ -209,7 +239,7 @@ module Aspera
|
|
|
209
239
|
auth_params[:grant_method] = if url_info.key?(:token)
|
|
210
240
|
:url_json
|
|
211
241
|
else
|
|
212
|
-
|
|
242
|
+
Aspera.assert(auth, 'Missing mandatory option: auth', type: ParameterError)
|
|
213
243
|
auth
|
|
214
244
|
end
|
|
215
245
|
# this is the base API url
|
|
@@ -219,11 +249,11 @@ module Aspera
|
|
|
219
249
|
# fill other auth parameters based on OAuth method
|
|
220
250
|
case auth_params[:grant_method]
|
|
221
251
|
when :web
|
|
222
|
-
|
|
252
|
+
Aspera.assert(redirect_uri, 'Missing mandatory option: redirect_uri', type: ParameterError)
|
|
223
253
|
auth_params[:redirect_uri] = redirect_uri
|
|
224
254
|
when :jwt
|
|
225
|
-
|
|
226
|
-
|
|
255
|
+
Aspera.assert(private_key, 'Missing mandatory option: private_key', type: ParameterError)
|
|
256
|
+
Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
|
|
227
257
|
auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
|
|
228
258
|
auth_params[:payload] = {
|
|
229
259
|
iss: auth_params[:client_id], # issuer
|
|
@@ -250,10 +280,9 @@ module Aspera
|
|
|
250
280
|
|
|
251
281
|
# read using the query and paging
|
|
252
282
|
# @return [Hash] {items: , total: }
|
|
253
|
-
def read_with_paging(subpath, query
|
|
283
|
+
def read_with_paging(subpath, query = nil, formatter: nil)
|
|
254
284
|
return self.class.call_paging(query: query, formatter: formatter) do |paged_query|
|
|
255
|
-
|
|
256
|
-
call(operation: 'GET', subpath: subpath, headers: {'Accept' => Rest::MIME_JSON}, query: paged_query)
|
|
285
|
+
read(subpath, query: paged_query, ret: :both)
|
|
257
286
|
end
|
|
258
287
|
end
|
|
259
288
|
|
|
@@ -352,7 +381,7 @@ module Aspera
|
|
|
352
381
|
file_id: user_info['read_only_home_file_id']
|
|
353
382
|
}
|
|
354
383
|
end
|
|
355
|
-
|
|
384
|
+
Aspera.assert(!@home_info[:node_id].to_s.empty?, "Cannot get user's home node id, check your default workspace or specify one", type: Error)
|
|
356
385
|
Log.dump(:context, @home_info)
|
|
357
386
|
@home_info
|
|
358
387
|
end
|
|
@@ -423,37 +452,38 @@ module Aspera
|
|
|
423
452
|
end
|
|
424
453
|
meta_schema.each do |field|
|
|
425
454
|
provided = pkg_meta.select{ |i| i['name'].eql?(field['name'])}
|
|
426
|
-
|
|
427
|
-
|
|
455
|
+
Aspera.assert(provided.count <= 1, type: Error){"only one field with name #{field['name']} allowed"}
|
|
456
|
+
Aspera.assert(!provided.empty?, type: Error){"missing mandatory field: #{field['name']}"} if field['required']
|
|
428
457
|
end
|
|
429
458
|
end
|
|
430
459
|
|
|
431
460
|
# Normalize package creation recipient lists as expected by AoC API
|
|
432
461
|
# AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
|
|
433
462
|
# in that case, the name is resolved and replaced with {type: , id: }
|
|
434
|
-
# @param package_data
|
|
435
|
-
# @param
|
|
436
|
-
# @
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
463
|
+
# @param package_data [Hash] The whole package creation payload
|
|
464
|
+
# @param rcpt_lst_field [String] The field in structure, i.e. recipients or bcc_recipients
|
|
465
|
+
# @param new_user_option [Hash] Additionnal fields for contact creation
|
|
466
|
+
# @return nil, `package_data` is modified
|
|
467
|
+
def resolve_package_recipients(package_data, rcpt_lst_field, new_user_option)
|
|
468
|
+
return unless package_data.key?(rcpt_lst_field)
|
|
469
|
+
Aspera.assert_type(package_data[rcpt_lst_field], Array){rcpt_lst_field}
|
|
440
470
|
new_user_option = {'package_contact' => true} if new_user_option.nil?
|
|
441
471
|
Aspera.assert_type(new_user_option, Hash){'new_user_option'}
|
|
472
|
+
ws_id = package_data['workspace_id']
|
|
442
473
|
# list with resolved elements
|
|
443
474
|
resolved_list = []
|
|
444
|
-
package_data[
|
|
475
|
+
package_data[rcpt_lst_field].each do |short_recipient_info|
|
|
445
476
|
case short_recipient_info
|
|
446
477
|
when Hash # native API information, check keys
|
|
447
|
-
Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{
|
|
478
|
+
Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{rcpt_lst_field} element shall have fields: id and type"}
|
|
448
479
|
when String # CLI helper: need to resolve provided name to type/id
|
|
449
480
|
# email: user, else dropbox
|
|
450
481
|
entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
|
|
451
482
|
begin
|
|
452
483
|
full_recipient_info = lookup_by_name(entity_type, short_recipient_info, query: {'current_workspace_id' => ws_id})
|
|
453
|
-
rescue
|
|
454
|
-
raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
|
|
484
|
+
rescue EntityNotFound
|
|
455
485
|
# dropboxes cannot be created on the fly
|
|
456
|
-
|
|
486
|
+
Aspera.assert_values(entity_type, 'contacts', type: Error){"No such shared inbox in workspace #{ws_id}"}
|
|
457
487
|
# unknown user: create it as external user
|
|
458
488
|
full_recipient_info = create('contacts', {
|
|
459
489
|
'current_workspace_id' => ws_id,
|
|
@@ -465,14 +495,13 @@ module Aspera
|
|
|
465
495
|
else
|
|
466
496
|
{'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
|
|
467
497
|
end
|
|
468
|
-
else #
|
|
469
|
-
raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
|
|
498
|
+
else Aspera.error_unexpected_value(short_recipient_info.class.name){"#{rcpt_lst_field} item must be a String (email, shared inbox) or Hash (id,type)"}
|
|
470
499
|
end
|
|
471
500
|
# add original or resolved recipient info
|
|
472
501
|
resolved_list.push(short_recipient_info)
|
|
473
502
|
end
|
|
474
503
|
# replace with resolved elements
|
|
475
|
-
package_data[
|
|
504
|
+
package_data[rcpt_lst_field] = resolved_list
|
|
476
505
|
return
|
|
477
506
|
end
|
|
478
507
|
|
|
@@ -505,8 +534,8 @@ module Aspera
|
|
|
505
534
|
# package_data['file_names']||=[..list of filenames to transfer...]
|
|
506
535
|
|
|
507
536
|
# lookup users
|
|
508
|
-
resolve_package_recipients(package_data,
|
|
509
|
-
resolve_package_recipients(package_data,
|
|
537
|
+
resolve_package_recipients(package_data, 'recipients', new_user_option)
|
|
538
|
+
resolve_package_recipients(package_data, 'bcc_recipients', new_user_option)
|
|
510
539
|
|
|
511
540
|
validate_metadata(package_data) if validate_meta
|
|
512
541
|
|
|
@@ -622,8 +651,7 @@ module Aspera
|
|
|
622
651
|
when NilClass
|
|
623
652
|
when ''
|
|
624
653
|
# workspace shared folder
|
|
625
|
-
perm_data[
|
|
626
|
-
perm_data['access_id'] = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
|
|
654
|
+
perm_data.merge!(self.class.workspace_access(app_info[:workspace_id]))
|
|
627
655
|
tag_workspace['shared_with_name'] = perm_data['access_id']
|
|
628
656
|
else
|
|
629
657
|
entity_info = lookup_by_name('contacts', shared_with, query: {'current_workspace_id' => app_info[:workspace_id]})
|
data/lib/aspera/api/cos_node.rb
CHANGED
|
@@ -55,8 +55,9 @@ module Aspera
|
|
|
55
55
|
operation: 'GET',
|
|
56
56
|
subpath: bucket,
|
|
57
57
|
headers: {'Accept' => 'application/xml'},
|
|
58
|
-
query: {'faspConnectionInfo' => nil}
|
|
59
|
-
|
|
58
|
+
query: {'faspConnectionInfo' => nil},
|
|
59
|
+
ret: :resp
|
|
60
|
+
).body
|
|
60
61
|
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
|
61
62
|
Log.dump(:ats_info, ats_info)
|
|
62
63
|
@storage_credentials = {
|
data/lib/aspera/api/faspex.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Aspera
|
|
|
28
28
|
|
|
29
29
|
def create_token
|
|
30
30
|
# Exchange context (passcode) for code
|
|
31
|
-
|
|
31
|
+
http = api.call(
|
|
32
32
|
operation: 'GET',
|
|
33
33
|
subpath: @path_authorize,
|
|
34
34
|
query: {
|
|
@@ -37,10 +37,11 @@ module Aspera
|
|
|
37
37
|
client_id: client_id,
|
|
38
38
|
redirect_uri: @redirect_uri
|
|
39
39
|
},
|
|
40
|
-
exception: false
|
|
40
|
+
exception: false,
|
|
41
|
+
ret: :resp
|
|
41
42
|
)
|
|
42
43
|
# code / state located in redirected URL query
|
|
43
|
-
info = Rest.query_to_h(URI.parse(
|
|
44
|
+
info = Rest.query_to_h(URI.parse(http['Location']).query)
|
|
44
45
|
Log.dump(:info, info)
|
|
45
46
|
raise Error, info['action_message'] if info['action_message']
|
|
46
47
|
Aspera.assert(info['code']){'Missing code in answer'}
|
|
@@ -133,7 +134,7 @@ module Aspera
|
|
|
133
134
|
case auth
|
|
134
135
|
when :public_link
|
|
135
136
|
# Get URL of final redirect of public link
|
|
136
|
-
redir_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET')
|
|
137
|
+
redir_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', ret: :resp).uri.to_s
|
|
137
138
|
Log.dump(:redir_url, redir_url, level: :trace1)
|
|
138
139
|
# get context from query
|
|
139
140
|
encoded_context = Rest.query_to_h(URI.parse(redir_url).query)['context']
|
|
@@ -145,7 +146,7 @@ module Aspera
|
|
|
145
146
|
base_url = redir_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
|
|
146
147
|
# Get web UI client_id and redirect_uri
|
|
147
148
|
# TODO: change this for something more reliable
|
|
148
|
-
config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET')
|
|
149
|
+
config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET').sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
|
|
149
150
|
Log.dump(:configjs, config)
|
|
150
151
|
{
|
|
151
152
|
base_url: "#{base_url}/#{PATH_API_V5}",
|
data/lib/aspera/api/node.rb
CHANGED
|
@@ -386,21 +386,19 @@ module Aspera
|
|
|
386
386
|
return oauth.authorization(refresh: true)
|
|
387
387
|
end
|
|
388
388
|
|
|
389
|
+
# Get a base download transfer spec (gen3)
|
|
390
|
+
# @return [Hash] Base transfer spec
|
|
391
|
+
def base_spec
|
|
392
|
+
create(
|
|
393
|
+
'files/download_setup',
|
|
394
|
+
{transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
|
|
395
|
+
)['transfer_specs'].first['transfer_spec']
|
|
396
|
+
end
|
|
397
|
+
|
|
389
398
|
# Get generic part of transfer spec with transport parameters only
|
|
390
399
|
# @return [Hash] Base transfer spec
|
|
391
400
|
def transport_params
|
|
392
|
-
|
|
393
|
-
# Retrieve values from API (and keep a copy/cache)
|
|
394
|
-
full_spec = create(
|
|
395
|
-
'files/download_setup',
|
|
396
|
-
{transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
|
|
397
|
-
)['transfer_specs'].first['transfer_spec']
|
|
398
|
-
# Set available fields
|
|
399
|
-
@std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
|
|
400
|
-
h[i] = full_spec[i] if full_spec.key?(i)
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
return @std_t_spec_cache
|
|
401
|
+
@std_t_spec_cache ||= base_spec.slice(*Transfer::Spec::TRANSPORT_FIELDS).freeze
|
|
404
402
|
end
|
|
405
403
|
|
|
406
404
|
# Create transfer spec for gen4
|
data/lib/aspera/ascmd.rb
CHANGED
|
@@ -94,8 +94,7 @@ module Aspera
|
|
|
94
94
|
arguments = [] if arguments.nil?
|
|
95
95
|
Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
|
|
96
96
|
Aspera.assert_type(action_sym, Symbol)
|
|
97
|
-
Aspera.
|
|
98
|
-
Aspera.assert(arguments.all?(String), 'arguments must be strings')
|
|
97
|
+
Aspera.assert_array_all(arguments, String){'arguments'}
|
|
99
98
|
remote_cmd = 'ascmd'
|
|
100
99
|
# lines of commands (String's)
|
|
101
100
|
command_lines = []
|
|
@@ -33,25 +33,12 @@ module Aspera
|
|
|
33
33
|
class Installation
|
|
34
34
|
include Singleton
|
|
35
35
|
|
|
36
|
-
DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
|
|
37
|
-
<?xml version='1.0' encoding='UTF-8'?>
|
|
38
|
-
<CONF version="2">
|
|
39
|
-
<default>
|
|
40
|
-
<file_system>
|
|
41
|
-
<resume_suffix>.aspera-ckpt</resume_suffix>
|
|
42
|
-
<partial_file_suffix>.partial</partial_file_suffix>
|
|
43
|
-
</file_system>
|
|
44
|
-
</default>
|
|
45
|
-
</CONF>
|
|
46
|
-
END_OF_CONFIG_FILE
|
|
47
|
-
# all executable files from SDK
|
|
48
|
-
EXE_FILES = %i[ascp ascp4 async transferd].freeze
|
|
49
|
-
SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
|
|
50
|
-
TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
|
|
51
|
-
# filename for ascp with optional extension (Windows)
|
|
52
|
-
private_constant :DEFAULT_ASPERA_CONF, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
|
|
53
36
|
# options for SSH client private key
|
|
54
37
|
CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
|
|
38
|
+
# prefix
|
|
39
|
+
USE_PRODUCT_PREFIX = 'product:'
|
|
40
|
+
# policy for product selection
|
|
41
|
+
FIRST_FOUND = 'FIRST'
|
|
55
42
|
|
|
56
43
|
# Loads YAML from cloud with locations of SDK archives for all platforms
|
|
57
44
|
# @return location structure
|
|
@@ -66,12 +53,13 @@ module Aspera
|
|
|
66
53
|
end
|
|
67
54
|
end
|
|
68
55
|
|
|
69
|
-
# set ascp executable
|
|
56
|
+
# set ascp executable "location"
|
|
70
57
|
def ascp_path=(v)
|
|
71
58
|
Aspera.assert_type(v, String)
|
|
72
|
-
Aspera.assert(!v.empty?){'ascp
|
|
73
|
-
|
|
74
|
-
@
|
|
59
|
+
Aspera.assert(!v.empty?){'ascp location cannot be empty: check your config file'}
|
|
60
|
+
@ascp_location = v
|
|
61
|
+
@ascp_path = nil
|
|
62
|
+
return
|
|
75
63
|
end
|
|
76
64
|
|
|
77
65
|
def ascp_path
|
|
@@ -93,8 +81,7 @@ module Aspera
|
|
|
93
81
|
pl = installed_products.find{ |i| i[:name].eql?(product_name)}
|
|
94
82
|
raise "No such product installed: #{product_name}" if pl.nil?
|
|
95
83
|
end
|
|
96
|
-
|
|
97
|
-
Log.log.debug{"ascp_path=#{@path_to_ascp}"}
|
|
84
|
+
@ascp_path = pl[:ascp_path]
|
|
98
85
|
end
|
|
99
86
|
|
|
100
87
|
# @return [Hash] with key = file name (String), and value = path to file
|
|
@@ -111,7 +98,9 @@ module Aspera
|
|
|
111
98
|
end
|
|
112
99
|
end
|
|
113
100
|
|
|
101
|
+
# TODO: if using another product than SDK, should use files from there
|
|
114
102
|
def check_or_create_sdk_file(filename, force: false, &block)
|
|
103
|
+
FileUtils.mkdir_p(Products::Transferd.sdk_directory)
|
|
115
104
|
return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
|
|
116
105
|
end
|
|
117
106
|
|
|
@@ -127,10 +116,18 @@ module Aspera
|
|
|
127
116
|
file = if k.eql?(:transferd)
|
|
128
117
|
Products::Transferd.transferd_path
|
|
129
118
|
else
|
|
130
|
-
#
|
|
131
|
-
|
|
119
|
+
# find ascp when needed
|
|
120
|
+
if @ascp_path.nil?
|
|
121
|
+
if @ascp_location.start_with?(USE_PRODUCT_PREFIX)
|
|
122
|
+
use_ascp_from_product(@ascp_location[USE_PRODUCT_PREFIX.length..-1])
|
|
123
|
+
else
|
|
124
|
+
@ascp_path = @ascp_location
|
|
125
|
+
end
|
|
126
|
+
Aspera.assert(File.exist?(@ascp_path)){"No such file: [#{@ascp_path}]"}
|
|
127
|
+
Log.log.debug{"ascp_path=#{@ascp_path}"}
|
|
128
|
+
end
|
|
132
129
|
# NOTE: that there might be a .exe at the end
|
|
133
|
-
@
|
|
130
|
+
@ascp_path.gsub('ascp', k.to_s)
|
|
134
131
|
end
|
|
135
132
|
when :ssh_private_dsa, :ssh_private_rsa
|
|
136
133
|
# assume last 3 letters are type
|
|
@@ -195,7 +192,9 @@ module Aspera
|
|
|
195
192
|
return exe_version
|
|
196
193
|
end
|
|
197
194
|
|
|
198
|
-
|
|
195
|
+
# Extract some stings from ascp logs
|
|
196
|
+
# Folder, PVCL, version, license information
|
|
197
|
+
def ascp_info_from_log
|
|
199
198
|
data = {}
|
|
200
199
|
# read PATHs from ascp directly, and pvcl modules as well
|
|
201
200
|
Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
|
|
@@ -227,8 +226,9 @@ module Aspera
|
|
|
227
226
|
return data
|
|
228
227
|
end
|
|
229
228
|
|
|
230
|
-
#
|
|
231
|
-
|
|
229
|
+
# Extract some stings from ascp binary
|
|
230
|
+
# Openssl information
|
|
231
|
+
def ascp_info_from_file
|
|
232
232
|
data = {}
|
|
233
233
|
File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
|
|
234
234
|
if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
|
|
@@ -243,17 +243,17 @@ module Aspera
|
|
|
243
243
|
# information for `ascp info`
|
|
244
244
|
def ascp_info
|
|
245
245
|
ascp_data = file_paths
|
|
246
|
-
ascp_data.merge!(
|
|
247
|
-
ascp_data.merge!(
|
|
246
|
+
ascp_data.merge!(ascp_info_from_log)
|
|
247
|
+
ascp_data.merge!(ascp_info_from_file)
|
|
248
248
|
return ascp_data
|
|
249
249
|
end
|
|
250
250
|
|
|
251
251
|
# @return the url for download of SDK archive for the given platform and version
|
|
252
252
|
def sdk_url_for_platform(platform: nil, version: nil)
|
|
253
|
-
|
|
253
|
+
all_locations = sdk_locations
|
|
254
254
|
platform = Environment.instance.architecture if platform.nil?
|
|
255
|
-
locations =
|
|
256
|
-
raise "No SDK for platform: #{platform}" if locations.empty?
|
|
255
|
+
locations = all_locations.select{ |l| l['platform'].eql?(platform)}
|
|
256
|
+
raise "No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}" if locations.empty?
|
|
257
257
|
version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
|
|
258
258
|
info = locations.select{ |entry| entry['version'].eql?(version)}
|
|
259
259
|
raise "No such version: #{version} for #{platform}" if info.empty?
|
|
@@ -355,13 +355,27 @@ module Aspera
|
|
|
355
355
|
|
|
356
356
|
private
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
358
|
+
DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
|
|
359
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
|
360
|
+
<CONF version="2">
|
|
361
|
+
<default>
|
|
362
|
+
<file_system>
|
|
363
|
+
<resume_suffix>.aspera-ckpt</resume_suffix>
|
|
364
|
+
<partial_file_suffix>.partial</partial_file_suffix>
|
|
365
|
+
</file_system>
|
|
366
|
+
</default>
|
|
367
|
+
</CONF>
|
|
368
|
+
END_OF_CONFIG_FILE
|
|
369
|
+
# all executable files from SDK
|
|
370
|
+
EXE_FILES = %i[ascp ascp4 async transferd].freeze
|
|
371
|
+
SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
|
|
372
|
+
TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
|
|
373
|
+
# filename for ascp with optional extension (Windows)
|
|
374
|
+
private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
|
|
362
375
|
|
|
363
376
|
def initialize
|
|
364
|
-
@
|
|
377
|
+
@ascp_path = nil
|
|
378
|
+
@ascp_location = nil
|
|
365
379
|
@sdk_dir = nil
|
|
366
380
|
@found_products = nil
|
|
367
381
|
@transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
|
data/lib/aspera/assert.rb
CHANGED
|
@@ -41,8 +41,8 @@ module Aspera
|
|
|
41
41
|
return if assertion
|
|
42
42
|
message = 'assertion failed'
|
|
43
43
|
info = yield if block_given?
|
|
44
|
-
message = "#{message}: #{info}" if info
|
|
45
|
-
message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
|
|
44
|
+
message = type.eql?(AssertError) ? "#{message}: #{info}" : info if info
|
|
45
|
+
# message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
|
|
46
46
|
report_error(type, message)
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -52,7 +52,29 @@ module Aspera
|
|
|
52
52
|
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
53
53
|
# @param block [Proc] Additional description in front of message
|
|
54
54
|
def assert_type(value, *classes, type: AssertError)
|
|
55
|
-
assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have #{value.inspect}"}
|
|
55
|
+
assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have (#{value.class})#{value.inspect}"}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Assert that all value of array are of the same specified type
|
|
59
|
+
# @param array [Array] The array to check
|
|
60
|
+
# @param klass [Class] The expected type of elements
|
|
61
|
+
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
62
|
+
# @param block [Proc] Additional description in front of message
|
|
63
|
+
def assert_array_all(array, klass, type: AssertError)
|
|
64
|
+
assert_type(array, Array, type: type)
|
|
65
|
+
assert(array.all?(klass), type: type){"#{"#{yield}: " if block_given?}expecting all as #{klass}, but have #{array.map(&:class).uniq}"}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Assert value is Hash, keys have type, and Values have type
|
|
69
|
+
# @param hash [Hash] The hash to check
|
|
70
|
+
# @param key_class [Class] The expected type of keys (or nil)
|
|
71
|
+
# @param value_class [Class] The expected type of values (or nil)
|
|
72
|
+
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
73
|
+
# @param block [Proc] Additional description in front of message
|
|
74
|
+
def assert_hash_all(hash, key_class, value_class, type: AssertError)
|
|
75
|
+
assert_type(hash, Hash, type: type)
|
|
76
|
+
assert_array_all(hash.keys, key_class, type: AssertError){"#{"#{yield}: " if block_given?}keys"} unless key_class.nil?
|
|
77
|
+
assert_array_all(hash.values, value_class, type: AssertError){"#{"#{yield}: " if block_given?}values"} unless value_class.nil?
|
|
56
78
|
end
|
|
57
79
|
|
|
58
80
|
# Assert that value is one of the given values
|
data/lib/aspera/cli/error.rb
CHANGED
|
@@ -6,11 +6,13 @@ module Aspera
|
|
|
6
6
|
class Error < StandardError; end
|
|
7
7
|
# Raised when an unexpected argument is provided.
|
|
8
8
|
class BadArgument < Error; end
|
|
9
|
+
class MissingArgument < Error; end
|
|
9
10
|
class NoSuchElement < Error; end
|
|
10
11
|
|
|
11
12
|
class BadIdentifier < Error
|
|
12
|
-
def initialize(res_type, res_id)
|
|
13
|
-
|
|
13
|
+
def initialize(res_type, res_id, field: 'identifier', count: 0)
|
|
14
|
+
msg = count.eql?(0) ? 'not found' : "found #{count}"
|
|
15
|
+
super("#{res_type} with #{field}=#{res_id}: #{msg}")
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|