aspera-cli 4.24.1 → 4.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +15 -2
  4. data/README.md +745 -436
  5. data/bin/ascli +20 -1
  6. data/bin/asession +23 -27
  7. data/lib/aspera/agent/base.rb +10 -21
  8. data/lib/aspera/agent/connect.rb +2 -3
  9. data/lib/aspera/agent/desktop.rb +2 -2
  10. data/lib/aspera/agent/direct.rb +49 -32
  11. data/lib/aspera/agent/factory.rb +31 -0
  12. data/lib/aspera/api/aoc.rb +79 -49
  13. data/lib/aspera/api/faspex.rb +212 -0
  14. data/lib/aspera/api/node.rb +99 -84
  15. data/lib/aspera/ascp/installation.rb +22 -21
  16. data/lib/aspera/ascp/management.rb +119 -23
  17. data/lib/aspera/assert.rb +14 -8
  18. data/lib/aspera/cli/extended_value.rb +15 -15
  19. data/lib/aspera/cli/formatter.rb +7 -5
  20. data/lib/aspera/cli/hints.rb +8 -0
  21. data/lib/aspera/cli/info.rb +4 -4
  22. data/lib/aspera/cli/main.rb +55 -70
  23. data/lib/aspera/cli/manager.rb +7 -4
  24. data/lib/aspera/cli/plugins/alee.rb +2 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +110 -186
  26. data/lib/aspera/cli/plugins/ats.rb +4 -4
  27. data/lib/aspera/cli/plugins/base.rb +335 -0
  28. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  29. data/lib/aspera/cli/plugins/config.rb +249 -220
  30. data/lib/aspera/cli/plugins/console.rb +15 -15
  31. data/lib/aspera/cli/plugins/cos.rb +2 -2
  32. data/lib/aspera/cli/plugins/factory.rb +78 -0
  33. data/lib/aspera/cli/plugins/faspex.rb +17 -20
  34. data/lib/aspera/cli/plugins/faspex5.rb +79 -193
  35. data/lib/aspera/cli/plugins/faspio.rb +14 -13
  36. data/lib/aspera/cli/plugins/httpgw.rb +13 -12
  37. data/lib/aspera/cli/plugins/node.rb +34 -32
  38. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  39. data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
  40. data/lib/aspera/cli/plugins/preview.rb +4 -4
  41. data/lib/aspera/cli/plugins/server.rb +15 -13
  42. data/lib/aspera/cli/plugins/shares.rb +18 -15
  43. data/lib/aspera/cli/sync_actions.rb +1 -1
  44. data/lib/aspera/cli/transfer_agent.rb +24 -20
  45. data/lib/aspera/cli/transfer_progress.rb +6 -6
  46. data/lib/aspera/cli/version.rb +3 -3
  47. data/lib/aspera/cli/wizard.rb +65 -53
  48. data/lib/aspera/colors.rb +6 -0
  49. data/lib/aspera/command_line_builder.rb +45 -50
  50. data/lib/aspera/command_line_converter.rb +2 -1
  51. data/lib/aspera/coverage.rb +1 -1
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +10 -7
  54. data/lib/aspera/faspex_gw.rb +6 -4
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/keychain/macos_security.rb +1 -1
  57. data/lib/aspera/log.rb +37 -9
  58. data/lib/aspera/nagios.rb +1 -1
  59. data/lib/aspera/oauth/base.rb +17 -10
  60. data/lib/aspera/oauth/factory.rb +8 -8
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/products/connect.rb +4 -3
  63. data/lib/aspera/products/desktop.rb +1 -4
  64. data/lib/aspera/products/other.rb +9 -1
  65. data/lib/aspera/products/transferd.rb +0 -1
  66. data/lib/aspera/rest.rb +126 -83
  67. data/lib/aspera/ssh.rb +3 -3
  68. data/lib/aspera/sync/args.schema.yaml +46 -3
  69. data/lib/aspera/sync/conf.schema.yaml +130 -94
  70. data/lib/aspera/sync/operations.rb +16 -16
  71. data/lib/aspera/temp_file_manager.rb +17 -5
  72. data/lib/aspera/transfer/error.rb +16 -7
  73. data/lib/aspera/transfer/parameters.rb +34 -20
  74. data/lib/aspera/transfer/resumer.rb +74 -0
  75. data/lib/aspera/transfer/spec.rb +4 -3
  76. data/lib/aspera/transfer/spec.schema.yaml +132 -51
  77. data/lib/aspera/transfer/spec_doc.rb +41 -35
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +6 -6
  80. data.tar.gz.sig +0 -0
  81. metadata +9 -7
  82. metadata.gz.sig +0 -0
  83. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  84. data/lib/aspera/cli/plugin.rb +0 -333
  85. data/lib/aspera/cli/plugin_factory.rb +0 -81
  86. data/lib/aspera/resumer.rb +0 -77
  87. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/agent/base'
3
+ require 'aspera/agent/factory'
4
4
 
5
5
  module Aspera
6
6
  module Transfer
@@ -13,33 +13,38 @@ module Aspera
13
13
  end
14
14
 
15
15
  # @param formatter [Cli::Formatter] Formatter to use, methods: special_format, check_row
16
- # @param include_option [Boolean] true : include CLI options
16
+ # @param include_option [Boolean] `true` : include CLI options
17
+ # @param agent_columns [Boolean] `true` : include agents columns
18
+ # @param schema [Hash] The JSON spec
17
19
  # @return [Array] a table suitable to display in manual
18
- def man_table(formatter, include_option: false, agent_columns: true)
20
+ def man_table(formatter, include_option: false, agent_columns: true, schema: Spec::SCHEMA)
19
21
  col_local = agent_to_short(:direct)
20
- cols = agent_columns ? %i[name type] + AGENT_LIST.map(&:last) + %i[description] : %i[name type description]
21
- rows = Spec::SCHEMA['properties'].filter_map do |name, properties|
22
+ cols = %i[name type description]
23
+ cols.insert(-2, *AGENT_LIST.map(&:last)) if agent_columns
24
+ rows = []
25
+ schema['properties'].each do |name, info|
26
+ if info['type'].eql?('object') && info['properties']
27
+ rows.concat(man_table(formatter, include_option: include_option, agent_columns: agent_columns, schema: info).last.map { |h| h.merge(name: "#{name}.#{h[:name]}") })
28
+ end
22
29
  # manual table
23
30
  columns = {
24
31
  name: name,
25
- type: properties['type'],
32
+ type: info['type'],
26
33
  description: []
27
34
  }
28
- # replace "back solidus" HTML entity with its text value
29
- #
30
- # split lines
35
+ # replace "back solidus" HTML entity with its text value, highlight keywords, and split lines
31
36
  columns[:description] =
32
- properties['description']
37
+ info['description']
33
38
  .gsub('\', '\\')
34
39
  .gsub(/`([a-z0-9_.+-]+)`/){formatter.keyword_highlight(Regexp.last_match(1))}
35
- .split("\n") if properties.key?('description')
36
- columns[:description].unshift("DEPRECATED: #{properties['x-deprecation']}") if properties.key?('x-deprecation')
40
+ .split("\n") if info.key?('description')
41
+ columns[:description].unshift("DEPRECATED: #{info['x-deprecation']}") if info.key?('x-deprecation')
37
42
  # add flags for supported agents in doc
38
43
  agents = []
39
44
  AGENT_LIST.each do |agent_info|
40
- agents.push(agent_info.last) if properties['x-agents'].nil? || properties['x-agents'].include?(agent_info.first.to_s)
45
+ agents.push(agent_info.last) if info['x-agents'].nil? || info['x-agents'].include?(agent_info.first.to_s)
41
46
  end
42
- agents.push(col_local) if properties['x-cli-option'] && !agents.include?(col_local)
47
+ Aspera.assert(agents.include?(col_local)){"#{name}: x-cli-option requires agent direct (or nil)"} if info['x-cli-option']
43
48
  if agent_columns
44
49
  AGENT_LIST.each do |agent_info|
45
50
  columns[agent_info.last] = formatter.tick(agents.include?(agent_info.last))
@@ -49,30 +54,31 @@ module Aspera
49
54
  end
50
55
  # only keep lines that are usable in supported agents
51
56
  next false if agents.empty?
52
- columns[:description].push("Allowed values: #{properties['enum'].map{ |v| formatter.keyword_highlight(v)}.join(', ')}") if properties.key?('enum')
53
- envvar = ''
54
- cli_option =
55
- if properties.key?('x-cli-envvar')
56
- envvar = 'env:'
57
- properties['x-cli-envvar']
58
- elsif properties['x-cli-switch']
59
- properties['x-cli-option']
60
- elsif properties['x-cli-special']
61
- ''
62
- elsif properties['x-cli-option']
63
- arg_type = properties.key?('enum') ? '{enum}' : "{#{[properties['type']].flatten.join('|')}}"
64
- conversion_tag = properties.key?('x-cli-convert') ? '(conversion)' : ''
65
- sep = properties['x-cli-option'].start_with?('--') ? '=' : ' '
66
- "#{properties['x-cli-option']}#{sep}#{conversion_tag}#{arg_type}"
67
- end
68
- columns[:description].push("(#{envvar}#{formatter.keyword_highlight(cli_option)})") if include_option && !cli_option.to_s.empty?
69
- formatter.check_row(columns)
70
- end.sort_by{ |i| i[:name]}
71
- [cols, rows]
57
+ columns[:description].push("Allowed values: #{info['enum'].map{ |v| formatter.keyword_highlight(v)}.join(', ')}") if info.key?('enum')
58
+ if include_option
59
+ envvar_prefix = ''
60
+ cli_option =
61
+ if info.key?('x-cli-envvar')
62
+ envvar_prefix = 'env:'
63
+ info['x-cli-envvar']
64
+ elsif info['x-cli-switch']
65
+ info['x-cli-option']
66
+ elsif info['x-cli-option']
67
+ arg_type = info.key?('enum') ? '{enum}' : "{#{[info['type']].flatten.join('|')}}"
68
+ # conversion_tag = info['x-cli-convert']
69
+ conversion_tag = info.key?('x-cli-convert') ? 'conversion' : nil
70
+ sep = info['x-cli-option'].start_with?('--') ? '=' : ' '
71
+ "#{info['x-cli-option']}#{sep}#{"(#{conversion_tag})" if conversion_tag}#{arg_type}"
72
+ end
73
+ columns[:description].push("(#{'special:' if info['x-cli-special']}#{envvar_prefix}#{formatter.keyword_highlight(cli_option)})") if cli_option
74
+ end
75
+ rows.push(formatter.check_row(columns))
76
+ end
77
+ [cols, rows.sort_by{ |i| i[:name]}]
72
78
  end
73
79
  end
74
80
  # Agents shown in manual for parameters (sub list)
75
- AGENT_LIST = Agent::Base.agent_list.map do |agent_sym|
81
+ AGENT_LIST = Agent::Factory.instance.list.map do |agent_sym|
76
82
  [agent_sym, agent_sym.to_s.capitalize, agent_to_short(agent_sym)]
77
83
  end.sort_by(&:last).freeze
78
84
  end
@@ -37,7 +37,7 @@ module Aspera
37
37
  return File.expand_path(url[SCHEME_FILE_PFX2.length..-1])
38
38
  else
39
39
  # download to temp file
40
- # autodelete on exit
40
+ # auto-delete on exit
41
41
  sdk_archive_path = TempFileManager.instance.new_file_path_global(suffix: File.basename(url))
42
42
  Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
43
43
  return sdk_archive_path
@@ -17,7 +17,7 @@ module Aspera
17
17
  def service(request, response)
18
18
  Log.log.debug{"received request from browser #{request.request_method} #{request.path}"}
19
19
  Aspera.assert_values(request.request_method, ['GET'], type: WEBrick::HTTPStatus::MethodNotAllowed){'HTTP verb'}
20
- additionnal_info = @web_auth.signal_request(request)
20
+ additional_info = @web_auth.signal_request(request)
21
21
  response.status = 200
22
22
  response.content_type = 'text/html'
23
23
  response.body = <<~HTML
@@ -91,7 +91,7 @@ module Aspera
91
91
  <body>
92
92
  <h1>Thank You!</h1>
93
93
  <p>You can close this window.</p>
94
- <p>#{additionnal_info}</p>
94
+ <p>#{additional_info}</p>
95
95
 
96
96
  <!-- JavaScript to generate IBM logos -->
97
97
  <script>
@@ -170,15 +170,15 @@ module Aspera
170
170
  # store the final query
171
171
  class WebAuth < WebServerSimple
172
172
  # @param endpoint_url [String] e.g. 'https://127.0.0.1:12345'
173
- # @param additionnal_info [String] Information in web page
174
- def initialize(endpoint_url, additionnal_info = nil)
173
+ # @param additional_info [String] Information in web page
174
+ def initialize(endpoint_url, additional_info = nil)
175
175
  uri = URI.parse(endpoint_url)
176
176
  super(uri)
177
177
  @mutex = Mutex.new
178
178
  @cond = ConditionVariable.new
179
179
  @expected_path = uri.path.empty? ? '/' : uri.path
180
180
  @query = nil
181
- @additionnal_info = additionnal_info
181
+ @additional_info = additional_info
182
182
  # last argument (self) is provided to constructor of servlet
183
183
  mount(@expected_path, WebAuthServlet, self)
184
184
  # server runs in thread
@@ -194,7 +194,7 @@ module Aspera
194
194
  @query = request.query
195
195
  @cond.signal
196
196
  end
197
- return @additionnal_info
197
+ return @additional_info
198
198
  end
199
199
 
200
200
  # wait for request on web server (main thread)
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aspera-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.24.1
4
+ version: 4.24.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Laurent Martin
@@ -37,7 +37,7 @@ cert_chain:
37
37
  eTf9kxhVM40wGQOECVNA8UsEEZHD48eF+csUYZtAJOF5oxTI8UyV9T/o6CgO0c9/
38
38
  Gzz+Qm5ULOUcPiJLjSpaiTrkiIVYiDGnqNSr6R1Hb1c=
39
39
  -----END CERTIFICATE-----
40
- date: 2025-10-02 00:00:00.000000000 Z
40
+ date: 2025-10-23 00:00:00.000000000 Z
41
41
  dependencies:
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: blankslate
@@ -282,6 +282,7 @@ files:
282
282
  - lib/aspera/agent/connect.rb
283
283
  - lib/aspera/agent/desktop.rb
284
284
  - lib/aspera/agent/direct.rb
285
+ - lib/aspera/agent/factory.rb
285
286
  - lib/aspera/agent/httpgw.rb
286
287
  - lib/aspera/agent/node.rb
287
288
  - lib/aspera/agent/transferd.rb
@@ -289,13 +290,13 @@ files:
289
290
  - lib/aspera/api/aoc.rb
290
291
  - lib/aspera/api/ats.rb
291
292
  - lib/aspera/api/cos_node.rb
293
+ - lib/aspera/api/faspex.rb
292
294
  - lib/aspera/api/httpgw.rb
293
295
  - lib/aspera/api/node.rb
294
296
  - lib/aspera/ascmd.rb
295
297
  - lib/aspera/ascp/installation.rb
296
298
  - lib/aspera/ascp/management.rb
297
299
  - lib/aspera/assert.rb
298
- - lib/aspera/cli/basic_auth_plugin.rb
299
300
  - lib/aspera/cli/error.rb
300
301
  - lib/aspera/cli/extended_value.rb
301
302
  - lib/aspera/cli/formatter.rb
@@ -303,19 +304,21 @@ files:
303
304
  - lib/aspera/cli/info.rb
304
305
  - lib/aspera/cli/main.rb
305
306
  - lib/aspera/cli/manager.rb
306
- - lib/aspera/cli/plugin.rb
307
- - lib/aspera/cli/plugin_factory.rb
308
307
  - lib/aspera/cli/plugins/alee.rb
309
308
  - lib/aspera/cli/plugins/aoc.rb
310
309
  - lib/aspera/cli/plugins/ats.rb
310
+ - lib/aspera/cli/plugins/base.rb
311
+ - lib/aspera/cli/plugins/basic_auth.rb
311
312
  - lib/aspera/cli/plugins/config.rb
312
313
  - lib/aspera/cli/plugins/console.rb
313
314
  - lib/aspera/cli/plugins/cos.rb
315
+ - lib/aspera/cli/plugins/factory.rb
314
316
  - lib/aspera/cli/plugins/faspex.rb
315
317
  - lib/aspera/cli/plugins/faspex5.rb
316
318
  - lib/aspera/cli/plugins/faspio.rb
317
319
  - lib/aspera/cli/plugins/httpgw.rb
318
320
  - lib/aspera/cli/plugins/node.rb
321
+ - lib/aspera/cli/plugins/oauth.rb
319
322
  - lib/aspera/cli/plugins/orchestrator.rb
320
323
  - lib/aspera/cli/plugins/preview.rb
321
324
  - lib/aspera/cli/plugins/server.rb
@@ -378,7 +381,6 @@ files:
378
381
  - lib/aspera/rest_call_error.rb
379
382
  - lib/aspera/rest_error_analyzer.rb
380
383
  - lib/aspera/rest_errors_aspera.rb
381
- - lib/aspera/resumer.rb
382
384
  - lib/aspera/secret_hider.rb
383
385
  - lib/aspera/ssh.rb
384
386
  - lib/aspera/sync/args.schema.yaml
@@ -388,9 +390,9 @@ files:
388
390
  - lib/aspera/temp_file_manager.rb
389
391
  - lib/aspera/timer_limiter.rb
390
392
  - lib/aspera/transfer/error.rb
391
- - lib/aspera/transfer/error_info.rb
392
393
  - lib/aspera/transfer/faux_file.rb
393
394
  - lib/aspera/transfer/parameters.rb
395
+ - lib/aspera/transfer/resumer.rb
394
396
  - lib/aspera/transfer/spec.rb
395
397
  - lib/aspera/transfer/spec.schema.yaml
396
398
  - lib/aspera/transfer/spec_doc.rb
metadata.gz.sig CHANGED
Binary file
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'aspera/rest'
4
- require 'aspera/cli/plugin'
5
-
6
- module Aspera
7
- module Cli
8
- # base class for applications supporting basic authentication
9
- class BasicAuthPlugin < Cli::Plugin
10
- class << self
11
- def declare_options(options)
12
- options.declare(:url, 'URL of application, e.g. https://app.example.com/aspera/app')
13
- options.declare(:username, "User's identifier")
14
- options.declare(:password, "User's password")
15
- options.parse_options!
16
- end
17
- end
18
-
19
- def initialize(context:, basic_options: true)
20
- super(context: context)
21
- BasicAuthPlugin.declare_options(options) if basic_options
22
- end
23
-
24
- # returns a Rest object with basic auth
25
- def basic_auth_params(subpath = nil)
26
- api_url = options.get_option(:url, mandatory: true)
27
- api_url = "#{api_url}/#{subpath}" unless subpath.nil?
28
- return {
29
- base_url: api_url,
30
- auth: {
31
- type: :basic,
32
- username: options.get_option(:username, mandatory: true),
33
- password: options.get_option(:password, mandatory: true)
34
- }
35
- }
36
- end
37
-
38
- def basic_auth_api(subpath = nil)
39
- return Rest.new(**basic_auth_params(subpath))
40
- end
41
- end
42
- end
43
- end
@@ -1,333 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'aspera/cli/extended_value'
4
- require 'aspera/assert'
5
-
6
- module Aspera
7
- module Cli
8
- # Base class for plugins
9
- class Plugin
10
- # operations without id
11
- GLOBAL_OPS = %i[create list].freeze
12
- # operations with id
13
- INSTANCE_OPS = %i[modify delete show].freeze
14
- # all standard operations
15
- ALL_OPS = (GLOBAL_OPS + INSTANCE_OPS).freeze
16
- # special query parameter: max number of items for list command
17
- MAX_ITEMS = 'max'
18
- # special query parameter: max number of pages for list command
19
- MAX_PAGES = 'pmax'
20
- # special identifier format: look for this name to find where supported
21
- REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
22
- PER_PAGE_DEFAULT = 1000
23
- private_constant :PER_PAGE_DEFAULT
24
-
25
- class << self
26
- def declare_generic_options(options)
27
- options.declare(:query, 'Additional filter for for some commands (list/delete)', types: [Hash, Array])
28
- options.declare(:property, 'Name of property to set (modify operation)')
29
- options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
30
- options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
31
- end
32
- end
33
-
34
- def initialize(context:)
35
- # check presence in descendant of mandatory method and constant
36
- Aspera.assert(respond_to?(:execute_action), type: InternalError){"Missing method 'execute_action' in #{self.class}"}
37
- Aspera.assert(self.class.constants.include?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
38
- @context = context
39
- add_manual_header if @context.man_header
40
- end
41
-
42
- # Global objects
43
- attr_reader :context
44
-
45
- def options; @context.options; end
46
- def transfer; @context.transfer; end
47
- def config; @context.config; end
48
- def formatter; @context.formatter; end
49
- def persistency; @context.persistency; end
50
-
51
- def add_manual_header(has_options = true)
52
- # manual header for all plugins
53
- options.parser.separator('')
54
- options.parser.separator("COMMAND: #{self.class.name.split('::').last.downcase}")
55
- options.parser.separator("SUBCOMMANDS: #{self.class.const_get(:ACTIONS).map(&:to_s).sort.join(' ')}")
56
- options.parser.separator('OPTIONS:') if has_options
57
- end
58
-
59
- # Must be called AFTER the instance action:
60
- # ... folder browse _call_instance_identifier
61
- #
62
- # @param description [String] description of the identifier
63
- # @param as_option [Symbol] option name to use if identifier is an option
64
- # @param block [Proc] block to search for identifier based on attribute value
65
- # @return [String, Array] identifier or list of ids
66
- def instance_identifier(description: 'identifier', as_option: nil, &block)
67
- if as_option.nil?
68
- res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
69
- else
70
- res_id = options.get_option(as_option)
71
- end
72
- # can be an Array
73
- if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
74
- if block
75
- res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
76
- else
77
- raise Cli::BadArgument, "Percent syntax for #{description} not supported in this context"
78
- end
79
- end
80
- return res_id
81
- end
82
-
83
- # For create and delete operations: execute one actin or multiple if bulk is yes
84
- # @param command [Symbol] operation: :create, :delete, ...
85
- # @param descr [String] description of the value
86
- # @param values [Object] the value(s), or the type of value to get from user
87
- # @param id_result [String] key in result hash to use as identifier
88
- # @param fields [Array] fields to display
89
- # @param &block [Proc] block to execute for each value
90
- def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default)
91
- Aspera.assert(block_given?){'missing block'}
92
- is_bulk = options.get_option(:bulk)
93
- case values
94
- when :identifier
95
- values = instance_identifier(description: descr)
96
- when Class
97
- values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk)
98
- end
99
- # if not bulk, there is a single value
100
- params = is_bulk ? values : [values]
101
- Log.log.warn('Empty list given for bulk operation') if params.empty?
102
- Log.dump(:bulk_operation, params)
103
- result_list = []
104
- params.each do |param|
105
- # init for delete
106
- result = {id_result => param}
107
- begin
108
- # execute custom code
109
- res = yield(param)
110
- # if block returns a hash, let's use this (create)
111
- result = res if res.is_a?(Hash)
112
- # TODO: remove when faspio gw api fixes this
113
- result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
114
- # create -> created
115
- result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
116
- rescue StandardError => e
117
- raise e if options.get_option(:bfail)
118
- result['status'] = e.to_s
119
- end
120
- result_list.push(result)
121
- end
122
- display_fields = [id_result, 'status']
123
- if is_bulk
124
- return Main.result_object_list(result_list, fields: display_fields)
125
- else
126
- display_fields = fields unless fields.eql?(:default)
127
- return Main.result_single_object(result_list.first, fields: display_fields)
128
- end
129
- end
130
-
131
- # Operations: Create, Delete, Show, List, Modify
132
- # @param api [Rest] api to use
133
- # @param entity [String] sub path in URL to resource relative to base url
134
- # @param command [Symbol] command to execute: create show list modify delete
135
- # @param display_fields [Array] fields to display by default
136
- # @param items_key [String] result is in a sub key of the json
137
- # @param delete_style [String] if set, the delete operation by array in payload
138
- # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
139
- # @param is_singleton [Boolean] if true, entity is the full path to the resource
140
- # @param tclo [Bool] if set, :list use paging with total_count, limit, offset
141
- # @param block [Proc] block to search for identifier based on attribute value
142
- # @return result suitable for CLI result
143
- def entity_execute(
144
- api:,
145
- entity:,
146
- command: nil,
147
- display_fields: nil,
148
- items_key: nil,
149
- delete_style: nil,
150
- id_as_arg: false,
151
- is_singleton: false,
152
- list_query: nil,
153
- tclo: false,
154
- &block
155
- )
156
- command = options.get_next_command(ALL_OPS) if command.nil?
157
- if is_singleton
158
- one_res_path = entity
159
- elsif INSTANCE_OPS.include?(command)
160
- one_res_id = instance_identifier(&block)
161
- one_res_path = "#{entity}/#{one_res_id}"
162
- one_res_path = "#{entity}?#{id_as_arg}=#{one_res_id}" if id_as_arg
163
- end
164
-
165
- case command
166
- when :create
167
- raise BadArgument, 'cannot create singleton' if is_singleton
168
- return do_bulk_operation(command: command, descr: 'data', fields: display_fields) do |params|
169
- api.create(entity, params)
170
- end
171
- when :delete
172
- raise BadArgument, 'cannot delete singleton' if is_singleton
173
- if !delete_style.nil?
174
- one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
175
- Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
176
- api.call(
177
- operation: 'DELETE',
178
- subpath: entity,
179
- content_type: Rest::MIME_JSON,
180
- body: {delete_style => one_res_id},
181
- headers: {'Accept' => Rest::MIME_JSON}
182
- )
183
- return Main.result_status('deleted')
184
- end
185
- return do_bulk_operation(command: command, values: one_res_id) do |one_id|
186
- api.delete("#{entity}/#{one_id}", query_read_delete)
187
- {'id' => one_id}
188
- end
189
- when :show
190
- return Main.result_single_object(api.read(one_res_path), fields: display_fields)
191
- when :list
192
- if tclo
193
- data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: list_query)
194
- return Main.result_object_list(data, total: total, fields: display_fields)
195
- end
196
- resp = api.call(operation: 'GET', subpath: entity, headers: {'Accept' => Rest::MIME_JSON}, query: query_read_delete)
197
- return Main.result_empty if resp[:http].code == '204'
198
- data = resp[:data]
199
- # TODO: not generic : which application is this for ?
200
- if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
201
- Log.log.debug('is vnd.api')
202
- data = data[entity]
203
- end
204
- data = data[items_key] if items_key
205
- case data
206
- when Hash
207
- return Main.result_single_object(data, fields: display_fields)
208
- when Array
209
- return Main.result_object_list(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
210
- return Main.result_value_list(data)
211
- else
212
- raise "An error occurred: unexpected result type for list: #{data.class}"
213
- end
214
- when :modify
215
- parameters = value_create_modify(command: command)
216
- property = options.get_option(:property)
217
- parameters = {property => parameters} unless property.nil?
218
- api.update(one_res_path, parameters)
219
- return Main.result_status('modified')
220
- else
221
- raise "unknown action: #{command}"
222
- end
223
- end
224
-
225
- # Query parameters in URL suitable for REST: list/GET and delete/DELETE
226
- def query_read_delete(default: nil)
227
- query = options.get_option(:query)
228
- # dup default, as it could be frozen
229
- query = default.dup if query.nil?
230
- Log.log.debug{"query_read_delete=#{query}".bg_red}
231
- begin
232
- # check it is suitable
233
- URI.encode_www_form(query) unless query.nil?
234
- rescue StandardError => e
235
- raise Cli::BadArgument, "Query must be an extended value (Hash, Array) which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
236
- end
237
- return query
238
- end
239
-
240
- # Retrieves an extended value from command line, used for creation or modification of entities
241
- # @param command [Symbol] command name for error message
242
- # @param type [Class] expected type of value, either a Class, an Array of Class
243
- # @param bulk [Boolean] if true, value must be an Array of <type>
244
- # @param default [Object] default value if not provided
245
- def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
246
- value = options.get_next_argument(
247
- "parameters for #{command}#{" (#{description})" unless description.nil?}", mandatory: default.nil?,
248
- validation: bulk ? Array : type
249
- )
250
- value = default if value.nil?
251
- unless type.nil?
252
- type = [type] unless type.is_a?(Array)
253
- Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
254
- if bulk
255
- Aspera.assert_type(value, Array, type: Cli::BadArgument)
256
- value.each do |v|
257
- Aspera.assert_values(v.class, type, type: Cli::BadArgument)
258
- end
259
- else
260
- Aspera.assert_values(value.class, type, type: Cli::BadArgument)
261
- end
262
- end
263
- return value
264
- end
265
-
266
- # Get a (full or partial) list of all entities of a given type with query: offset/limit
267
- # @param `api` [Rest] the API object
268
- # @param `entity` [String,Symbol] the API endpoint of entity to list
269
- # @param `items_key` [String] key in the result to get the list of items
270
- # @param `query` [Hash,nil] additional query parameters
271
- # @return [Array] items, total_count
272
- def list_entities_limit_offset_total_count(
273
- api:,
274
- entity:,
275
- items_key: nil,
276
- query: nil
277
- )
278
- entity = entity.to_s if entity.is_a?(Symbol)
279
- items_key = entity.split('/').last if items_key.nil?
280
- query = {} if query.nil?
281
- Aspera.assert_type(entity, String)
282
- Aspera.assert_type(items_key, String)
283
- Aspera.assert_type(query, Hash)
284
- Log.log.debug{"list_entities t=#{entity} k=#{items_key} q=#{query}"}
285
- result = []
286
- offset = 0
287
- max_items = query.delete(MAX_ITEMS)
288
- remain_pages = query.delete(MAX_PAGES)
289
- # merge default parameters, by default 100 per page
290
- query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
291
- total_count = nil
292
- loop do
293
- query['offset'] = offset
294
- page_result = api.read(entity, query)
295
- Aspera.assert_type(page_result[items_key], Array)
296
- result.concat(page_result[items_key])
297
- # reach the limit set by user ?
298
- if !max_items.nil? && (result.length >= max_items)
299
- result = result.slice(0, max_items)
300
- break
301
- end
302
- total_count ||= page_result['total_count']
303
- break if result.length >= total_count
304
- remain_pages -= 1 unless remain_pages.nil?
305
- break if remain_pages == 0
306
- offset += page_result[items_key].length
307
- formatter.long_operation_running
308
- end
309
- formatter.long_operation_terminated
310
- return result, total_count
311
- end
312
-
313
- # Lookup an entity id from its name
314
- # @param entity [String] the type of entity to lookup, by default it is the path, and it is also the field name in result
315
- # @param value [String] the value to lookup
316
- # @param field [String] the 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
319
- def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
320
- if query.eql?(:default)
321
- Aspera.assert(field.eql?('name')){'Default query is on name only'}
322
- query = {'q'=> value}
323
- end
324
- found = list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first.select{ |i| i[field].eql?(value)}
325
- case found.length
326
- when 0 then raise "No #{entity} with #{field} = #{value}"
327
- when 1 then return found.first
328
- else raise "Found #{found.length} #{entity} with #{field} = #{value}"
329
- end
330
- end
331
- end
332
- end
333
- end