aspera-cli 4.25.0.pre → 4.25.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -17
  4. data/CONTRIBUTING.md +119 -47
  5. data/README.md +325 -239
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/factory.rb +9 -6
  8. data/lib/aspera/agent/transferd.rb +8 -8
  9. data/lib/aspera/api/aoc.rb +33 -24
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/faspex.rb +11 -5
  12. data/lib/aspera/ascmd.rb +1 -1
  13. data/lib/aspera/ascp/installation.rb +7 -7
  14. data/lib/aspera/ascp/management.rb +9 -5
  15. data/lib/aspera/assert.rb +3 -3
  16. data/lib/aspera/cli/extended_value.rb +10 -2
  17. data/lib/aspera/cli/formatter.rb +15 -62
  18. data/lib/aspera/cli/manager.rb +9 -43
  19. data/lib/aspera/cli/plugins/aoc.rb +71 -66
  20. data/lib/aspera/cli/plugins/ats.rb +30 -36
  21. data/lib/aspera/cli/plugins/base.rb +11 -6
  22. data/lib/aspera/cli/plugins/config.rb +21 -16
  23. data/lib/aspera/cli/plugins/console.rb +2 -1
  24. data/lib/aspera/cli/plugins/faspex.rb +7 -4
  25. data/lib/aspera/cli/plugins/faspex5.rb +12 -9
  26. data/lib/aspera/cli/plugins/faspio.rb +5 -2
  27. data/lib/aspera/cli/plugins/httpgw.rb +2 -1
  28. data/lib/aspera/cli/plugins/node.rb +10 -6
  29. data/lib/aspera/cli/plugins/oauth.rb +12 -11
  30. data/lib/aspera/cli/plugins/orchestrator.rb +2 -1
  31. data/lib/aspera/cli/plugins/preview.rb +2 -2
  32. data/lib/aspera/cli/plugins/server.rb +3 -2
  33. data/lib/aspera/cli/plugins/shares.rb +59 -20
  34. data/lib/aspera/cli/transfer_agent.rb +1 -2
  35. data/lib/aspera/cli/version.rb +1 -1
  36. data/lib/aspera/command_line_builder.rb +5 -5
  37. data/lib/aspera/coverage.rb +5 -1
  38. data/lib/aspera/dot_container.rb +108 -0
  39. data/lib/aspera/environment.rb +69 -89
  40. data/lib/aspera/faspex_postproc.rb +1 -1
  41. data/lib/aspera/id_generator.rb +7 -10
  42. data/lib/aspera/keychain/macos_security.rb +2 -2
  43. data/lib/aspera/log.rb +2 -1
  44. data/lib/aspera/oauth/base.rb +25 -38
  45. data/lib/aspera/oauth/factory.rb +5 -6
  46. data/lib/aspera/oauth/generic.rb +1 -1
  47. data/lib/aspera/oauth/jwt.rb +1 -1
  48. data/lib/aspera/oauth/url_json.rb +4 -3
  49. data/lib/aspera/oauth/web.rb +2 -2
  50. data/lib/aspera/preview/file_types.rb +1 -1
  51. data/lib/aspera/preview/terminal.rb +95 -29
  52. data/lib/aspera/preview/utils.rb +6 -5
  53. data/lib/aspera/rest.rb +5 -2
  54. data/lib/aspera/ssh.rb +6 -5
  55. data/lib/aspera/sync/conf.schema.yaml +2 -2
  56. data/lib/aspera/sync/operations.rb +3 -3
  57. data/lib/aspera/transfer/parameters.rb +6 -6
  58. data/lib/aspera/transfer/spec.schema.yaml +4 -4
  59. data/lib/aspera/transfer/spec_doc.rb +11 -21
  60. data/lib/aspera/uri_reader.rb +17 -3
  61. data.tar.gz.sig +0 -0
  62. metadata +17 -2
  63. metadata.gz.sig +0 -0
@@ -186,20 +186,21 @@ module Aspera
186
186
  Log.log.debug('fasp local shutdown')
187
187
  end
188
188
 
189
+ # @param id [String] Transfer session identifier
189
190
  # @return [Array] list of sessions for a job
190
- def sessions_by_job(job_id)
191
- @sessions.select{ |session| session[:job_id].eql?(job_id)}
191
+ def sessions_by_job(id)
192
+ @sessions.select{ |session| session[:job_id].eql?(id)}
192
193
  end
193
194
 
194
195
  # Send command to management port of command (used in `asession).
195
196
  # Examples:
196
197
  # {'type'=>'START','source'=>_path_,'destination'=>_path_}
197
198
  # {'type'=>'DONE'}
198
- # @param data [Hash] Command on mgt port
199
- # @param id [String] Optional identifier or transfer session
199
+ # @param data [Hash] Command on mgt port (snake case)
200
+ # @param id [String] Optional identifier of transfer session
200
201
  def send_command(data, id: nil)
201
202
  Log.dump(:command, data)
202
- sessions = id ? @sessions.select{ |session| session[:job_id].eql?(id)} : @sessions
203
+ sessions = id ? sessions_by_job(id) : @sessions
203
204
  if sessions.empty?
204
205
  Log.log.warn('No transfer session')
205
206
  return
@@ -251,11 +252,14 @@ module Aspera
251
252
  Aspera.assert_type(session, Hash)
252
253
  notify_progress(:sessions_init, info: 'starting')
253
254
  begin
255
+ # do not use
254
256
  capture_stderr = false
255
257
  stderr_r, stderr_w = nil
256
258
  spawn_args = {}
257
259
  command_pid = nil
258
- command_arguments = []
260
+ # get location of command executable (ascp, async)
261
+ command_path = Ascp::Installation.instance.path(name)
262
+ command_arguments = [command_path]
259
263
  if @monitor
260
264
  # we use Socket directly, instead of TCPServer, as it gives access to lower level options
261
265
  socket_class = defined?(JRUBY_VERSION) ? ServerSocket : Socket
@@ -266,20 +270,18 @@ module Aspera
266
270
  mgt_server_socket.listen(1)
267
271
  # build arguments and add mgt port
268
272
  command_arguments = if name.eql?(:async)
269
- ["--exclusive-mgmt-port=#{mgt_server_socket.local_address.ip_port}"]
273
+ [command_path, "--exclusive-mgmt-port=#{mgt_server_socket.local_address.ip_port}"]
270
274
  else
271
- ['-M', mgt_server_socket.local_address.ip_port.to_s]
275
+ [command_path, '-M', mgt_server_socket.local_address.ip_port.to_s]
272
276
  end
273
277
  end
274
278
  command_arguments.concat(args)
275
279
  if capture_stderr
276
280
  # capture process stderr
277
281
  stderr_r, stderr_w = IO.pipe
278
- spawn_args[err] = stderr_w
282
+ spawn_args[:err] = stderr_w
279
283
  end
280
- # get location of command executable (ascp, async)
281
- command_path = Ascp::Installation.instance.path(name)
282
- command_pid = Environment.secure_spawn(env: env, exec: command_path, args: command_arguments, **spawn_args)
284
+ command_pid = Environment.secure_execute(*command_arguments, mode: :background, env: env, **spawn_args)
283
285
  # close here, but still used in other process (pipe)
284
286
  stderr_w&.close
285
287
  notify_progress(:sessions_init, info: "waiting for #{name} to start")
@@ -16,15 +16,18 @@ module Aspera
16
16
  Aspera::Agent.const_get(agent.to_s.capitalize).new(**options)
17
17
  end
18
18
 
19
- # Discover available agents
20
- # @return [Array] list of symbols of agents
21
- def list
19
+ IGNORED_ITEMS = %i[factory base]
20
+ # Available agents: :long : Capitalized name string, :short : single character symbol
21
+ ALL =
22
22
  Dir.children(File.dirname(File.expand_path(__FILE__)))
23
23
  .select{ |file| file.end_with?(Environment::RB_EXT)}
24
24
  .map{ |file| File.basename(file, Environment::RB_EXT).to_sym}
25
- .reject{ |item| IGNORED_ITEMS.include?(item)}
26
- end
27
- IGNORED_ITEMS = %i[factory base]
25
+ .reject{ |item| IGNORED_ITEMS.include?(item)}.each_with_object({}) do |agent_sym, hash|
26
+ hash[agent_sym] = {
27
+ long: agent_sym.to_s.capitalize,
28
+ short: agent_sym.eql?(:direct) ? :a : agent_sym.to_s[0].to_sym
29
+ }.freeze
30
+ end.freeze
28
31
  private_constant :IGNORED_ITEMS
29
32
  end
30
33
  end
@@ -21,10 +21,10 @@ module Aspera
21
21
 
22
22
  private_constant :LOCAL_SOCKET_ADDR, :PORT_SEP, :AUTO_LOCAL_TCP_PORT
23
23
 
24
- # @param url [String] URL of the transfer manager daemon
25
- # @param start [Bool] if false, expect that an external daemon is already running
26
- # @param stop [Bool] if false, do not shutdown daemon on exit
27
- # @param base [Hash] base class options
24
+ # @param url [String] URL of the transfer manager daemon
25
+ # @param start [Boolean] If `false`, expect that an external daemon is already running
26
+ # @param stop [Boolean] If `false`, do not shutdown daemon on exit
27
+ # @param base [Hash] Base class options
28
28
  def initialize(
29
29
  url: AUTO_LOCAL_TCP_PORT,
30
30
  start: true,
@@ -79,9 +79,9 @@ module Aspera
79
79
  log_stdout = "#{transferd_base_tmp}.out"
80
80
  log_stderr = "#{transferd_base_tmp}.err"
81
81
  File.write(conf_file, config.to_json)
82
- @daemon_pid = Environment.secure_spawn(
83
- exec: Ascp::Installation.instance.path(:transferd),
84
- args: ['--config', conf_file],
82
+ @daemon_pid = Environment.secure_execute(
83
+ Ascp::Installation.instance.path(:transferd), '--config', conf_file,
84
+ mode: :background,
85
85
  out: log_stdout,
86
86
  err: log_stderr
87
87
  )
@@ -170,7 +170,7 @@ module Aspera
170
170
  def stop_daemon
171
171
  if !@daemon_pid.nil?
172
172
  Log.log.debug("Stopping daemon #{@daemon_pid}")
173
- Process.kill(:INT, @daemon_pid)
173
+ Process.kill(:KILL, @daemon_pid)
174
174
  _, status = Process.wait2(@daemon_pid)
175
175
  Log.log.debug("daemon stopped #{status}")
176
176
  @daemon_pid = nil
@@ -33,7 +33,7 @@ module Aspera
33
33
  # types of events for shared folder creation
34
34
  # Node events: permission.created permission.modified permission.deleted
35
35
  PERMISSIONS_CREATED = ['permission.created'].freeze
36
- # Special name when creating workspace shared folders
36
+ # Special user identifier when creating workspace shared folders
37
37
  ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
38
38
 
39
39
  private_constant :MAX_AOC_URL_REDIRECT,
@@ -48,11 +48,13 @@ module Aspera
48
48
  :ID_AK_ADMIN
49
49
 
50
50
  # various API scopes supported
51
- SCOPE_FILES_SELF = 'self'
52
- SCOPE_FILES_USER = 'user:all'
53
- SCOPE_FILES_ADMIN = 'admin:all'
54
- SCOPE_FILES_ADMIN_USER = 'admin-user:all'
55
- SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
51
+ module Scope
52
+ SELF = 'self'
53
+ USER = 'user:all'
54
+ ADMIN = 'admin:all'
55
+ ADMIN_USER = 'admin-user:all'
56
+ ADMIN_USER_USER = "#{ADMIN_USER}+#{USER}"
57
+ end
56
58
  FILES_APP = 'files'
57
59
  PACKAGES_APP = 'packages'
58
60
  API_V1 = 'api/v1'
@@ -227,10 +229,12 @@ module Aspera
227
229
  @workspace_info = nil
228
230
  @home_info = nil
229
231
  auth_params = {
230
- type: :oauth2,
231
- client_id: client_id,
232
- client_secret: client_secret,
233
- scope: scope
232
+ type: :oauth2,
233
+ params: {
234
+ client_id: client_id,
235
+ client_secret: client_secret,
236
+ scope: scope
237
+ }
234
238
  }
235
239
  # analyze type of url
236
240
  url_info = AoC.link_info(url)
@@ -256,20 +260,20 @@ module Aspera
256
260
  Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
257
261
  auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
258
262
  auth_params[:payload] = {
259
- iss: auth_params[:client_id], # issuer
263
+ iss: client_id, # issuer
260
264
  sub: username, # subject
261
265
  aud: JWT_AUDIENCE
262
266
  }
263
267
  # add jwt payload for global client id
264
- auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(auth_params[:client_id])
268
+ auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(client_id)
265
269
  auth_params[:cache_ids] = [url_info[:organization]]
266
270
  when :url_json
267
- auth_params[:url] = {grant_type: 'url_token'} # URL arguments
271
+ auth_params[:url] = {grant_type: 'url_token'} # Query arguments
268
272
  auth_params[:json] = {url_token: url_info[:token]} # JSON body
269
273
  # password protection of link
270
274
  auth_params[:json][:password] = password unless password.nil?
271
275
  # basic auth required for /token
272
- auth_params[:auth] = {type: :basic, username: auth_params[:client_id], password: auth_params[:client_secret]}
276
+ auth_params[:auth] = {type: :basic, username: client_id, password: client_secret}
273
277
  else Aspera.error_unexpected_value(auth_params[:grant_method]){'auth, use one of: :web, :jwt'}
274
278
  end
275
279
  super(
@@ -386,11 +390,12 @@ module Aspera
386
390
  @home_info
387
391
  end
388
392
 
389
- # @param node_id [String] identifier of node in AoC
390
- # @param workspace_id [String] workspace identifier
391
- # @param workspace_name [String] workspace name
392
- # @param scope e.g. Node::SCOPE_USER, or nil (requires secret)
393
- # @param package_info [Hash] created package information
393
+ # Return a Node API for given node id, in a given context (files, packages), for the given scope.
394
+ # @param node_id [String] identifier of node in AoC
395
+ # @param workspace_id [String,nil] workspace identifier
396
+ # @param workspace_name [String,nil] workspace name
397
+ # @param scope [String,nil] e.g. Node::SCOPE_USER, or Node::SCOPE_ADMIN, or nil (requires secret)
398
+ # @param package_info [Hash,nil] created package information
394
399
  # @returns [Node] a node API for access key
395
400
  def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::SCOPE_USER, package_info: nil)
396
401
  Aspera.assert_type(node_id, String)
@@ -409,18 +414,22 @@ module Aspera
409
414
  app_info[:package_name] = package_info['name']
410
415
  end
411
416
  node_params = {base_url: node_info['url']}
412
- # if secret is available
413
- if scope.nil?
417
+ ak_secret = @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'])
418
+ # If secret is available, or no scope, use basic auth
419
+ if scope.nil? || ak_secret
420
+ Aspera.assert(ak_secret, "Secret not found for access key #{node_info['access_key']}@#{node_info['url']}", type: Error)
414
421
  node_params[:auth] = {
415
422
  type: :basic,
416
423
  username: node_info['access_key'],
417
- password: @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'], mandatory: true)
424
+ password: ak_secret
418
425
  }
419
426
  else
420
427
  # OAuth bearer token
421
428
  node_params[:auth] = auth_params.clone
422
- node_params[:auth][:scope] = Node.token_scope(node_info['access_key'], scope)
423
- # special header required for bearer token only
429
+ node_params[:auth][:params] ||= {}
430
+ node_params[:auth][:params][:scope] = Node.token_scope(node_info['access_key'], scope)
431
+ node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::SCOPE_ADMIN)
432
+ # Special header required for bearer token only
424
433
  node_params[:headers] = {Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
425
434
  end
426
435
  node_params[:app_info] = app_info
@@ -5,6 +5,7 @@ require 'aspera/rest'
5
5
 
6
6
  module Aspera
7
7
  module Api
8
+ # ATS API without authentication
8
9
  class Ats < Aspera::Rest
9
10
  SERVICE_BASE_URL = 'https://ats.aspera.io'
10
11
  # currently supported clouds
@@ -34,7 +34,7 @@ module Aspera
34
34
  query: {
35
35
  response_type: :code,
36
36
  state: @context,
37
- client_id: client_id,
37
+ client_id: params[:client_id],
38
38
  redirect_uri: @redirect_uri
39
39
  },
40
40
  exception: false,
@@ -46,7 +46,7 @@ module Aspera
46
46
  raise Error, info['action_message'] if info['action_message']
47
47
  Aspera.assert(info['code']){'Missing code in answer'}
48
48
  # Exchange code for token
49
- return create_token_call(optional_scope_client_id.merge(
49
+ return create_token_call(base_params.merge(
50
50
  grant_type: 'authorization_code',
51
51
  code: info['code'],
52
52
  redirect_uri: @redirect_uri
@@ -155,7 +155,9 @@ module Aspera
155
155
  base_url: "#{base_url}/#{PATH_AUTH}",
156
156
  grant_method: :faspex_pub_link,
157
157
  context: encoded_context,
158
- client_id: config[:client_id],
158
+ params: {
159
+ client_id: config[:client_id]
160
+ },
159
161
  redirect_uri: config[:redirect_uri]
160
162
  }
161
163
  }
@@ -177,7 +179,9 @@ module Aspera
177
179
  type: :oauth2,
178
180
  base_url: "#{url}/#{PATH_AUTH}",
179
181
  grant_method: :web,
180
- client_id: client_id,
182
+ params: {
183
+ client_id: client_id
184
+ },
181
185
  redirect_uri: redirect_uri
182
186
  }
183
187
  }
@@ -190,7 +194,9 @@ module Aspera
190
194
  type: :oauth2,
191
195
  base_url: "#{url}/#{PATH_AUTH}",
192
196
  grant_method: :jwt,
193
- client_id: client_id,
197
+ params: {
198
+ client_id: client_id
199
+ },
194
200
  payload: {
195
201
  iss: client_id, # issuer
196
202
  aud: client_id, # audience (this field is not clear...)
data/lib/aspera/ascmd.rb CHANGED
@@ -89,7 +89,7 @@ module Aspera
89
89
  # Version 2 allows use of reverse proxy with multiple addresses.
90
90
  # @param [Symbol] one of OPERATIONS
91
91
  # @param [Array] parameters for "as" command
92
- # @return result of command, type depends on command (bool, array, hash)
92
+ # @return [Boolean,Array,Hash] result of command, type depends on command
93
93
  def execute_single(action_sym, arguments, version: 1, host: nil)
94
94
  arguments = [] if arguments.nil?
95
95
  Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
@@ -294,12 +294,12 @@ module Aspera
294
294
  end
295
295
 
296
296
  # Retrieves ascp binary for current system architecture from URL or file
297
- # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
- # @param folder [String] Destination folder path
299
- # @param backup [Bool] If destination folder exists, then rename
300
- # @param with_exe [Bool] If false, only retrieves files, but do not generate or restrict access
301
- # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
302
- # @return ascp version (from execution)
297
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
+ # @param folder [String] Destination folder path
299
+ # @param backup [Boolean] If destination folder exists, then rename
300
+ # @param with_exe [Boolean] If false, only retrieves files, but do not generate or restrict access
301
+ # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
302
+ # @return [Array] name, ascp version (from execution), folder
303
303
  def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
304
304
  url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
305
305
  folder = Products::Transferd.sdk_directory if folder.nil?
@@ -348,7 +348,7 @@ module Aspera
348
348
  sdk_name = 'IBM Aspera Transfer SDK'
349
349
  sdk_version = transferd_version || sdk_ascp_version
350
350
  File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
351
- return sdk_name, sdk_version
351
+ return sdk_name, sdk_version, folder
352
352
  end
353
353
 
354
354
  attr_accessor :transferd_urls
@@ -5,8 +5,12 @@ require 'aspera/assert'
5
5
  module Aspera
6
6
  module Ascp
7
7
  # processing of ascp management port events
8
+ # Reference: `mgmtmess.c`
8
9
  class Management
9
- # from https://www.google.com/search?q=FASP+error+codes
10
+ # References:
11
+ # https://www.google.com/search?q=FASP+error+codes
12
+ # https://www.ibm.com/support/pages/error-code-reference-tables
13
+ # mgmtmess.c : as_mgmt_err_is_retryable
10
14
  # Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
11
15
  # rubocop:disable Layout/FirstHashElementLineBreak
12
16
  ERRORS = {
@@ -289,7 +293,7 @@ module Aspera
289
293
  # cspell: enable
290
294
 
291
295
  class << self
292
- # translate native event name to snake case
296
+ # Translate native event name to snake case
293
297
  def field_native_to_snake(name)
294
298
  case name
295
299
  when 'Elapsedusec' then 'elapsed_usec'
@@ -298,7 +302,7 @@ module Aspera
298
302
  end
299
303
  end
300
304
 
301
- # translate snake case event name to native
305
+ # Translate snake case event name to native
302
306
  # @param name [String] Field name
303
307
  def field_snake_to_native(name)
304
308
  field = name.delete('_')
@@ -320,7 +324,7 @@ module Aspera
320
324
  end
321
325
 
322
326
  # Build command to send on management port
323
- # @param data [Hash] e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
327
+ # @param data [Hash] keys are snake case: e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
324
328
  # @return [String] frame to send on management port
325
329
  def command_to_stream(data)
326
330
  data
@@ -339,7 +343,7 @@ module Aspera
339
343
  end
340
344
  attr_reader :last_event
341
345
 
342
- # process line of mgt port event
346
+ # Process line of mgt port event
343
347
  # @param line [String] line of mgt port event
344
348
  # @return [Hash] event hash or nil if event is not yet complete
345
349
  def process_line(line)
data/lib/aspera/assert.rb CHANGED
@@ -31,7 +31,7 @@ module Aspera
31
31
  end
32
32
 
33
33
  # Assert that a condition is true, else raise exception
34
- # @param assertion [Bool] Must be true
34
+ # @param assertion [TrueClass, FalseClass] Must be true
35
35
  # @param info [String,nil] Fixed message in case assert fails, else use `block`
36
36
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
37
37
  # @param block [Proc] Produces a string that describes the problem for complex messages
@@ -78,7 +78,7 @@ module Aspera
78
78
  end
79
79
 
80
80
  # Assert that value is one of the given values
81
- # @param value [any] Value to check
81
+ # @param value [Object] Value to check
82
82
  # @param values [Array] Accepted values
83
83
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
84
84
  # @param block [Proc] Additional description in front of message
@@ -91,7 +91,7 @@ module Aspera
91
91
  end
92
92
 
93
93
  # The value is not one of the expected values
94
- # @param value [any] The wrong value
94
+ # @param value [Object] The wrong value
95
95
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
96
96
  # @param block [Proc] Additional description in front of message
97
97
  def error_unexpected_value(value, type: InternalError)
@@ -64,6 +64,15 @@ module Aspera
64
64
  def assert_no_value(value, ext_type)
65
65
  Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
66
66
  end
67
+
68
+ def read_stdin(mode)
69
+ case mode
70
+ when '' then $stdin.read
71
+ when 'bin' then $stdin.binmode.read
72
+ when 'chomp' then $stdin.chomp
73
+ else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
74
+ end
75
+ end
67
76
  end
68
77
 
69
78
  private
@@ -87,8 +96,7 @@ module Aspera
87
96
  re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
88
97
  ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
89
98
  secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
90
- stdin: lambda{ |i| ExtendedValue.assert_no_value(i, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
91
- stdbin: lambda{ |i| ExtendedValue.assert_no_value(i, :stdbin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
99
+ stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
92
100
  yaml: lambda{ |i| YAML.load(i)},
93
101
  zlib: lambda{ |i| Zlib::Inflate.inflate(i)},
94
102
  extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
@@ -8,6 +8,7 @@ require 'aspera/environment'
8
8
  require 'aspera/log'
9
9
  require 'aspera/assert'
10
10
  require 'aspera/markdown'
11
+ require 'aspera/dot_container'
11
12
  require 'terminal-table'
12
13
  require 'tty-spinner'
13
14
  require 'yaml'
@@ -64,9 +65,10 @@ module Aspera
64
65
  end
65
66
  end
66
67
 
68
+ # Give Markdown String, or matched data, return formatted string for terminal
67
69
  # used by spec_doc
68
70
  # @param match [MatchData,String]
69
- def markdown(match)
71
+ def markdown_text(match)
70
72
  if match.is_a?(String)
71
73
  match = Markdown::FORMATS.match(match)
72
74
  Aspera.assert(match)
@@ -84,11 +86,11 @@ module Aspera
84
86
  end
85
87
  end
86
88
 
87
- # replace empty values with a readable version on terminal
88
- def enhance_display_values_hash(input_hash)
89
- stack = [input_hash]
90
- until stack.empty?
91
- current = stack.pop
89
+ # Replace special values with a readable version on terminal
90
+ def replace_specific_for_terminal(input_hash)
91
+ hash_to_process = [input_hash]
92
+ until hash_to_process.empty?
93
+ current = hash_to_process.pop
92
94
  current.each do |key, value|
93
95
  case value
94
96
  when NilClass
@@ -100,73 +102,24 @@ module Aspera
100
102
  when Array
101
103
  if value.empty?
102
104
  current[key] = special_format('empty list')
105
+ elsif value.all?(String)
106
+ current[key] = value.join(',')
103
107
  else
104
108
  value.each do |item|
105
- stack.push(item) if item.is_a?(Hash)
109
+ hash_to_process.push(item) if item.is_a?(Hash)
106
110
  end
107
111
  end
108
112
  when Hash
109
113
  if value.empty?
110
114
  current[key] = special_format('empty dict')
111
115
  else
112
- stack.push(value)
116
+ hash_to_process.push(value)
113
117
  end
114
118
  end
115
119
  end
116
120
  end
117
121
  end
118
122
 
119
- # Given a list of string, display that list in a single cell
120
- def list_to_string(list)
121
- list.join(',')
122
- end
123
-
124
- # Build new prefix
125
- def add_prefix(prefix, key)
126
- [prefix, key].compact.join('.')
127
- end
128
-
129
- # Add elements of enumerator to the stack, in reverse order
130
- def add_elements(stack, prefix, enum)
131
- enum.reverse_each do |key, value|
132
- stack.push([add_prefix(prefix, key), value])
133
- end
134
- end
135
-
136
- # Flatten a Hash into single level hash
137
- def flatten_hash(input)
138
- Aspera.assert_type(input, Hash)
139
- return input if input.empty?
140
- flat = {}
141
- # tail (pop,push) contains the next element to display
142
- stack = [[nil, input]]
143
- until stack.empty?
144
- prefix, current = stack.pop
145
- # empty things will be displayed as such
146
- if current.respond_to?(:empty?) && current.empty?
147
- flat[prefix] = current
148
- next
149
- end
150
- case current
151
- when Hash
152
- add_elements(stack, prefix, current)
153
- when Array
154
- if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
155
- flat[prefix] = list_to_string(current.map(&:to_s))
156
- elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
157
- flat[prefix] = list_to_string(current.map{ |i| i['name']})
158
- elsif current.all?{ |i| i.is_a?(Hash) && i.keys.sort == %w[name value]}
159
- add_elements(stack, prefix, current.each_with_object({}){ |i, h| h[i['name']] = i['value']})
160
- else
161
- add_elements(stack, prefix, current.each_with_index.map{ |v, i| [i, v]})
162
- end
163
- else
164
- flat[prefix] = current
165
- end
166
- end
167
- flat
168
- end
169
-
170
123
  def all_but(list)
171
124
  list = [list] unless list.is_a?(Array)
172
125
  return list.map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
@@ -356,13 +309,13 @@ module Aspera
356
309
  if data.empty?
357
310
  display_message(:data, self.class.special_format('empty dict'))
358
311
  else
359
- data = self.class.flatten_hash(data) if @options[:flat_hash]
312
+ data = DotContainer.new(data).to_dotted if @options[:flat_hash]
360
313
  display_table([data], compute_fields([data], fields), single: true)
361
314
  end
362
315
  when :object_list
363
316
  # :object_list is an Array of Hash, where key=column name
364
317
  Aspera.assert_array_all(data, Hash){'result'}
365
- data = data.map{ |obj| self.class.flatten_hash(obj)} if @options[:flat_hash]
318
+ data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
366
319
  display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
367
320
  when :value_list
368
321
  # :value_list is a simple array of values, name of column provided in `name`
@@ -474,7 +427,7 @@ module Aspera
474
427
  return
475
428
  end
476
429
  filter_columns_on_select(object_array)
477
- object_array.each{ |i| self.class.enhance_display_values_hash(i)}
430
+ object_array.each{ |i| self.class.replace_specific_for_terminal(i)}
478
431
  # if table has only one element, and only one field, display the value
479
432
  if object_array.length == 1 && fields.length == 1
480
433
  Log.log.debug("display_table: single element, field: #{fields.first}")