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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -758
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +671 -419
  6. data/lib/aspera/api/aoc.rb +71 -43
  7. data/lib/aspera/api/cos_node.rb +3 -2
  8. data/lib/aspera/api/faspex.rb +6 -5
  9. data/lib/aspera/api/node.rb +10 -12
  10. data/lib/aspera/ascmd.rb +1 -2
  11. data/lib/aspera/ascp/installation.rb +53 -39
  12. data/lib/aspera/assert.rb +25 -3
  13. data/lib/aspera/cli/error.rb +4 -2
  14. data/lib/aspera/cli/extended_value.rb +84 -60
  15. data/lib/aspera/cli/formatter.rb +55 -22
  16. data/lib/aspera/cli/main.rb +21 -14
  17. data/lib/aspera/cli/manager.rb +348 -247
  18. data/lib/aspera/cli/plugins/alee.rb +3 -3
  19. data/lib/aspera/cli/plugins/aoc.rb +70 -14
  20. data/lib/aspera/cli/plugins/base.rb +57 -49
  21. data/lib/aspera/cli/plugins/config.rb +69 -84
  22. data/lib/aspera/cli/plugins/console.rb +13 -8
  23. data/lib/aspera/cli/plugins/cos.rb +1 -1
  24. data/lib/aspera/cli/plugins/faspex.rb +32 -26
  25. data/lib/aspera/cli/plugins/faspex5.rb +45 -43
  26. data/lib/aspera/cli/plugins/faspio.rb +5 -5
  27. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  28. data/lib/aspera/cli/plugins/node.rb +131 -120
  29. data/lib/aspera/cli/plugins/oauth.rb +1 -1
  30. data/lib/aspera/cli/plugins/orchestrator.rb +114 -32
  31. data/lib/aspera/cli/plugins/preview.rb +26 -46
  32. data/lib/aspera/cli/plugins/server.rb +6 -8
  33. data/lib/aspera/cli/plugins/shares.rb +27 -32
  34. data/lib/aspera/cli/sync_actions.rb +49 -38
  35. data/lib/aspera/cli/transfer_agent.rb +16 -34
  36. data/lib/aspera/cli/version.rb +1 -1
  37. data/lib/aspera/cli/wizard.rb +8 -5
  38. data/lib/aspera/command_line_builder.rb +20 -17
  39. data/lib/aspera/coverage.rb +1 -1
  40. data/lib/aspera/environment.rb +41 -34
  41. data/lib/aspera/faspex_gw.rb +1 -1
  42. data/lib/aspera/keychain/factory.rb +1 -2
  43. data/lib/aspera/markdown.rb +31 -0
  44. data/lib/aspera/nagios.rb +6 -5
  45. data/lib/aspera/oauth/base.rb +17 -27
  46. data/lib/aspera/oauth/factory.rb +1 -1
  47. data/lib/aspera/oauth/url_json.rb +2 -1
  48. data/lib/aspera/preview/file_types.rb +23 -37
  49. data/lib/aspera/products/connect.rb +3 -3
  50. data/lib/aspera/rest.rb +51 -39
  51. data/lib/aspera/rest_error_analyzer.rb +4 -4
  52. data/lib/aspera/ssh.rb +5 -2
  53. data/lib/aspera/ssl.rb +41 -0
  54. data/lib/aspera/sync/conf.schema.yaml +182 -34
  55. data/lib/aspera/sync/database.rb +2 -1
  56. data/lib/aspera/sync/operations.rb +125 -69
  57. data/lib/aspera/transfer/parameters.rb +3 -4
  58. data/lib/aspera/transfer/spec.rb +2 -3
  59. data/lib/aspera/transfer/spec.schema.yaml +48 -18
  60. data/lib/aspera/transfer/spec_doc.rb +14 -14
  61. data/lib/aspera/uri_reader.rb +1 -1
  62. data/lib/transferd_pb.rb +2 -2
  63. data.tar.gz.sig +0 -0
  64. metadata +19 -6
  65. metadata.gz.sig +3 -2
@@ -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
- raise "No host found in URL.Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}" if uri.host.nil?
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')[:http].uri
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
- raise Error, 'AoC shall redirect to login page with a query' if final_uri.query.nil?
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 a hash with :data and :http keys
144
+ # Block must return an Array with data and http response
145
145
  # @return [Hash] {items: , total: }
146
- def call_paging(query: {}, formatter: nil)
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
- result = yield(new_query)
162
- Aspera.assert(result[:data])
163
- Aspera.assert(result[:http])
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 = result[:data]
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(url:, auth:, subpath: API_V1, client_id: nil, client_secret: nil, scope: nil, redirect_uri: nil, private_key: nil, passphrase: nil, username: nil,
184
- password: nil, workspace: nil, secret_finder: nil)
185
- # test here because link may set url
186
- raise ParameterError, 'Missing mandatory option: url' if url.nil?
187
- raise ParameterError, 'Missing mandatory option: scope' if scope.nil?
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
- raise ParameterError, 'Missing mandatory option: auth' if auth.nil?
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
- raise ParameterError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
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
- raise ParameterError, 'Missing mandatory option: private_key' if private_key.nil?
226
- raise ParameterError, 'Missing mandatory option: username' if username.nil?
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: {}, formatter: nil)
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
- # Use `call` instead of `read` to get headers
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
- raise "Cannot get user's home node id, check your default workspace or specify one" if @home_info[:node_id].to_s.empty?
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
- raise "only one field with name #{field['name']} allowed" if provided.count > 1
427
- raise "missing mandatory field: #{field['name']}" if field['required'] && provided.empty?
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 The whole package creation payload
435
- # @param recipient_list_field The field in structure, i.e. recipients or bcc_recipients
436
- # @return nil package_data is modified
437
- def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
438
- return unless package_data.key?(recipient_list_field)
439
- Aspera.assert_type(package_data[recipient_list_field], Array){recipient_list_field}
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[recipient_list_field].each do |short_recipient_info|
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])){"#{recipient_list_field} element shall have fields: id and 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 RuntimeError => e
454
- raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
484
+ rescue EntityNotFound
455
485
  # dropboxes cannot be created on the fly
456
- raise "No such shared inbox in workspace #{ws_id}" if entity_type.eql?('dropboxes')
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 # unexpected extended value, must be String or Hash
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[recipient_list_field] = resolved_list
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, package_data['workspace_id'], 'recipients', new_user_option)
509
- resolve_package_recipients(package_data, package_data['workspace_id'], 'bcc_recipients', new_user_option)
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['access_type'] = 'user'
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]})
@@ -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
- )[:http].body
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 = {
@@ -28,7 +28,7 @@ module Aspera
28
28
 
29
29
  def create_token
30
30
  # Exchange context (passcode) for code
31
- resp = api.call(
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(resp[:http]['Location']).query)
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')[:http].uri.to_s
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')[:data].sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
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}",
@@ -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
- if @std_t_spec_cache.nil?
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.assert_type(arguments, Array)
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 path
56
+ # set ascp executable "location"
70
57
  def ascp_path=(v)
71
58
  Aspera.assert_type(v, String)
72
- Aspera.assert(!v.empty?){'ascp path cannot be empty: check your config file'}
73
- Aspera.assert(File.exist?(v)){"No such file: [#{v}]"}
74
- @path_to_ascp = v
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
- self.ascp_path = pl[:ascp_path]
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
- # ensure at least ascp is found
131
- use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
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
- @path_to_ascp.gsub('ascp', k.to_s)
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
- def ascp_pvcl_info
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
- # extract some stings from ascp binary
231
- def ascp_ssl_info
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!(ascp_pvcl_info)
247
- ascp_data.merge!(ascp_ssl_info)
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
- locations = sdk_locations
253
+ all_locations = sdk_locations
254
254
  platform = Environment.instance.architecture if platform.nil?
255
- locations = locations.select{ |l| l['platform'].eql?(platform)}
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
- # policy for product selection
359
- FIRST_FOUND = 'FIRST'
360
-
361
- private_constant :FIRST_FOUND
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
- @path_to_ascp = nil
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
@@ -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
- super("#{res_type} with identifier #{res_id} not found")
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