aspera-cli 4.25.4 → 4.25.6

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 (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -11
  4. data/CONTRIBUTING.md +41 -23
  5. data/README.md +9 -7
  6. data/lib/aspera/agent/direct.rb +10 -8
  7. data/lib/aspera/agent/factory.rb +3 -3
  8. data/lib/aspera/agent/node.rb +1 -1
  9. data/lib/aspera/api/alee.rb +1 -0
  10. data/lib/aspera/api/aoc.rb +10 -6
  11. data/lib/aspera/api/ats.rb +1 -1
  12. data/lib/aspera/api/cos_node.rb +1 -0
  13. data/lib/aspera/api/faspex.rb +17 -19
  14. data/lib/aspera/api/httpgw.rb +19 -3
  15. data/lib/aspera/api/node.rb +56 -2
  16. data/lib/aspera/ascp/installation.rb +32 -34
  17. data/lib/aspera/cli/error.rb +8 -0
  18. data/lib/aspera/cli/info.rb +2 -1
  19. data/lib/aspera/cli/main.rb +30 -12
  20. data/lib/aspera/cli/manager.rb +31 -28
  21. data/lib/aspera/cli/plugins/aoc.rb +2 -2
  22. data/lib/aspera/cli/plugins/base.rb +1 -88
  23. data/lib/aspera/cli/plugins/faspex.rb +6 -6
  24. data/lib/aspera/cli/plugins/faspex5.rb +41 -53
  25. data/lib/aspera/cli/plugins/node.rb +26 -68
  26. data/lib/aspera/cli/plugins/server.rb +1 -1
  27. data/lib/aspera/cli/plugins/shares.rb +4 -2
  28. data/lib/aspera/cli/transfer_agent.rb +3 -0
  29. data/lib/aspera/cli/version.rb +1 -1
  30. data/lib/aspera/dot_container.rb +10 -10
  31. data/lib/aspera/environment.rb +29 -19
  32. data/lib/aspera/keychain/macos_security.rb +1 -1
  33. data/lib/aspera/log.rb +1 -1
  34. data/lib/aspera/markdown.rb +1 -1
  35. data/lib/aspera/persistency_folder.rb +1 -1
  36. data/lib/aspera/preview/utils.rb +2 -2
  37. data/lib/aspera/rest.rb +39 -36
  38. data/lib/aspera/rest_list.rb +121 -0
  39. data/lib/aspera/sync/operations.rb +2 -2
  40. data/lib/aspera/transfer/parameters.rb +8 -8
  41. data/lib/aspera/yaml.rb +1 -1
  42. data.tar.gz.sig +0 -0
  43. metadata +4 -3
  44. metadata.gz.sig +0 -0
@@ -4,13 +4,18 @@ require 'aspera/log'
4
4
  require 'aspera/rest'
5
5
  require 'aspera/transfer/faux_file'
6
6
  require 'aspera/assert'
7
+ require 'net/protocol'
7
8
  require 'securerandom'
8
9
  require 'websocket'
9
10
  require 'base64'
10
11
  require 'json'
11
12
 
13
+ # throw exception on error, instead of error code
14
+ WebSocket.should_raise = true
15
+
12
16
  module Aspera
13
17
  module Api
18
+ # Aspera HTTP Gateway API client
14
19
  # Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
15
20
  # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
16
21
  # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
@@ -75,6 +80,15 @@ module Aspera
75
80
  Log.log.trace2{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
76
81
  end
77
82
 
83
+ # Check header ourself and give precise error message, as websocket will only throw error without details
84
+ # @param [String] Response Header
85
+ # @return [String] Response Header
86
+ def validated_ws_response_header(header)
87
+ first_line = header.split("\r\n").first
88
+ raise RestCallError.new({messages: ["Unexpected: #{first_line}", 'Expected: 101 Switching Protocols']}) unless first_line.split(/\s+/, 3)[1].eql?('101')
89
+ header
90
+ end
91
+
78
92
  # message processing for read thread
79
93
  def process_received_message(message)
80
94
  Log.log.debug{"#{LOG_WS_RECV}message: [#{message}] (#{message.class})"}
@@ -153,8 +167,9 @@ module Aspera
153
167
  @ws_handshake = ::WebSocket::Handshake::Client.new(url: upload_url, headers: {})
154
168
  @ws_io.write(@ws_handshake.to_s)
155
169
  sleep(0.1)
156
- @ws_handshake << @ws_io.readuntil("\r\n\r\n")
157
- Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
170
+ # Get whole HTTP response header, Check and process
171
+ # no need to check `finished?` or result of `<<` (true), as we give the whole header at once
172
+ @ws_handshake << validated_ws_response_header(@ws_io.readuntil("\r\n\r\n"))
158
173
  Log.log.debug{"#{LOG_WS_SEND}handshake success"}
159
174
  # data shared between main thread and read thread
160
175
  @shared_info = {
@@ -232,14 +247,15 @@ module Aspera
232
247
  # throttling may have skipped last one
233
248
  @notify_cb&.call(:transfer, session_id: session_id, info: session_sent_bytes)
234
249
  @notify_cb&.call(:session_end, session_id: session_id)
235
- @notify_cb&.call(:end)
236
250
  ws_send(ws_type: :close, data: nil)
237
251
  Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
238
252
  @ws_read_thread.join
239
253
  Log.log.debug{'Read thread joined'}
254
+ ensure
240
255
  # session no more used
241
256
  @ws_io = nil
242
257
  http_session&.finish
258
+ @notify_cb&.call(:end)
243
259
  end
244
260
 
245
261
  def download(transfer_spec)
@@ -14,7 +14,8 @@ require 'net/ssh/buffer'
14
14
 
15
15
  module Aspera
16
16
  module Api
17
- # Provides additional functions using node API with gen4 extensions (access keys)
17
+ # Aspera Node API client
18
+ # with gen4 extensions (access keys)
18
19
  class Node < Rest
19
20
  # Format of node scope : node.<access key>:<scope>
20
21
  module Scope
@@ -508,7 +509,7 @@ module Aspera
508
509
  # Get the transfer user from info on access key
509
510
  transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
510
511
  # Get settings from name.value array to hash key.value
511
- settings = info['settings']&.each_with_object({}){ |i, h| h[i['name']] = i['value']}
512
+ settings = info['settings']&.to_h{ |i| [i['name'], i['value']]}
512
513
  # Check WSS ports
513
514
  Transfer::Spec::WSS_FIELDS.each do |i|
514
515
  transfer_spec[i] = settings[i] if settings.key?(i)
@@ -520,6 +521,59 @@ module Aspera
520
521
  return transfer_spec
521
522
  end
522
523
 
524
+ # Executes `GET` call in loop using `iteration_token` (`/ops/transfers`)
525
+ # @param iteration [Array] a single element array with the iteration token or nil
526
+ # @param call_args [Hash] additional arguments to pass to `Rest.call`
527
+ # @return [Array] list of items returned by the API call
528
+ def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
529
+ Aspera.assert_type(iteration, Array, NilClass){'iteration'}
530
+ Aspera.assert_type(query, Hash, NilClass){'query'}
531
+ Aspera.assert(!call_args.key?(:query))
532
+ query = {} if query.nil?
533
+ query[:iteration_token] = iteration[0] unless iteration.nil? || iteration[0].nil?
534
+ max = query.delete(RestList::MAX_ITEMS)
535
+ # Return empty list immediately if max is 0
536
+ return [] if max&.zero?
537
+ item_list = []
538
+ loop do
539
+ data, http = read(subpath, query, **call_args, ret: :both)
540
+ Aspera.assert_type(data, Array){"Expected data to be an Array, got: #{data.class}"}
541
+ # no data
542
+ break if data.empty?
543
+ item_list.concat(data)
544
+ # Check if we reached the max limit
545
+ if max&.<=(item_list.length)
546
+ item_list = item_list.slice(0, max)
547
+ break
548
+ end
549
+ # Update progress spinner
550
+ RestParameters.instance.spinner_cb.call(item_list.length)
551
+ # Parse Link header according to RFC 8288 to extract next iteration token
552
+ next_url = Rest.parse_link_header(http['Link'], rel: 'next')
553
+ next_iteration_token = nil
554
+ if next_url
555
+ begin
556
+ parsed_uri = URI.parse(next_url)
557
+ query_params = Rest.query_to_h(parsed_uri.query) if parsed_uri.query
558
+ next_iteration_token = query_params['iteration_token'] if query_params
559
+ rescue URI::InvalidURIError => e
560
+ Log.log.warn{"Invalid URI in Link header: #{next_url} - #{e.message}"}
561
+ end
562
+ end
563
+ # Stop if no next token
564
+ break if next_iteration_token.nil?
565
+ # Stop if same token as current (infinite loop protection)
566
+ break if next_iteration_token.eql?(query[:iteration_token])
567
+ # Update token for next iteration
568
+ query[:iteration_token] = next_iteration_token
569
+ end
570
+ # Signal completion
571
+ RestParameters.instance.spinner_cb.call(action: :success)
572
+ # save iteration token if needed
573
+ iteration[0] = query[:iteration_token] unless iteration.nil?
574
+ item_list
575
+ end
576
+
523
577
  private
524
578
 
525
579
  # Method called in loop for each entry for `resolve_api_fid`
@@ -83,16 +83,15 @@ module Aspera
83
83
 
84
84
  # @return [Hash] with key = file name (String), and value = path to file
85
85
  def file_paths
86
- return SDK_FILES.each_with_object({}) do |v, m|
87
- m[v.to_s] =
88
- begin
89
- path(v)
90
- rescue Errno::ENOENT => e
91
- e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
92
- rescue => e
93
- e.message
94
- end
95
- end
86
+ return SDK_FILES.to_h do |v|
87
+ [v.to_s, begin
88
+ path(v)
89
+ rescue Errno::ENOENT => e
90
+ e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
91
+ rescue => e
92
+ e.message
93
+ end]
94
+ end
96
95
  end
97
96
 
98
97
  # TODO: if using another product than SDK, should use files from there
@@ -179,33 +178,32 @@ module Aspera
179
178
  # Folder, PVCL, version, license information
180
179
  def ascp_info_from_log
181
180
  data = {}
181
+ _, stderr, status = Environment.secure_execute(path(:ascp), '-DDL-', mode: :capture, exception: false)
182
182
  # read PATHs from ascp directly, and pvcl modules as well
183
- Open3.popen3(path(:ascp), '-DDL-') do |_stdin, _stdout, stderr, thread|
184
- last_line = ''
185
- while (line = stderr.gets)
186
- line.chomp!
187
- # skip lines that may have accents
188
- next unless line.valid_encoding?
189
- last_line = line
190
- case line
191
- when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
192
- data[Regexp.last_match(1)] = Regexp.last_match(3)
193
- when /^DBG Added module group:"(?<module>[^"]+)" name:"(?<scheme>[^"]+)", version:"(?<version>[^"]+)" interface:"(?<interface>[^"]+)"$/
194
- c = Regexp.last_match.named_captures.symbolize_keys
195
- data[c[:interface]] ||= {}
196
- data[c[:interface]][c[:module]] ||= []
197
- data[c[:interface]][c[:module]].push("#{c[:scheme]} v#{c[:version]}")
198
- when %r{^DBG License result \(/license/(\S+)\): (.+)$}
199
- data[Regexp.last_match(1)] = Regexp.last_match(2)
200
- when /^LOG (.+) version ([0-9.]+)$/
201
- data['product_name'] = Regexp.last_match(1)
202
- data['product_version'] = Regexp.last_match(2)
203
- when /^LOG Initializing FASP version ([^,]+),/
204
- data['ascp_version'] = Regexp.last_match(1)
205
- end
183
+ last_line = ''
184
+ stderr.lines do |line|
185
+ line.chomp!
186
+ # Skip lines that may have accents
187
+ next unless line.valid_encoding?
188
+ last_line = line
189
+ case line
190
+ when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
191
+ data[Regexp.last_match(1)] = Regexp.last_match(3)
192
+ when /^DBG Added module group:"(?<module>[^"]+)" name:"(?<scheme>[^"]+)", version:"(?<version>[^"]+)" interface:"(?<interface>[^"]+)"$/
193
+ c = Regexp.last_match.named_captures.symbolize_keys
194
+ data[c[:interface]] ||= {}
195
+ data[c[:interface]][c[:module]] ||= []
196
+ data[c[:interface]][c[:module]].push("#{c[:scheme]} v#{c[:version]}")
197
+ when %r{^DBG License result \(/license/(\S+)\): (.+)$}
198
+ data[Regexp.last_match(1)] = Regexp.last_match(2)
199
+ when /^LOG (.+) version ([0-9.]+)$/
200
+ data['product_name'] = Regexp.last_match(1)
201
+ data['product_version'] = Regexp.last_match(2)
202
+ when /^LOG Initializing FASP version ([^,]+),/
203
+ data['ascp_version'] = Regexp.last_match(1)
206
204
  end
207
- raise last_line if !thread.value.exitstatus.eql?(1) && !data.key?('root')
208
205
  end
206
+ raise last_line if !status.exitstatus.eql?(1) && !data.key?('root')
209
207
  return data
210
208
  end
211
209
 
@@ -9,7 +9,15 @@ module Aspera
9
9
  class MissingArgument < Error; end
10
10
  class NoSuchElement < Error; end
11
11
 
12
+ # Raised when a lookup for a specific entity fails to return exactly one result.
13
+ #
14
+ # Provides a formatted message indicating whether the entity was missing
15
+ # or if multiple matches were found (ambiguity).
12
16
  class BadIdentifier < Error
17
+ # @param res_type [String] The type of entity being looked up (e.g., 'user').
18
+ # @param res_id [String] The value of the identifier that failed.
19
+ # @param field [String] The name of the field used for lookup (defaults to 'identifier').
20
+ # @param count [Integer] The number of matches found (0 for not found, >1 for ambiguous).
13
21
  def initialize(res_type, res_id, field: 'identifier', count: 0)
14
22
  msg = count.eql?(0) ? 'not found' : "found #{count}"
15
23
  super("#{res_type} with #{field}=#{res_id}: #{msg}")
@@ -7,7 +7,8 @@ module Aspera
7
7
  CMD_NAME = 'ascli'
8
8
  # Name of the containing gem, same as in <gem name>.gemspec
9
9
  GEM_NAME = 'aspera-cli'
10
- DOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
10
+ DOC_URL = 'https://ibm.biz/ascli-doc'
11
+ RUBYDOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
11
12
  GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
12
13
  SRC_URL = 'https://github.com/IBM/aspera-cli'
13
14
  CONTAINER = 'docker.io/martinlaurent/ascli'
@@ -12,18 +12,34 @@ require 'aspera/cli/hints'
12
12
  require 'aspera/secret_hider'
13
13
  require 'aspera/log'
14
14
  require 'aspera/assert'
15
+ require 'net/ssh/errors'
16
+ require 'openssl'
15
17
 
16
18
  module Aspera
17
19
  module Cli
18
20
  # Global objects shared with plugins
19
21
  class Context
22
+ # @!attribute [rw] options
23
+ # @return [Manager] the command line options manager
24
+ # @!attribute [rw] transfer
25
+ # @return [TransferAgent] the transfer agent, used by transfer plugins
26
+ # @!attribute [rw] config
27
+ # @return [Plugins::Config] the configuration plugin, used by plugins to get configuration values and presets
28
+ # @!attribute [rw] formatter
29
+ # @return [Formatter] the formatter, used by plugins to display results and messages
30
+ # @!attribute [rw] persistency
31
+ # @return [Object] # whatever the type is
32
+ # @!attribute [rw] man_header
33
+ # @return [Boolean] whether to display the manual header in plugin help
20
34
  MEMBERS = %i[options transfer config formatter persistency man_header].freeze
21
35
  attr_accessor(*MEMBERS)
22
36
 
37
+ # Initialize all members to nil, so that they are defined and can be validated later
23
38
  def initialize
24
- @man_header = true
39
+ MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
25
40
  end
26
41
 
42
+ # Validate that all members are set, raise exception if not
27
43
  def validate
28
44
  MEMBERS.each do |i|
29
45
  Aspera.assert(instance_variable_defined?(:"@#{i}"))
@@ -35,7 +51,7 @@ module Aspera
35
51
  transfer.eql?(:only_manual)
36
52
  end
37
53
 
38
- def only_manual
54
+ def only_manual!
39
55
  @transfer = :only_manual
40
56
  end
41
57
  end
@@ -89,13 +105,15 @@ module Aspera
89
105
  status_table.each do |item|
90
106
  worst = TransferAgent.session_status(item[STATUS_FIELD])
91
107
  global_status = worst unless worst.eql?(:success)
92
- item[STATUS_FIELD] = item[STATUS_FIELD].map(&:to_s).join(',')
108
+ item[STATUS_FIELD] = item[STATUS_FIELD].join(',')
93
109
  end
94
110
  raise global_status unless global_status.eql?(:success)
95
111
  return result_object_list(status_table)
96
112
  end
97
113
 
98
114
  # Display image for that URL or directly blob
115
+ #
116
+ # @param url_or_blob [String] URL or blob to display as image
99
117
  def result_image(url_or_blob)
100
118
  return {type: :image, data: url_or_blob}
101
119
  end
@@ -111,6 +129,9 @@ module Aspera
111
129
  end
112
130
 
113
131
  # A list of values
132
+ #
133
+ # @param data [Array] The list of values
134
+ # @param name [String] The name of the list (used for display)
114
135
  def result_value_list(data, name: 'id')
115
136
  Aspera.assert_type(data, Array)
116
137
  Aspera.assert_type(name, String)
@@ -155,7 +176,9 @@ module Aspera
155
176
  execute_command = true
156
177
  # Catch exceptions
157
178
  begin
158
- init_agents_options_plugins
179
+ init_agents_and_options
180
+ # Find plugins, shall be after parse! ?
181
+ Plugins::Factory.instance.add_plugins_from_lookup_folders
159
182
  # Help requested without command ? (plugins must be known here)
160
183
  show_usage if @option_help && @context.options.command_or_arg_empty?
161
184
  generate_bash_completion if @bash_completion
@@ -253,17 +276,11 @@ module Aspera
253
276
  return
254
277
  end
255
278
 
256
- def init_agents_options_plugins
257
- init_agents_and_options
258
- # Find plugins, shall be after parse! ?
259
- Plugins::Factory.instance.add_plugins_from_lookup_folders
260
- end
261
-
262
279
  def show_usage(all: true, exit: true)
263
280
  # Display main plugin options (+config)
264
281
  @context.formatter.display_message(:error, @context.options.parser)
265
282
  if all
266
- @context.only_manual
283
+ @context.only_manual!
267
284
  # List plugins that have a "require" field, i.e. all but main plugin
268
285
  Plugins::Factory.instance.plugin_list.each do |plugin_name_sym|
269
286
  # Config was already included in the global options
@@ -283,6 +300,7 @@ module Aspera
283
300
 
284
301
  # This can throw exception if there is a problem with the environment, needs to be caught by execute method
285
302
  def init_agents_and_options
303
+ @context.man_header = true
286
304
  # Create formatter, in case there is an exception, it is used to display.
287
305
  @context.formatter = Formatter.new
288
306
  # Create command line manager with arguments
@@ -336,7 +354,7 @@ module Aspera
336
354
  OPTIONS
337
355
  #{t}Options begin with a '-' (minus), and value is provided on command line.
338
356
  #{t}Special values are supported beginning with special prefix @pfx:, where pfx is one of:
339
- #{t}#{ExtendedValue.instance.modifiers.map(&:to_s).join(', ')}
357
+ #{t}#{ExtendedValue.instance.modifiers.join(', ')}
340
358
  #{t}Dates format is 'DD-MM-YY HH:MM:SS', or 'now' or '-<num>h'
341
359
 
342
360
  ARGS
@@ -194,7 +194,9 @@ module Aspera
194
194
  [error_msg, 'Use:'].concat(accept_list.map{ |c| "- #{c}"}.sort).join("\n")
195
195
  end
196
196
 
197
- # change option name with dash to name with underscore
197
+ # Change option name with dash to name with underscore
198
+ # @param name [String] option name
199
+ # @return [String]
198
200
  def option_line_to_name(name)
199
201
  return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
200
202
  end
@@ -211,7 +213,7 @@ module Aspera
211
213
  def initialize(program_name, argv = nil)
212
214
  # command line values *not* starting with '-'
213
215
  @unprocessed_cmd_line_arguments = []
214
- # command line values starting with '-'
216
+ # command line values starting with at least one '-'
215
217
  @unprocessed_cmd_line_options = []
216
218
  # a copy of all initial options
217
219
  @initial_cli_options = []
@@ -330,7 +332,7 @@ module Aspera
330
332
 
331
333
  # @param descr [String] description for help
332
334
  # @param mandatory [Boolean] if true, raise error if option not set
333
- # @param multiple [Boolean] if true, return remaining arguments (Array) unil END
335
+ # @param multiple [Boolean] if true, return remaining arguments (Array) until END
334
336
  # @param accept_list [Array, NilClass] list of allowed values (Symbol)
335
337
  # @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
336
338
  # @param aliases [Hash] map of aliases: key = alias, value = real value
@@ -466,21 +468,17 @@ module Aspera
466
468
  # @return [Hash] options as taken from config file and command line just before command execution
467
469
  def unprocessed_options_with_value
468
470
  result = {}
469
- @initial_cli_options.each do |option_value|
470
- case option_value
471
- when /^#{OPTION_PREFIX}([^=]+)$/o
472
- # ignore
473
- when /^#{OPTION_PREFIX}([^=]+)=(.*)$/o
474
- name = Regexp.last_match(1)
475
- value = Regexp.last_match(2)
476
- name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
477
- value = ExtendedValue.instance.evaluate(value, context: "option: #{name}")
478
- Log.log.debug{"option #{name}=#{value}"}
479
- result[name] = value
480
- @unprocessed_cmd_line_options.delete(option_value)
481
- else
482
- raise Cli::BadArgument, "wrong option format: #{option_value}"
483
- end
471
+ @initial_cli_options.each do |option_argument|
472
+ # ignore short options
473
+ next unless option_argument.start_with?(OPTION_PREFIX)
474
+ name, value = option_argument[OPTION_PREFIX.length..-1].split(OPTION_VALUE_SEPARATOR, 2)
475
+ # ignore options without value
476
+ next if value.nil?
477
+ Log.log.debug{"option #{name}=#{value}"}
478
+ path = name.split(DotContainer::SEPARATOR)
479
+ path[0] = self.class.option_line_to_name(path[0])
480
+ DotContainer.dotted_to_container(path, smart_convert(value), result)
481
+ @unprocessed_cmd_line_options.delete(option_argument)
484
482
  end
485
483
  return result
486
484
  end
@@ -516,20 +514,25 @@ module Aspera
516
514
  rescue OptionParser::InvalidOption => e
517
515
  Log.log.trace1{"InvalidOption #{e}".red}
518
516
  # An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
519
- if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
520
- option, path, value = m.captures
521
- option_sym = self.class.option_line_to_name(option).to_sym
522
- if @declared_options.key?(option_sym)
523
- set_option(option_sym, DotContainer.dotted_to_container(path, smart_convert(value), get_option(option_sym)), where: 'dotted')
524
- retry
517
+ if e.args.first.start_with?(OPTION_PREFIX)
518
+ name, value = e.args.first[OPTION_PREFIX.length..-1].split(OPTION_VALUE_SEPARATOR, 2)
519
+ if !value.nil?
520
+ path = name.split(DotContainer::SEPARATOR)
521
+ option_sym = self.class.option_line_to_name(path.shift).to_sym
522
+ if @declared_options.key?(option_sym)
523
+ # it's a known option, so let's process it
524
+ set_option(option_sym, DotContainer.dotted_to_container(path, smart_convert(value), get_option(option_sym)), where: 'dotted')
525
+ # resume to next
526
+ retry
527
+ end
525
528
  end
526
529
  end
527
- # save for later processing
530
+ # Save for later processing
528
531
  unknown_options.push(e.args.first)
529
532
  retry
530
533
  end
531
534
  Log.log.trace1{"remains: #{unknown_options}"}
532
- # set unprocessed options for next time
535
+ # Set unprocessed options for next time
533
536
  @unprocessed_cmd_line_options = unknown_options
534
537
  end
535
538
 
@@ -596,9 +599,9 @@ module Aspera
596
599
  ExtendedValue.assert_no_value(arg, :p)
597
600
  result = nil
598
601
  get_next_argument(:args, multiple: true).each do |arg|
599
- Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not inlude #{OPTION_VALUE_SEPARATOR}"}
602
+ Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not include #{OPTION_VALUE_SEPARATOR}"}
600
603
  path, value = arg.split(OPTION_VALUE_SEPARATOR, 2)
601
- result = DotContainer.dotted_to_container(path, smart_convert(value), result)
604
+ result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
602
605
  end
603
606
  result
604
607
  end
@@ -744,7 +744,7 @@ module Aspera
744
744
  # Short link entity: `short_links` have:
745
745
  # - a numerical id, e.g. `764412`
746
746
  # - a resource type, e.g. `UrlToken`
747
- # - a ressource id, e.g. `scQ7uXPbvQ`
747
+ # - a resource id, e.g. `scQ7uXPbvQ`
748
748
  # - a short URL path, e.g. `dxyRpT9`
749
749
  # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
750
750
  # @param &perm_block [Proc] Optional: create/modify/delete permissions on node
@@ -887,7 +887,7 @@ module Aspera
887
887
 
888
888
  def reject_packages_from_persistency(all_packages, skip_ids_persistency)
889
889
  return if skip_ids_persistency.nil?
890
- skip_package = skip_ids_persistency.data.each_with_object({}){ |i, m| m[i] = true}
890
+ skip_package = skip_ids_persistency.data.to_h{ |i| [i, true]}
891
891
  all_packages.reject!{ |pkg| skip_package[pkg['id']]}
892
892
  end
893
893
 
@@ -14,10 +14,6 @@ module Aspera
14
14
  INSTANCE_OPS = %i[modify delete show].freeze
15
15
  # All standard operations (create list modify delete show)
16
16
  ALL_OPS = (GLOBAL_OPS + INSTANCE_OPS).freeze
17
- # Special query parameter: `max`: max number of items for list command
18
- MAX_ITEMS = 'max'
19
- # Special query parameter: `pmax`: max number of pages for list command
20
- MAX_PAGES = 'pmax'
21
17
 
22
18
  class << self
23
19
  def declare_options(options)
@@ -138,7 +134,6 @@ module Aspera
138
134
  # @param delete_style [String] If set, the delete operation by array in payload
139
135
  # @param id_as_arg [String] If set, the id is provided as url argument ?<id_as_arg>=<id>
140
136
  # @param is_singleton [Boolean] If `true`, entity is the full path to the resource
141
- # @param tclo [Boolean] If `true`, :list use paging with total_count, limit, offset
142
137
  # @param block [Proc] Block to search for identifier based on attribute value
143
138
  # @return [Hash] Result suitable for CLI result
144
139
  def entity_execute(
@@ -151,7 +146,6 @@ module Aspera
151
146
  id_as_arg: false,
152
147
  is_singleton: false,
153
148
  list_query: nil,
154
- tclo: false,
155
149
  &block
156
150
  )
157
151
  command = options.get_next_command(ALL_OPS) if command.nil?
@@ -189,10 +183,6 @@ module Aspera
189
183
  when :show
190
184
  return Main.result_single_object(api.read(one_res_path), fields: display_fields)
191
185
  when :list
192
- if tclo
193
- data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
194
- return Main.result_object_list(data, total: total, fields: display_fields)
195
- end
196
186
  data, http = api.read(entity, query_read_delete, ret: :both)
197
187
  return Main.result_empty if http.code == '204'
198
188
  # TODO: not generic : which application is this for ?
@@ -259,86 +249,9 @@ module Aspera
259
249
  return value
260
250
  end
261
251
 
262
- # Get a (full or partial) list of all entities of a given type with query: offset/limit
263
- # @param api [Rest] API object
264
- # @param entity [String,Symbol] API endpoint of entity to list
265
- # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
266
- # @param query [Hash,nil] Additional query parameters
267
- # @return [Array<(Array<Hash>, Integer)>] items, total_count
268
- def list_entities_limit_offset_total_count(
269
- api:,
270
- entity:,
271
- items_key: nil,
272
- query: nil
273
- )
274
- entity = entity.to_s if entity.is_a?(Symbol)
275
- items_key = entity.split('/').last if items_key.nil?
276
- query = {} if query.nil?
277
- Aspera.assert_type(entity, String)
278
- Aspera.assert_type(items_key, String)
279
- Aspera.assert_type(query, Hash)
280
- Log.log.debug{"list_entities t=#{entity} k=#{items_key} q=#{query}"}
281
- result = []
282
- offset = 0
283
- max_items = query.delete(MAX_ITEMS)
284
- remain_pages = query.delete(MAX_PAGES)
285
- # Merge default parameters, by default 100 per page
286
- query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
287
- total_count = nil
288
- loop do
289
- query['offset'] = offset
290
- page_result = api.read(entity, query)
291
- Aspera.assert_type(page_result[items_key], Array)
292
- result.concat(page_result[items_key])
293
- # Reach the limit set by user ?
294
- if !max_items.nil? && (result.length >= max_items)
295
- result = result.slice(0, max_items)
296
- break
297
- end
298
- total_count ||= page_result['total_count']
299
- break if result.length >= total_count
300
- remain_pages -= 1 unless remain_pages.nil?
301
- break if remain_pages == 0
302
- offset += page_result[items_key].length
303
- RestParameters.instance.spinner_cb.call("#{result.length} / #{total_count || '?'}")
304
- end
305
- RestParameters.instance.spinner_cb.call(action: :success)
306
- return result, total_count
307
- end
308
-
309
- # Lookup an entity id from its name.
310
- # Uses query `q` if `query` is `:default` and `field` is `name`.
311
- # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
312
- # @param value [String] Value to lookup
313
- # @param field [String] Field to match, by default it is `'name'`
314
- # @param items_key [String] Key in the result to get the list of items (override entity)
315
- # @param query [Hash] Additional query parameters (Default: `:default`)
316
- def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
317
- if query.eql?(:default)
318
- Aspera.assert(field.eql?('name')){'Default query is on name only'}
319
- query = {'q'=> value}
320
- end
321
- 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}
322
- end
323
-
324
- # Lookup entity by field and value. Extract single result from list of result returned by block.
325
- # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
326
- # @param value [String] Value to lookup
327
- # @param field [String] Field to match, by default it is `'name'`
328
- # @param block [Proc] Get list of entity matching query.
329
- def lookup_entity_generic(entity:, value:, field: 'name', &block)
330
- Aspera.assert(block_given?)
331
- found = yield
332
- Aspera.assert_array_all(found, Hash)
333
- found = found.select{ |i| i[field].eql?(value)}
334
- return found.first if found.length.eql?(1)
335
- raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
336
- end
337
-
338
- PER_PAGE_DEFAULT = 1000
339
252
  # Percent selector: select by this field for this value
340
253
  REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
341
- private_constant :PER_PAGE_DEFAULT, :REGEX_LOOKUP_ID_BY_FIELD
254
+ private_constant :REGEX_LOOKUP_ID_BY_FIELD
342
255
  end
343
256
  end
344
257
  end