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
@@ -17,13 +17,13 @@ module Aspera
17
17
  nagios = Nagios.new
18
18
  begin
19
19
  api = Api::Alee.new(nil, nil, version: 'ping')
20
- result = api.call(operation: 'GET')
21
- raise "unexpected response: #{result[:http].body}" unless result[:http].body.eql?('pong')
20
+ http = api.read(nil, ret: :resp)
21
+ raise "unexpected response: #{http.body}" unless http.body.eql?('pong')
22
22
  nagios.add_ok('api', 'answered ok')
23
23
  rescue StandardError => e
24
24
  nagios.add_critical('api', e.to_s)
25
25
  end
26
- return nagios.result
26
+ Main.result_object_list(nagios.status_list)
27
27
  when :entitlement
28
28
  entitlement_id = options.get_option(:username, mandatory: true)
29
29
  customer_id = options.get_option(:password, mandatory: true)
@@ -69,9 +69,9 @@ module Aspera
69
69
  base_url = "#{base_url}.#{Api::AoC::SAAS_DOMAIN_PROD}" unless base_url.include?('.')
70
70
  # AoC is only https
71
71
  return unless base_url.start_with?('https://')
72
- res_http = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false)[:http]
73
- return if res_http['Location'].nil?
74
- redirect_uri = URI.parse(res_http['Location'])
72
+ location = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false, ret: :resp)['Location']
73
+ return if location.nil?
74
+ redirect_uri = URI.parse(location)
75
75
  od = Api::AoC.split_org_domain(URI.parse(base_url))
76
76
  return unless redirect_uri.path.end_with?("oauth2/#{od[:organization]}/login")
77
77
  # either in standard domain, or product name in page
@@ -132,7 +132,7 @@ module Aspera
132
132
  test_args: 'organization'
133
133
  }
134
134
  end
135
- options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', values: :bool, default: Api::AoC.saas_url?(app_url))
135
+ options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
136
136
  options.parse_options!
137
137
  # make username mandatory for jwt, this triggers interactive input
138
138
  wiz_username = options.get_option(:username, mandatory: true)
@@ -205,10 +205,10 @@ module Aspera
205
205
  @cache_workspace_info = nil
206
206
  @cache_home_node_file = nil
207
207
  @cache_api_aoc = nil
208
- options.declare(:workspace, 'Name of workspace', types: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
209
- options.declare(:new_user_option, 'New user creation option for unknown package recipients', types: Hash)
210
- options.declare(:validate_metadata, 'Validate shared inbox metadata', values: :bool, default: true)
211
- options.declare(:package_folder, 'Field of package to use as folder name, or @none:', types: [String, NilClass])
208
+ options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
209
+ options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
210
+ options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
211
+ options.declare(:package_folder, 'Field of package to use as folder name, or @none:', allowed: [String, NilClass])
212
212
  options.parse_options!
213
213
  # add node plugin options (for manual)
214
214
  Node.declare_options(options)
@@ -288,7 +288,7 @@ module Aspera
288
288
  query = query_read_delete(default: default_query)
289
289
  # caller may add specific modifications or checks to query
290
290
  yield(query) if block_given?
291
- result = aoc_api.read_with_paging(resource_class_path, query: base_query.merge(query).compact, formatter: formatter)
291
+ result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact, formatter: formatter)
292
292
  return Main.result_object_list(result[:items], fields: fields, total: result[:total])
293
293
  end
294
294
 
@@ -313,7 +313,7 @@ module Aspera
313
313
  Aspera.assert_type(query, Hash){'query'}
314
314
  PACKAGE_RECEIVED_BASE_QUERY.each{ |k, v| query[k] = v unless query.key?(k)}
315
315
  resolve_dropbox_name_default_ws_id(query)
316
- return aoc_api.read_with_paging('packages', query: query.compact, formatter: formatter)
316
+ return aoc_api.read_with_paging('packages', query.compact, formatter: formatter)
317
317
  end
318
318
 
319
319
  NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
@@ -374,6 +374,7 @@ module Aspera
374
374
  Aspera.error_unreachable_line
375
375
  end
376
376
 
377
+ # @param resource_type [Symbol] One of ADMIN_OBJECTS
377
378
  def execute_resource_action(resource_type)
378
379
  # get path on API, resource type is singular, but api is plural
379
380
  resource_class_path =
@@ -391,8 +392,9 @@ module Aspera
391
392
  global_operations = %i[create list]
392
393
  supported_operations = %i[show modify]
393
394
  supported_operations.push(:delete, *global_operations) unless singleton_object
394
- supported_operations.push(:do) if resource_type.eql?(:node)
395
+ supported_operations.push(:do, :bearer_token) if resource_type.eql?(:node)
395
396
  supported_operations.push(:set_pub_key) if resource_type.eql?(:client)
397
+ supported_operations.push(:shared_folder, :dropbox) if resource_type.eql?(:workspace)
396
398
  command = options.get_next_command(supported_operations)
397
399
  # require identifier for non global commands
398
400
  if !singleton_object && !global_operations.include?(command)
@@ -454,6 +456,60 @@ module Aspera
454
456
  when :do
455
457
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
456
458
  return execute_nodegen4_command(command_repo, res_id)
459
+ when :bearer_token
460
+ node_api = aoc_api.node_api_from(
461
+ node_id: res_id,
462
+ scope: options.get_next_argument('scope')
463
+ )
464
+ return Main.result_text(node_api.oauth.authorization)
465
+ when :dropbox
466
+ command_shared = options.get_next_command(%i[list])
467
+ case command_shared
468
+ when :list
469
+ query = options.get_option(:query) || {}
470
+ res_data = aoc_api.read('dropboxes', query.merge({'workspace_id'=>res_id}))
471
+ return Main.result_object_list(res_data, fields: %w[id name description])
472
+ end
473
+ when :shared_folder
474
+ query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
475
+ shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
476
+ # inside a workspace
477
+ command_shared = options.get_next_command(%i[list member])
478
+ case command_shared
479
+ when :list
480
+ return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
481
+ when :member
482
+ shared_folder_id = instance_identifier
483
+ shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
484
+ Aspera.assert(shared_folder)
485
+ command_shared_member = options.get_next_command(%i[list])
486
+ case command_shared_member
487
+ when :list
488
+ node_api = aoc_api.node_api_from(
489
+ node_id: shared_folder['node_id'],
490
+ workspace_id: res_id,
491
+ workspace_name: nil,
492
+ scope: Api::Node::SCOPE_USER
493
+ )
494
+ result = node_api.read(
495
+ 'permissions',
496
+ {'file_id' => shared_folder['file_id'], 'tag' => "aspera.files.workspace.id=#{res_id}"}
497
+ )
498
+ result.each do |item|
499
+ item['member'] = begin
500
+ if Api::AoC.workspace_access?(item)
501
+ {'name'=>'[Internal permission]'}
502
+ else
503
+ aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name': 'not found'}
504
+ end
505
+ rescue => e
506
+ {'name'=>e.to_s}
507
+ end
508
+ end
509
+ # TODO : read users and group name and add, if query "include_members"
510
+ return Main.result_object_list(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
511
+ end
512
+ end
457
513
  else Aspera.error_unexpected_value(command)
458
514
  end
459
515
  end
@@ -736,7 +792,7 @@ module Aspera
736
792
  }
737
793
  return result_list('short_links', fields: Formatter.all_but('data'), base_query: list_params) if command.eql?(:list)
738
794
  one_id = instance_identifier
739
- found = aoc_api.read_with_paging('short_links', query: list_params, formatter: formatter)[:items].find{ |item| item['id'].eql?(one_id)}
795
+ found = aoc_api.read_with_paging('short_links', list_params, formatter: formatter)[:items].find{ |item| item['id'].eql?(one_id)}
740
796
  raise Cli::BadIdentifier.new('Short link', one_id) if found.nil?
741
797
  return Main.result_single_object(found, fields: Formatter.all_but('data'))
742
798
  when :modify
@@ -886,7 +942,7 @@ module Aspera
886
942
  # enforce workspace id from link (should be already ok, but in case user wanted to override)
887
943
  package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
888
944
  end
889
- package_data['encryption_at_rest'] = true if transfer.option_transfer_spec['content_protection'].eql?('encrypt')
945
+ package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
890
946
  # transfer may raise an error
891
947
  created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
892
948
  Main.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
@@ -949,7 +1005,7 @@ module Aspera
949
1005
  destination_folder,
950
1006
  Environment.instance.sanitized_filename(package_info[per_package_field1])
951
1007
  )
952
- transfer.option_transfer_spec['destination_root'] = self.class.unique_folder(
1008
+ transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(
953
1009
  folder,
954
1010
  extension: per_package_field2.eql?('seq') ? :seq : package_info[per_package_field2],
955
1011
  always: per_package_sub_always
@@ -18,15 +18,22 @@ module Aspera
18
18
  MAX_ITEMS = 'max'
19
19
  # Special query parameter: `pmax`: max number of pages for list command
20
20
  MAX_PAGES = 'pmax'
21
- # Special identifier format: look for this name to find where supported
22
- REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
23
21
 
24
22
  class << self
25
23
  def declare_options(options)
26
- options.declare(:query, 'Additional filter for for some commands (list/delete)', types: [Hash, Array])
24
+ options.declare(:query, 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass])
27
25
  options.declare(:property, 'Name of property to set (modify operation)')
28
- options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
29
- options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
26
+ options.declare(:bulk, 'Bulk operation (only some)', allowed: Allowed::TYPES_BOOLEAN, default: false)
27
+ options.declare(:bfail, 'Bulk operation error handling', allowed: Allowed::TYPES_BOOLEAN, default: true)
28
+ end
29
+
30
+ # @return [Hash,NilClass] `{field:,value:}` if identifier is a percent selector, else `nil`
31
+ def percent_selector(identifier)
32
+ Aspera.assert_type(identifier, String)
33
+ if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
34
+ return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
35
+ end
36
+ return
30
37
  end
31
38
  end
32
39
 
@@ -55,26 +62,17 @@ module Aspera
55
62
  options.parser.separator('OPTIONS:') if has_options
56
63
  end
57
64
 
58
- # Must be called AFTER the instance action:
59
- # ... folder browse _call_instance_identifier
65
+ # Resource identifier as positional parameter
60
66
  #
61
67
  # @param description [String] description of the identifier
62
- # @param as_option [Symbol] option name to use if identifier is an option
63
68
  # @param block [Proc] block to search for identifier based on attribute value
64
69
  # @return [String, Array] identifier or list of ids
65
- def instance_identifier(description: 'identifier', as_option: nil, &block)
66
- if as_option.nil?
67
- res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
68
- else
69
- res_id = options.get_option(as_option)
70
- end
70
+ def instance_identifier(description: 'identifier', &block)
71
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
71
72
  # Can be an Array
72
- if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
73
- if block
74
- res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
75
- else
76
- raise Cli::BadArgument, "Percent syntax for #{description} not supported in this context"
77
- end
73
+ if res_id.is_a?(String) && (m = Base.percent_selector(res_id))
74
+ Aspera.assert(block, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
75
+ res_id = yield(m[:field], m[:value])
78
76
  end
79
77
  return res_id
80
78
  end
@@ -172,12 +170,11 @@ module Aspera
172
170
  if !delete_style.nil?
173
171
  one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
174
172
  Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
175
- api.call(
176
- operation: 'DELETE',
177
- subpath: entity,
173
+ api.delete(
174
+ entity,
175
+ nil,
178
176
  content_type: Rest::MIME_JSON,
179
- body: {delete_style => one_res_id},
180
- headers: {'Accept' => Rest::MIME_JSON}
177
+ body: {delete_style => one_res_id}
181
178
  )
182
179
  return Main.result_status('deleted')
183
180
  end
@@ -192,11 +189,10 @@ module Aspera
192
189
  data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
193
190
  return Main.result_object_list(data, total: total, fields: display_fields)
194
191
  end
195
- resp = api.call(operation: 'GET', subpath: entity, headers: {'Accept' => Rest::MIME_JSON}, query: query_read_delete)
196
- return Main.result_empty if resp[:http].code == '204'
197
- data = resp[:data]
192
+ data, http = api.read(entity, query_read_delete, ret: :both)
193
+ return Main.result_empty if http.code == '204'
198
194
  # TODO: not generic : which application is this for ?
199
- if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
195
+ if http['Content-Type'].start_with?('application/vnd.api+json')
200
196
  Log.log.debug('is vnd.api')
201
197
  data = data[entity]
202
198
  end
@@ -223,9 +219,8 @@ module Aspera
223
219
 
224
220
  # Query parameters in URL suitable for REST: list/GET and delete/DELETE
225
221
  def query_read_delete(default: nil)
226
- query = options.get_option(:query)
227
222
  # Dup default, as it could be frozen
228
- query = default.dup if query.nil?
223
+ query = options.get_option(:query) || default.dup
229
224
  Log.log.debug{"query_read_delete=#{query}".bg_red}
230
225
  begin
231
226
  # Check it is suitable
@@ -249,7 +244,7 @@ module Aspera
249
244
  value = default if value.nil?
250
245
  unless type.nil?
251
246
  type = [type] unless type.is_a?(Array)
252
- Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
247
+ Aspera.assert_array_all(type, Class){'check types'}
253
248
  if bulk
254
249
  Aspera.assert_type(value, Array, type: Cli::BadArgument)
255
250
  value.each do |v|
@@ -263,10 +258,10 @@ module Aspera
263
258
  end
264
259
 
265
260
  # Get a (full or partial) list of all entities of a given type with query: offset/limit
266
- # @param `api` [Rest] the API object
267
- # @param `entity` [String,Symbol] the API endpoint of entity to list
268
- # @param `items_key` [String] key in the result to get the list of items
269
- # @param `query` [Hash,nil] additional query parameters
261
+ # @param api [Rest] API object
262
+ # @param entity [String,Symbol] API endpoint of entity to list
263
+ # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
264
+ # @param query [Hash,nil] Additional query parameters
270
265
  # @return [Array] items, total_count
271
266
  def list_entities_limit_offset_total_count(
272
267
  api:,
@@ -309,26 +304,39 @@ module Aspera
309
304
  return result, total_count
310
305
  end
311
306
 
312
- # Lookup an entity id from its name
313
- # @param entity [String] the type of entity to lookup, by default it is the path, and it is also the field name in result
314
- # @param value [String] the value to lookup
315
- # @param field [String] the field to match, by default it is 'name'
316
- # @param items_key [String] key in the result to get the list of items (override entity)
317
- # @param query [Hash] additional query parameters
307
+ # Lookup an entity id from its name.
308
+ # Uses query `q` if `query` is `:default` and `field` is `name`.
309
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
310
+ # @param value [String] Value to lookup
311
+ # @param field [String] Field to match, by default it is `'name'`
312
+ # @param items_key [String] Key in the result to get the list of items (override entity)
313
+ # @param query [Hash] Additional query parameters (Default: `:default`)
318
314
  def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
319
315
  if query.eql?(:default)
320
316
  Aspera.assert(field.eql?('name')){'Default query is on name only'}
321
317
  query = {'q'=> value}
322
318
  end
323
- found = list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first.select{ |i| i[field].eql?(value)}
324
- case found.length
325
- when 0 then raise "No #{entity} with #{field} = #{value}"
326
- when 1 then return found.first
327
- else raise "Found #{found.length} #{entity} with #{field} = #{value}"
328
- end
319
+ lookup_entity_generic(entity: entity, field: field, value: value){list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first}
320
+ end
321
+
322
+ # Lookup entity by field and value. Extract single result from list of result returned by block.
323
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
324
+ # @param value [String] Value to lookup
325
+ # @param field [String] Field to match, by default it is `'name'`
326
+ # @param block [Proc] Get list of entity matching query.
327
+ def lookup_entity_generic(entity:, value:, field: 'name', &block)
328
+ Aspera.assert(block_given?)
329
+ found = yield
330
+ Aspera.assert_array_all(found, Hash)
331
+ found = found.select{ |i| i[field].eql?(value)}
332
+ return found.first if found.length.eql?(1)
333
+ raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
329
334
  end
335
+
330
336
  PER_PAGE_DEFAULT = 1000
331
- private_constant :PER_PAGE_DEFAULT
337
+ # Percent selector: select by this field for this value
338
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
339
+ private_constant :PER_PAGE_DEFAULT, :REGEX_LOOKUP_ID_BY_FIELD
332
340
  end
333
341
  end
334
342
  end