aspera-cli 4.24.2 → 4.25.0.pre2

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 (73) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1067 -758
  4. data/CONTRIBUTING.md +93 -120
  5. data/README.md +817 -510
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/transferd.rb +4 -4
  8. data/lib/aspera/api/aoc.rb +71 -43
  9. data/lib/aspera/api/cos_node.rb +3 -2
  10. data/lib/aspera/api/faspex.rb +6 -5
  11. data/lib/aspera/api/node.rb +10 -12
  12. data/lib/aspera/ascmd.rb +1 -2
  13. data/lib/aspera/ascp/installation.rb +55 -41
  14. data/lib/aspera/ascp/management.rb +9 -5
  15. data/lib/aspera/assert.rb +28 -6
  16. data/lib/aspera/cli/error.rb +4 -2
  17. data/lib/aspera/cli/extended_value.rb +94 -62
  18. data/lib/aspera/cli/formatter.rb +55 -22
  19. data/lib/aspera/cli/main.rb +21 -14
  20. data/lib/aspera/cli/manager.rb +349 -248
  21. data/lib/aspera/cli/plugins/alee.rb +3 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +94 -51
  23. data/lib/aspera/cli/plugins/base.rb +62 -49
  24. data/lib/aspera/cli/plugins/config.rb +85 -96
  25. data/lib/aspera/cli/plugins/console.rb +15 -9
  26. data/lib/aspera/cli/plugins/cos.rb +1 -1
  27. data/lib/aspera/cli/plugins/faspex.rb +34 -27
  28. data/lib/aspera/cli/plugins/faspex5.rb +47 -44
  29. data/lib/aspera/cli/plugins/faspio.rb +7 -6
  30. data/lib/aspera/cli/plugins/httpgw.rb +3 -2
  31. data/lib/aspera/cli/plugins/node.rb +132 -120
  32. data/lib/aspera/cli/plugins/oauth.rb +1 -1
  33. data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
  34. data/lib/aspera/cli/plugins/preview.rb +26 -46
  35. data/lib/aspera/cli/plugins/server.rb +9 -10
  36. data/lib/aspera/cli/plugins/shares.rb +77 -43
  37. data/lib/aspera/cli/sync_actions.rb +49 -38
  38. data/lib/aspera/cli/transfer_agent.rb +16 -34
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/cli/wizard.rb +8 -5
  41. data/lib/aspera/command_line_builder.rb +20 -17
  42. data/lib/aspera/coverage.rb +6 -2
  43. data/lib/aspera/environment.rb +71 -84
  44. data/lib/aspera/faspex_gw.rb +1 -1
  45. data/lib/aspera/faspex_postproc.rb +1 -1
  46. data/lib/aspera/keychain/factory.rb +1 -2
  47. data/lib/aspera/keychain/macos_security.rb +2 -2
  48. data/lib/aspera/log.rb +2 -1
  49. data/lib/aspera/markdown.rb +31 -0
  50. data/lib/aspera/nagios.rb +6 -5
  51. data/lib/aspera/oauth/base.rb +17 -27
  52. data/lib/aspera/oauth/factory.rb +1 -1
  53. data/lib/aspera/oauth/url_json.rb +2 -1
  54. data/lib/aspera/preview/file_types.rb +23 -37
  55. data/lib/aspera/preview/terminal.rb +95 -29
  56. data/lib/aspera/preview/utils.rb +6 -5
  57. data/lib/aspera/products/connect.rb +3 -3
  58. data/lib/aspera/rest.rb +51 -39
  59. data/lib/aspera/rest_error_analyzer.rb +4 -4
  60. data/lib/aspera/ssh.rb +5 -2
  61. data/lib/aspera/ssl.rb +41 -0
  62. data/lib/aspera/sync/conf.schema.yaml +182 -34
  63. data/lib/aspera/sync/database.rb +2 -1
  64. data/lib/aspera/sync/operations.rb +128 -72
  65. data/lib/aspera/transfer/parameters.rb +3 -4
  66. data/lib/aspera/transfer/spec.rb +2 -3
  67. data/lib/aspera/transfer/spec.schema.yaml +49 -19
  68. data/lib/aspera/transfer/spec_doc.rb +14 -14
  69. data/lib/aspera/uri_reader.rb +1 -1
  70. data/lib/transferd_pb.rb +2 -2
  71. data.tar.gz.sig +0 -0
  72. metadata +33 -6
  73. metadata.gz.sig +0 -0
@@ -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)
@@ -62,6 +62,7 @@ module Aspera
62
62
  'Aspera on Cloud'
63
63
  end
64
64
 
65
+ # @return [Hash,NilClass]
65
66
  def detect(base_url)
66
67
  # no protocol ?
67
68
  base_url = "https://#{base_url}" unless base_url.match?(%r{^[a-z]{1,6}://})
@@ -69,9 +70,9 @@ module Aspera
69
70
  base_url = "#{base_url}.#{Api::AoC::SAAS_DOMAIN_PROD}" unless base_url.include?('.')
70
71
  # AoC is only https
71
72
  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'])
73
+ location = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', exception: false, ret: :resp)['Location']
74
+ return if location.nil?
75
+ redirect_uri = URI.parse(location)
75
76
  od = Api::AoC.split_org_domain(URI.parse(base_url))
76
77
  return unless redirect_uri.path.end_with?("oauth2/#{od[:organization]}/login")
77
78
  # either in standard domain, or product name in page
@@ -81,7 +82,8 @@ module Aspera
81
82
  }
82
83
  end
83
84
 
84
- # @param base [String] Base folder path
85
+ # @param base [String] Base folder path
86
+ # @param always [Bool] `true` always add number, `false` only if base folder already exists
85
87
  # @return [String] Folder path that does jot exist, with possible .<number> extension
86
88
  def next_available_folder(base, always: false)
87
89
  counter = always ? 1 : 0
@@ -95,27 +97,26 @@ module Aspera
95
97
  # Get folder path that does not exist
96
98
  # If it exists, an extension is added
97
99
  # or a sequential number if extension == :seq
98
- # @param folder [String] base folder
99
- def unique_folder(folder, extension: nil, always: false)
100
- case extension
101
- when nil
102
- folder
103
- when :seq
104
- # reuse helper
105
- next_available_folder(folder, always: always)
106
- else
107
- if Dir.exist?(folder) || always
108
- # NOTE: it might already exist
109
- "#{folder}.#{Environment.instance.sanitized_filename(extension)}"
110
- else
111
- folder
112
- end
100
+ # @param package_info [Hash] Package information
101
+ # @param destination_folder [String] Base folder
102
+ # @param fld. [Array] List of fields of package
103
+ def unique_folder(package_info, destination_folder, fld: nil, seq: false, opt: false)
104
+ Aspera.assert_array_all(fld, String, type: Cli::BadArgument){'fld'}
105
+ Aspera.assert([1, 2].include?(fld.length)){'fld must have 1 or 2 elements'}
106
+ folder = Environment.instance.sanitized_filename(package_info[fld[0]])
107
+ if seq
108
+ folder = next_available_folder(folder, always: !opt)
109
+ elsif fld[1] && (Dir.exist?(folder) || !opt)
110
+ # NOTE: it might already exist
111
+ folder = "#{folder}.#{Environment.instance.sanitized_filename(fld[1])}"
113
112
  end
113
+ puts("sub= #{folder}")
114
+ File.join(destination_folder, folder)
114
115
  end
115
116
  end
116
117
 
117
118
  # @param wizard [Wizard] The wizard object
118
- # @param app_url [Wizard] The wizard object
119
+ # @param app_url [String] Tested URL
119
120
  # @return [Hash] :preset_value, :test_args
120
121
  def wizard(wizard, app_url)
121
122
  pub_link_info = Api::AoC.link_info(app_url)
@@ -132,7 +133,7 @@ module Aspera
132
133
  test_args: 'organization'
133
134
  }
134
135
  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))
136
+ 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
137
  options.parse_options!
137
138
  # make username mandatory for jwt, this triggers interactive input
138
139
  wiz_username = options.get_option(:username, mandatory: true)
@@ -205,10 +206,10 @@ module Aspera
205
206
  @cache_workspace_info = nil
206
207
  @cache_home_node_file = nil
207
208
  @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])
209
+ options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
210
+ options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
211
+ options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
212
+ options.declare(:package_folder, 'Handling of reception of packages in folders', allowed: Hash, default: {})
212
213
  options.parse_options!
213
214
  # add node plugin options (for manual)
214
215
  Node.declare_options(options)
@@ -245,7 +246,7 @@ module Aspera
245
246
 
246
247
  # Generate or update Hash with workspace id and name (option), if not already set
247
248
  # @param hash [Hash, Nil] set in provided hash
248
- # @param string [Bool] true to set key as string, else as symbol
249
+ # @param string [TrueClass,FalseClass] true to set key as string, else as symbol
249
250
  # @param name [Bool] include name
250
251
  # @return [Hash] with key `workspace_[id,name]` (symbol or string) only if defined
251
252
  def workspace_id_hash(hash: nil, string: false, name: false)
@@ -288,7 +289,7 @@ module Aspera
288
289
  query = query_read_delete(default: default_query)
289
290
  # caller may add specific modifications or checks to query
290
291
  yield(query) if block_given?
291
- result = aoc_api.read_with_paging(resource_class_path, query: base_query.merge(query).compact, formatter: formatter)
292
+ result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact, formatter: formatter)
292
293
  return Main.result_object_list(result[:items], fields: fields, total: result[:total])
293
294
  end
294
295
 
@@ -313,7 +314,7 @@ module Aspera
313
314
  Aspera.assert_type(query, Hash){'query'}
314
315
  PACKAGE_RECEIVED_BASE_QUERY.each{ |k, v| query[k] = v unless query.key?(k)}
315
316
  resolve_dropbox_name_default_ws_id(query)
316
- return aoc_api.read_with_paging('packages', query: query.compact, formatter: formatter)
317
+ return aoc_api.read_with_paging('packages', query.compact, formatter: formatter)
317
318
  end
318
319
 
319
320
  NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
@@ -374,6 +375,7 @@ module Aspera
374
375
  Aspera.error_unreachable_line
375
376
  end
376
377
 
378
+ # @param resource_type [Symbol] One of ADMIN_OBJECTS
377
379
  def execute_resource_action(resource_type)
378
380
  # get path on API, resource type is singular, but api is plural
379
381
  resource_class_path =
@@ -391,8 +393,9 @@ module Aspera
391
393
  global_operations = %i[create list]
392
394
  supported_operations = %i[show modify]
393
395
  supported_operations.push(:delete, *global_operations) unless singleton_object
394
- supported_operations.push(:do) if resource_type.eql?(:node)
396
+ supported_operations.push(:do, :bearer_token) if resource_type.eql?(:node)
395
397
  supported_operations.push(:set_pub_key) if resource_type.eql?(:client)
398
+ supported_operations.push(:shared_folder, :dropbox) if resource_type.eql?(:workspace)
396
399
  command = options.get_next_command(supported_operations)
397
400
  # require identifier for non global commands
398
401
  if !singleton_object && !global_operations.include?(command)
@@ -454,6 +457,60 @@ module Aspera
454
457
  when :do
455
458
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
456
459
  return execute_nodegen4_command(command_repo, res_id)
460
+ when :bearer_token
461
+ node_api = aoc_api.node_api_from(
462
+ node_id: res_id,
463
+ scope: options.get_next_argument('scope')
464
+ )
465
+ return Main.result_text(node_api.oauth.authorization)
466
+ when :dropbox
467
+ command_shared = options.get_next_command(%i[list])
468
+ case command_shared
469
+ when :list
470
+ query = options.get_option(:query) || {}
471
+ res_data = aoc_api.read('dropboxes', query.merge({'workspace_id'=>res_id}))
472
+ return Main.result_object_list(res_data, fields: %w[id name description])
473
+ end
474
+ when :shared_folder
475
+ query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
476
+ shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
477
+ # inside a workspace
478
+ command_shared = options.get_next_command(%i[list member])
479
+ case command_shared
480
+ when :list
481
+ return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
482
+ when :member
483
+ shared_folder_id = instance_identifier
484
+ shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
485
+ Aspera.assert(shared_folder)
486
+ command_shared_member = options.get_next_command(%i[list])
487
+ case command_shared_member
488
+ when :list
489
+ node_api = aoc_api.node_api_from(
490
+ node_id: shared_folder['node_id'],
491
+ workspace_id: res_id,
492
+ workspace_name: nil,
493
+ scope: Api::Node::SCOPE_USER
494
+ )
495
+ result = node_api.read(
496
+ 'permissions',
497
+ {'file_id' => shared_folder['file_id'], 'tag' => "aspera.files.workspace.id=#{res_id}"}
498
+ )
499
+ result.each do |item|
500
+ item['member'] = begin
501
+ if Api::AoC.workspace_access?(item)
502
+ {'name'=>'[Internal permission]'}
503
+ else
504
+ aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name': 'not found'}
505
+ end
506
+ rescue => e
507
+ {'name'=>e.to_s}
508
+ end
509
+ end
510
+ # TODO : read users and group name and add, if query "include_members"
511
+ 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])
512
+ end
513
+ end
457
514
  else Aspera.error_unexpected_value(command)
458
515
  end
459
516
  end
@@ -736,7 +793,7 @@ module Aspera
736
793
  }
737
794
  return result_list('short_links', fields: Formatter.all_but('data'), base_query: list_params) if command.eql?(:list)
738
795
  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)}
796
+ found = aoc_api.read_with_paging('short_links', list_params, formatter: formatter)[:items].find{ |item| item['id'].eql?(one_id)}
740
797
  raise Cli::BadIdentifier.new('Short link', one_id) if found.nil?
741
798
  return Main.result_single_object(found, fields: Formatter.all_but('data'))
742
799
  when :modify
@@ -886,7 +943,7 @@ module Aspera
886
943
  # enforce workspace id from link (should be already ok, but in case user wanted to override)
887
944
  package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
888
945
  end
889
- package_data['encryption_at_rest'] = true if transfer.option_transfer_spec['content_protection'].eql?('encrypt')
946
+ package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
890
947
  # transfer may raise an error
891
948
  created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
892
949
  Main.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
@@ -921,13 +978,8 @@ module Aspera
921
978
  end
922
979
  # download all files, or specified list only
923
980
  ts_paths = transfer.ts_source_paths(default: ['.'])
924
- per_package_def = options.get_option(:package_folder)
925
- unless per_package_def.nil?
926
- raise Cli::BadArgument, "Invalid package folder option : #{per_package_def}" unless per_package_def =~ /\A([^+]+)(?:\+([^?]+)(\?)?)?\z/
927
- per_package_field1 = Regexp.last_match(1)
928
- per_package_field2 = Regexp.last_match(2)
929
- per_package_sub_always = Regexp.last_match(3).nil?
930
- end
981
+ per_package_def = options.get_option(:package_folder).symbolize_keys
982
+ save_metadata = per_package_def.delete(:inf)
931
983
  # get value outside of loop
932
984
  destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
933
985
  result_transfer = []
@@ -943,23 +995,14 @@ module Aspera
943
995
  Transfer::Spec::DIRECTION_RECEIVE,
944
996
  {'paths'=> ts_paths}
945
997
  )
946
- unless per_package_def.nil?
947
- # folder based on first field
948
- folder = File.join(
949
- destination_folder,
950
- Environment.instance.sanitized_filename(package_info[per_package_field1])
951
- )
952
- transfer.option_transfer_spec['destination_root'] = self.class.unique_folder(
953
- folder,
954
- extension: per_package_field2.eql?('seq') ? :seq : package_info[per_package_field2],
955
- always: per_package_sub_always
956
- )
957
- end
958
- formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{destination_folder}]})
998
+ transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
999
+ dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1000
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
959
1001
  statuses = transfer.start(
960
1002
  transfer_spec,
961
1003
  rest_token: package_node_api
962
1004
  )
1005
+ File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
963
1006
  result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
964
1007
  # update skip list only if all transfer sessions completed
965
1008
  if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
@@ -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
 
@@ -41,10 +48,15 @@ module Aspera
41
48
  # Global objects
42
49
  attr_reader :context
43
50
 
51
+ # @return [Manager]
44
52
  def options; @context.options; end
53
+ # @return [TransferAgent]
45
54
  def transfer; @context.transfer; end
55
+ # @return [Config]
46
56
  def config; @context.config; end
57
+ # @return [Formatter]
47
58
  def formatter; @context.formatter; end
59
+ # @return [PersistencyFolder]
48
60
  def persistency; @context.persistency; end
49
61
 
50
62
  def add_manual_header(has_options = true)
@@ -55,26 +67,17 @@ module Aspera
55
67
  options.parser.separator('OPTIONS:') if has_options
56
68
  end
57
69
 
58
- # Must be called AFTER the instance action:
59
- # ... folder browse _call_instance_identifier
70
+ # Resource identifier as positional parameter
60
71
  #
61
72
  # @param description [String] description of the identifier
62
- # @param as_option [Symbol] option name to use if identifier is an option
63
73
  # @param block [Proc] block to search for identifier based on attribute value
64
74
  # @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
75
+ def instance_identifier(description: 'identifier', &block)
76
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
71
77
  # 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
78
+ if res_id.is_a?(String) && (m = Base.percent_selector(res_id))
79
+ Aspera.assert(block, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
80
+ res_id = yield(m[:field], m[:value])
78
81
  end
79
82
  return res_id
80
83
  end
@@ -172,12 +175,11 @@ module Aspera
172
175
  if !delete_style.nil?
173
176
  one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
174
177
  Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
175
- api.call(
176
- operation: 'DELETE',
177
- subpath: entity,
178
+ api.delete(
179
+ entity,
180
+ nil,
178
181
  content_type: Rest::MIME_JSON,
179
- body: {delete_style => one_res_id},
180
- headers: {'Accept' => Rest::MIME_JSON}
182
+ body: {delete_style => one_res_id}
181
183
  )
182
184
  return Main.result_status('deleted')
183
185
  end
@@ -192,11 +194,10 @@ module Aspera
192
194
  data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
193
195
  return Main.result_object_list(data, total: total, fields: display_fields)
194
196
  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]
197
+ data, http = api.read(entity, query_read_delete, ret: :both)
198
+ return Main.result_empty if http.code == '204'
198
199
  # TODO: not generic : which application is this for ?
199
- if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
200
+ if http['Content-Type'].start_with?('application/vnd.api+json')
200
201
  Log.log.debug('is vnd.api')
201
202
  data = data[entity]
202
203
  end
@@ -223,9 +224,8 @@ module Aspera
223
224
 
224
225
  # Query parameters in URL suitable for REST: list/GET and delete/DELETE
225
226
  def query_read_delete(default: nil)
226
- query = options.get_option(:query)
227
227
  # Dup default, as it could be frozen
228
- query = default.dup if query.nil?
228
+ query = options.get_option(:query) || default.dup
229
229
  Log.log.debug{"query_read_delete=#{query}".bg_red}
230
230
  begin
231
231
  # Check it is suitable
@@ -249,7 +249,7 @@ module Aspera
249
249
  value = default if value.nil?
250
250
  unless type.nil?
251
251
  type = [type] unless type.is_a?(Array)
252
- Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
252
+ Aspera.assert_array_all(type, Class){'check types'}
253
253
  if bulk
254
254
  Aspera.assert_type(value, Array, type: Cli::BadArgument)
255
255
  value.each do |v|
@@ -263,10 +263,10 @@ module Aspera
263
263
  end
264
264
 
265
265
  # 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
266
+ # @param api [Rest] API object
267
+ # @param entity [String,Symbol] API endpoint of entity to list
268
+ # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
269
+ # @param query [Hash,nil] Additional query parameters
270
270
  # @return [Array] items, total_count
271
271
  def list_entities_limit_offset_total_count(
272
272
  api:,
@@ -309,26 +309,39 @@ module Aspera
309
309
  return result, total_count
310
310
  end
311
311
 
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
312
+ # Lookup an entity id from its name.
313
+ # Uses query `q` if `query` is `:default` and `field` is `name`.
314
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
315
+ # @param value [String] Value to lookup
316
+ # @param field [String] Field to match, by default it is `'name'`
317
+ # @param items_key [String] Key in the result to get the list of items (override entity)
318
+ # @param query [Hash] Additional query parameters (Default: `:default`)
318
319
  def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
319
320
  if query.eql?(:default)
320
321
  Aspera.assert(field.eql?('name')){'Default query is on name only'}
321
322
  query = {'q'=> value}
322
323
  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
324
+ 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}
325
+ end
326
+
327
+ # Lookup entity by field and value. Extract single result from list of result returned by block.
328
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
329
+ # @param value [String] Value to lookup
330
+ # @param field [String] Field to match, by default it is `'name'`
331
+ # @param block [Proc] Get list of entity matching query.
332
+ def lookup_entity_generic(entity:, value:, field: 'name', &block)
333
+ Aspera.assert(block_given?)
334
+ found = yield
335
+ Aspera.assert_array_all(found, Hash)
336
+ found = found.select{ |i| i[field].eql?(value)}
337
+ return found.first if found.length.eql?(1)
338
+ raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
329
339
  end
340
+
330
341
  PER_PAGE_DEFAULT = 1000
331
- private_constant :PER_PAGE_DEFAULT
342
+ # Percent selector: select by this field for this value
343
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
344
+ private_constant :PER_PAGE_DEFAULT, :REGEX_LOOKUP_ID_BY_FIELD
332
345
  end
333
346
  end
334
347
  end