aspera-cli 4.12.0 → 4.14.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +45 -5
  4. data/CONTRIBUTING.md +113 -22
  5. data/README.md +1289 -754
  6. data/bin/ascli +3 -3
  7. data/examples/dascli +1 -1
  8. data/examples/rubyc +24 -0
  9. data/lib/aspera/aoc.rb +63 -74
  10. data/lib/aspera/ascmd.rb +5 -3
  11. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  12. data/lib/aspera/cli/extended_value.rb +24 -37
  13. data/lib/aspera/cli/formatter.rb +23 -25
  14. data/lib/aspera/cli/info.rb +2 -4
  15. data/lib/aspera/cli/main.rb +27 -27
  16. data/lib/aspera/cli/manager.rb +143 -120
  17. data/lib/aspera/cli/plugin.rb +88 -43
  18. data/lib/aspera/cli/plugins/alee.rb +2 -2
  19. data/lib/aspera/cli/plugins/aoc.rb +235 -104
  20. data/lib/aspera/cli/plugins/ats.rb +16 -18
  21. data/lib/aspera/cli/plugins/bss.rb +3 -3
  22. data/lib/aspera/cli/plugins/config.rb +190 -373
  23. data/lib/aspera/cli/plugins/console.rb +4 -6
  24. data/lib/aspera/cli/plugins/cos.rb +12 -13
  25. data/lib/aspera/cli/plugins/faspex.rb +21 -21
  26. data/lib/aspera/cli/plugins/faspex5.rb +399 -150
  27. data/lib/aspera/cli/plugins/node.rb +260 -174
  28. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  29. data/lib/aspera/cli/plugins/preview.rb +40 -62
  30. data/lib/aspera/cli/plugins/server.rb +33 -16
  31. data/lib/aspera/cli/plugins/shares.rb +24 -33
  32. data/lib/aspera/cli/plugins/sync.rb +6 -6
  33. data/lib/aspera/cli/transfer_agent.rb +47 -30
  34. data/lib/aspera/cli/version.rb +2 -1
  35. data/lib/aspera/colors.rb +9 -7
  36. data/lib/aspera/command_line_builder.rb +2 -1
  37. data/lib/aspera/cos_node.rb +1 -1
  38. data/lib/aspera/data/6 +0 -0
  39. data/lib/aspera/environment.rb +7 -3
  40. data/lib/aspera/fasp/agent_connect.rb +6 -1
  41. data/lib/aspera/fasp/agent_direct.rb +17 -17
  42. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  43. data/lib/aspera/fasp/agent_node.rb +14 -4
  44. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  45. data/lib/aspera/fasp/error_info.rb +2 -0
  46. data/lib/aspera/fasp/installation.rb +19 -19
  47. data/lib/aspera/fasp/parameters.rb +29 -20
  48. data/lib/aspera/fasp/parameters.yaml +5 -2
  49. data/lib/aspera/fasp/resume_policy.rb +3 -3
  50. data/lib/aspera/fasp/transfer_spec.rb +8 -5
  51. data/lib/aspera/fasp/uri.rb +23 -21
  52. data/lib/aspera/faspex_gw.rb +1 -0
  53. data/lib/aspera/faspex_postproc.rb +3 -3
  54. data/lib/aspera/hash_ext.rb +12 -2
  55. data/lib/aspera/keychain/macos_security.rb +13 -13
  56. data/lib/aspera/log.rb +1 -0
  57. data/lib/aspera/node.rb +73 -84
  58. data/lib/aspera/oauth.rb +4 -3
  59. data/lib/aspera/persistency_action_once.rb +1 -1
  60. data/lib/aspera/preview/file_types.rb +8 -6
  61. data/lib/aspera/preview/generator.rb +23 -11
  62. data/lib/aspera/preview/options.rb +3 -2
  63. data/lib/aspera/preview/terminal.rb +80 -0
  64. data/lib/aspera/preview/utils.rb +11 -11
  65. data/lib/aspera/proxy_auto_config.js +2 -2
  66. data/lib/aspera/rest.rb +42 -4
  67. data/lib/aspera/rest_call_error.rb +3 -1
  68. data/lib/aspera/secret_hider.rb +10 -5
  69. data/lib/aspera/ssh.rb +1 -1
  70. data/lib/aspera/sync.rb +41 -33
  71. data/lib/aspera/web_server_simple.rb +22 -18
  72. data.tar.gz.sig +0 -0
  73. metadata +40 -48
  74. metadata.gz.sig +0 -0
  75. data/docs/test_env.conf +0 -179
  76. data/examples/aoc.rb +0 -30
  77. data/examples/faspex4.rb +0 -94
  78. data/examples/node.rb +0 -96
  79. data/examples/server.rb +0 -93
  80. data/lib/aspera/data/7 +0 -0
data/bin/ascli CHANGED
@@ -1,8 +1,9 @@
1
- #!/usr/bin/env ruby
1
+ #!/usr/bin/env ruby -EUTF-8:UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'rubygems'
5
5
  require 'securerandom'
6
+ # compute gem root based on this script location
6
7
  GEM_ROOT = File.realpath(File.join(File.dirname(File.realpath(__FILE__)), '..'))
7
8
  # coverage for tests
8
9
  if ENV.key?('ENABLE_COVERAGE')
@@ -20,10 +21,9 @@ if ENV.key?('ENABLE_COVERAGE')
20
21
  end
21
22
  SimpleCov.start
22
23
  end
24
+ # if in development, add path to gem
23
25
  $LOAD_PATH.unshift(File.join(GEM_ROOT, 'lib'))
24
26
  require 'aspera/cli/main'
25
27
  require 'aspera/environment'
26
- Encoding.default_internal = Encoding::UTF_8
27
- Encoding.default_external = Encoding::UTF_8
28
28
  Aspera::Environment.fix_home
29
29
  Aspera::Cli::Main.new(ARGV).process_command_line
data/examples/dascli CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # set env var image to specify another docker image
3
- : "${image:=martinlaurent/ascli}"
3
+ : "${image:=docker.io/martinlaurent/ascli}"
4
4
  # set env var version to specify another image version (default: latest version)
5
5
  : "${version:=latest}"
6
6
  # set env var imgtag to specify a specific image/version
data/examples/rubyc ADDED
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ # https://github.com/you54f/ruby-packer
3
+ # https://github.com/YOU54F/ruby-packer/releases
4
+ set -e
5
+ FOLDER="$(dirname $0)/../tmp"
6
+ RUBYC="$FOLDER/rubyc"
7
+ if test ! -e "$RUBYC"; then
8
+ mkdir -p "$FOLDER"
9
+ case $(uname -sm|tr ' ' -) in
10
+ Darwin-arm64)
11
+ curl -L https://github.com/YOU54F/ruby-packer/releases/download/rel-20230812/rubyc-Darwin-arm64.tar.gz | tar -xz -C "$FOLDER"
12
+ mv "$FOLDER/rubyc-Darwin-arm64" "$RUBYC"
13
+ ;;
14
+ Linux-x86_64)
15
+ curl -L https://github.com/YOU54F/ruby-packer/releases/download/rel-20230812/rubyc-Linux-x86_64.tar.gz | tar -xz -C "$FOLDER"
16
+ mv "$FOLDER/rubyc-Linux-x86_64" "$RUBYC"
17
+ ;;
18
+ *)
19
+ echo "This architecture is not supported." >&2
20
+ exit 1
21
+ ;;
22
+ esac
23
+ fi
24
+ exec "$RUBYC" "$@"
data/lib/aspera/aoc.rb CHANGED
@@ -42,6 +42,9 @@ module Aspera
42
42
  OAUTH_API_SUBPATH = 'api/v1/oauth2'
43
43
  # minimum fields for user info if retrieval fails
44
44
  USER_INFO_FIELDS_MIN = %w[name email id default_workspace_id organization_id].freeze
45
+ # types of events for shared folder creation
46
+ # Node events: permission.created permission.modified permission.deleted
47
+ PERMISSIONS_CREATED = ['permission.created'].freeze
45
48
 
46
49
  private_constant :MAX_REDIRECT,
47
50
  :GLOBAL_CLIENT_APPS,
@@ -50,7 +53,8 @@ module Aspera
50
53
  :PUBLIC_LINK_PATHS,
51
54
  :JWT_AUDIENCE,
52
55
  :OAUTH_API_SUBPATH,
53
- :USER_INFO_FIELDS_MIN
56
+ :USER_INFO_FIELDS_MIN,
57
+ :PERMISSIONS_CREATED
54
58
 
55
59
  # various API scopes supported
56
60
  SCOPE_FILES_SELF = 'self'
@@ -63,8 +67,6 @@ module Aspera
63
67
  FILES_APP = 'files'
64
68
  PACKAGES_APP = 'packages'
65
69
  API_V1 = 'api/v1'
66
- # error message when entity not found
67
- ENTITY_NOT_FOUND = 'No such'
68
70
 
69
71
  # class static methods
70
72
  class << self
@@ -252,25 +254,30 @@ module Aspera
252
254
  return @cache_user_info
253
255
  end
254
256
 
255
- # @returns [Aspera::Node] a node API for access key
256
257
  # @param node_id [String] identifier of node in AoC
258
+ # @param workspace_id [String] workspace identifier
259
+ # @param workspace_name [String] workspace name
257
260
  # @param scope e.g. SCOPE_NODE_USER, or nil (requires secret)
258
- def node_api_from(node_id: nil, workspace_info: nil, package_info: nil, scope: nil)
259
- if node_id.nil?
260
- if package_info.nil?
261
- raise 'INTERNAL ERROR: either node_id or package_info is required'
262
- else
263
- node_id = package_info['node_id']
264
- end
261
+ # @param package_info [Hash] created package information
262
+ # @returns [Aspera::Node] a node API for access key
263
+ def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: SCOPE_NODE_USER, package_info: nil)
264
+ raise 'invalid type for node_id' unless node_id.is_a?(String)
265
+ node_info = read("nodes/#{node_id}")[:data]
266
+ if workspace_name.nil? && !workspace_id.nil?
267
+ workspace_name = read("workspaces/#{workspace_id}")[:data]['name']
265
268
  end
266
- if workspace_info.nil?
267
- if package_info.nil?
268
- raise 'INTERNAL ERROR: either workspace_info or package_info is required'
269
- else
270
- workspace_info = package_info['workspace_id']
271
- end
269
+ app_info = {
270
+ api: self, # for callback
271
+ app: package_info.nil? ? FILES_APP : PACKAGES_APP,
272
+ node_info: node_info,
273
+ workspace_id: workspace_id,
274
+ workspace_name: workspace_name
275
+ }
276
+ if PACKAGES_APP.eql?(app_info[:app])
277
+ raise 'package info required' if package_info.nil?
278
+ app_info[:package_id] = package_info['id']
279
+ app_info[:package_name] = package_info['name']
272
280
  end
273
- node_info = read("nodes/#{node_id}")[:data]
274
281
  node_rest_params = {base_url: node_info['url']}
275
282
  # if secret is available
276
283
  if scope.nil?
@@ -286,38 +293,9 @@ module Aspera
286
293
  # special header required for bearer token only
287
294
  node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
288
295
  end
289
- app_info = {
290
- node_info: node_info,
291
- workspace_info: workspace_info,
292
- app: package_info.nil? ? FILES_APP : PACKAGES_APP,
293
- api: self # for callback
294
- }
295
- app_info[:package_info] = package_info unless package_info.nil?
296
296
  return Node.new(params: node_rest_params, app_info: app_info)
297
297
  end
298
298
 
299
- # Query entity type by name and returns the id if a single entry only
300
- # @param entity_type path of entity in API
301
- # @param entity_name name of searched entity
302
- # @param options additional search options
303
- def lookup_entity_by_name(entity_type, entity_name, options={})
304
- # returns entities whose name contains value (case insensitive)
305
- matching_items = read(entity_type, options.merge({'q' => CGI.escape(entity_name)}))[:data]
306
- case matching_items.length
307
- when 1 then return matching_items.first
308
- when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{entity_type}: "#{entity_name}"}
309
- else
310
- # multiple case insensitive partial matches, try case insensitive full match
311
- # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
312
- name_matches = matching_items.select{|i|i['name'].casecmp?(entity_name)}
313
- case name_matches.length
314
- when 1 then return name_matches.first
315
- when 0 then raise %Q(#{entity_type}: multiple case insensitive partial match for: "#{entity_name}": #{matching_items.map{|i|i['name']}} but no case insensitive full match. Please be more specific or give exact name.) # rubocop:disable Layout/LineLength
316
- else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{|i|i['name']}}"
317
- end
318
- end
319
- end
320
-
321
299
  # Check metadata: remove when validation is done server side
322
300
  def validate_metadata(pkg_data)
323
301
  # validate only for shared inboxes
@@ -369,7 +347,7 @@ module Aspera
369
347
  # email: user, else dropbox
370
348
  entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
371
349
  begin
372
- full_recipient_info = lookup_entity_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
350
+ full_recipient_info = lookup_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
373
351
  rescue RuntimeError => e
374
352
  raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
375
353
  # dropboxes cannot be created on the fly
@@ -433,7 +411,10 @@ module Aspera
433
411
  # create a new package container
434
412
  created_package = create('packages', package_data)[:data]
435
413
 
436
- package_node_api = node_api_from(package_info: created_package, scope: AoC::SCOPE_NODE_USER)
414
+ package_node_api = node_api_from(
415
+ node_id: created_package['node_id'],
416
+ workspace_id: created_package['workspace_id'],
417
+ package_info: created_package)
437
418
 
438
419
  # tell AoC what to expect in package: 1 transfer (can also be done after transfer)
439
420
  # TODO: if multi session was used we should probably tell
@@ -454,15 +435,14 @@ module Aspera
454
435
  transfer_type = Fasp::TransferSpec.action(transfer_spec)
455
436
  # Analytics tags
456
437
  ################
457
- ws_info = app_info[:workspace_info]
458
438
  transfer_spec.deep_merge!({
459
439
  'tags' => {
460
- 'aspera' => {
461
- 'usage_id' => "aspera.files.workspace.#{ws_info['id']}", # activity tracking
440
+ Fasp::TransferSpec::TAG_RESERVED => {
441
+ 'usage_id' => "aspera.files.workspace.#{app_info[:workspace_id]}", # activity tracking
462
442
  'files' => {
463
443
  'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
464
- 'workspace_name' => ws_info['name'], # activity tracking
465
- 'workspace_id' => ws_info['id']
444
+ 'workspace_name' => app_info[:workspace_name], # activity tracking
445
+ 'workspace_id' => app_info[:workspace_id]
466
446
  }
467
447
  }
468
448
  }
@@ -477,37 +457,40 @@ module Aspera
477
457
  ##################
478
458
  case app_info[:app]
479
459
  when FILES_APP
480
- file_id = transfer_spec['tags']['aspera']['node']['file_id']
481
- transfer_spec.deep_merge!({'tags' => {'aspera' => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
460
+ file_id = transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['node']['file_id']
461
+ transfer_spec.deep_merge!({'tags' => {Fasp::TransferSpec::TAG_RESERVED => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
482
462
  unless transfer_spec.key?('remote_access_key')
483
463
  when PACKAGES_APP
484
464
  transfer_spec.deep_merge!({
485
465
  'tags' => {
486
- 'aspera' => {
466
+ Fasp::TransferSpec::TAG_RESERVED => {
487
467
  'files' => {
488
- 'package_id' => app_info[:package_info]['id'],
489
- 'package_name' => app_info[:package_info]['name'],
468
+ 'package_id' => app_info[:package_id],
469
+ 'package_name' => app_info[:package_name],
490
470
  'package_operation' => transfer_type
491
471
  }}}})
492
472
  end
493
- transfer_spec['tags']['aspera']['files']['node_id'] = app_info[:node_info]['id']
494
- transfer_spec['tags']['aspera']['app'] = app_info[:app]
473
+ transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
474
+ transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
495
475
  end
496
476
 
497
477
  ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
498
478
  # Callback from Plugins::Node
499
- def permissions_create_params(create_param:, app_info:)
479
+ # add application specific tags to permissions creation
480
+ # @param create_param [Hash] parameters for creating permissions
481
+ # @param app_info [Hash] application information
482
+ def permissions_set_create_params(create_param:, app_info:)
500
483
  # workspace shared folder:
501
- # access_id = "#{ID_AK_ADMIN}_WS_#{ app_info[:workspace_info]['id']}"
484
+ # access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
502
485
  default_params = {
503
486
  # 'access_type' => 'user', # mandatory: user or group
504
487
  # 'access_id' => access_id, # id of user or group
505
488
  'tags' => {
506
- 'aspera' => {
489
+ Fasp::TransferSpec::TAG_RESERVED => {
507
490
  'files' => {
508
491
  'workspace' => {
509
- 'id' => app_info[:workspace_info]['id'],
510
- 'workspace_name' => app_info[:workspace_info]['name'],
492
+ 'id' => app_info[:workspace_id],
493
+ 'workspace_name' => app_info[:workspace_name],
511
494
  'user_name' => current_user_info['name'],
512
495
  'shared_by_user_id' => current_user_info['id'],
513
496
  'shared_by_name' => current_user_info['name'],
@@ -517,28 +500,34 @@ module Aspera
517
500
  'node' => app_info[:node_info]['name']}}}}}
518
501
  create_param.deep_merge!(default_params)
519
502
  if create_param.key?('with')
520
- contact_info = lookup_entity_by_name(
503
+ contact_info = lookup_by_name(
521
504
  'contacts',
522
505
  create_param['with'],
523
- {'current_workspace_id' => app_info[:workspace_info]['id'], 'context' => 'share_folder'})
506
+ {'current_workspace_id' => app_info[:workspace_id], 'context' => 'share_folder'})
524
507
  create_param.delete('with')
525
508
  create_param['access_type'] = contact_info['source_type']
526
509
  create_param['access_id'] = contact_info['source_id']
527
- create_param['tags']['aspera']['files']['workspace']['shared_with_name'] = contact_info['email']
510
+ create_param['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['workspace']['shared_with_name'] = contact_info['email']
528
511
  end
529
512
  # optional
530
513
  app_info[:opt_link_name] = create_param.delete('link_name')
531
514
  end
532
515
 
533
516
  # Callback from Plugins::Node
534
- def permissions_create_event(created_data:, app_info:)
517
+ # send shared folder event to AoC
518
+ # @param created_data [Hash] response from permission creation
519
+ # @param app_info [Hash] hash with app info
520
+ # @param types [Array] event types
521
+ def permissions_send_event(created_data:, app_info:, types: PERMISSIONS_CREATED)
522
+ raise "INTERNAL: (assert) Invalid event types: #{types}" unless types.is_a?(Array) && !types.empty?
535
523
  event_creation = {
536
- 'types' => ['permission.created'],
524
+ 'types' => types,
537
525
  'node_id' => app_info[:node_info]['id'],
538
- 'workspace_id' => app_info[:workspace_info]['id'],
539
- 'data' => created_data # Response from previous step
526
+ 'workspace_id' => app_info[:workspace_id],
527
+ 'data' => created_data
540
528
  }
541
- # (optional). The name of the folder to be displayed to the destination user. Use it if its value is different from the "share_as" field.
529
+ # (optional). The name of the folder to be displayed to the destination user.
530
+ # Use it if its value is different from the "share_as" field.
542
531
  event_creation['link_name'] = app_info[:opt_link_name] unless app_info[:opt_link_name].nil?
543
532
  create('events', event_creation)
544
533
  end
data/lib/aspera/ascmd.rb CHANGED
@@ -24,10 +24,11 @@ module Aspera
24
24
  # concatenate arguments, enclose in double quotes, protect backslash and double quotes, add "as_" command and as_exit
25
25
  stdin_input = (args || []).map{|v| '"' + v.gsub(/["\\]/n) {|s| '\\' + s } + '"'}.unshift('as_' + action_sym.to_s).join(' ') + "\nas_exit\n"
26
26
  # execute, get binary output
27
- bytebuffer = @command_executor.execute('ascmd', stdin_input).unpack('C*')
27
+ byte_buffer = @command_executor.execute('ascmd', stdin_input).unpack('C*')
28
+ raise 'ERROR: empty answer from server' if byte_buffer.empty?
28
29
  # get hash or table result
29
- result = self.class.parse(bytebuffer, :result)
30
- raise 'ERROR: unparsed bytes remaining' unless bytebuffer.empty?
30
+ result = self.class.parse(byte_buffer, :result)
31
+ raise 'ERROR: unparsed bytes remaining' unless byte_buffer.empty?
31
32
  # get and delete info,always present in results
32
33
  system_info = result[:info]
33
34
  result.delete(:info)
@@ -126,6 +127,7 @@ module Aspera
126
127
  byte_array = buffer.shift(num_bytes)
127
128
  byte_array = [byte_array] unless byte_array.is_a?(Array)
128
129
  result = byte_array.pack('C*').unpack1(type_descr[:unpack])
130
+ result.force_encoding('UTF-8') if type_name.eql?(:zstr)
129
131
  Log.log.debug{"#{' .' * indent_level}-> base:#{byte_array} -> #{result}"}
130
132
  result = Time.at(result) if type_name.eql?(:epoch)
131
133
  when :buffer_list
@@ -9,9 +9,9 @@ module Aspera
9
9
  class BasicAuthPlugin < Aspera::Cli::Plugin
10
10
  class << self
11
11
  def register_options(env)
12
- env[:options].add_opt_simple(:url, 'URL of application, e.g. https://org.asperafiles.com')
13
- env[:options].add_opt_simple(:username, 'username to log in')
14
- env[:options].add_opt_simple(:password, "user's password")
12
+ env[:options].declare(:url, 'URL of application, e.g. https://org.asperafiles.com')
13
+ env[:options].declare(:username, 'Username to log in')
14
+ env[:options].declare(:password, "User's password")
15
15
  env[:options].parse_options!
16
16
  end
17
17
  end
@@ -23,14 +23,14 @@ module Aspera
23
23
 
24
24
  # returns a Rest object with basic auth
25
25
  def basic_auth_params(subpath=nil)
26
- api_url = options.get_option(:url, is_type: :mandatory)
26
+ api_url = options.get_option(:url, mandatory: true)
27
27
  api_url = api_url + '/' + subpath unless subpath.nil?
28
28
  return {
29
29
  base_url: api_url,
30
30
  auth: {
31
31
  type: :basic,
32
- username: options.get_option(:username, is_type: :mandatory),
33
- password: options.get_option(:password, is_type: :mandatory)
32
+ username: options.get_option(:username, mandatory: true),
33
+ password: options.get_option(:password, mandatory: true)
34
34
  }}
35
35
  end
36
36
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/cli/plugins/config'
4
3
  require 'aspera/uri_reader'
5
4
  require 'aspera/environment'
6
5
  require 'json'
@@ -39,38 +38,33 @@ module Aspera
39
38
 
40
39
  def initialize
41
40
  @handlers = {
42
- decoder: {
43
- base64: lambda{|v|Base64.decode64(v)},
44
- json: lambda{|v|JSON.parse(v)},
45
- zlib: lambda{|v|Zlib::Inflate.inflate(v)},
46
- ruby: lambda{|v|Environment.secure_eval(v)},
47
- csvt: lambda{|v|ExtendedValue.decode_csvt(v)},
48
- lines: lambda{|v|v.split("\n")},
49
- list: lambda{|v|v[1..-1].split(v[0])}
50
- },
51
- reader: {
52
- val: lambda{|v|v},
53
- file: lambda{|v|File.read(File.expand_path(v))},
54
- path: lambda{|v|File.expand_path(v)},
55
- env: lambda{|v|ENV[v]},
56
- uri: lambda{|v|UriReader.read(v)},
57
- stdin: lambda{|v|raise 'no value allowed for stdin' unless v.empty?; $stdin.read} # rubocop:disable Style/Semicolon
58
- }
41
+ base64: lambda{|v|Base64.decode64(v)},
42
+ csvt: lambda{|v|ExtendedValue.decode_csvt(v)},
43
+ env: lambda{|v|ENV[v]},
44
+ file: lambda{|v|File.read(File.expand_path(v))},
45
+ json: lambda{|v|JSON.parse(v)},
46
+ lines: lambda{|v|v.split("\n")},
47
+ list: lambda{|v|v[1..-1].split(v[0])},
48
+ path: lambda{|v|File.expand_path(v)},
49
+ ruby: lambda{|v|Environment.secure_eval(v)},
50
+ secret: lambda{|v|raise 'no value allowed for secret' unless v.empty?; $stdin.getpass('secret> ')}, # rubocop:disable Style/Semicolon
51
+ stdin: lambda{|v|raise 'no value allowed for stdin' unless v.empty?; $stdin.read}, # rubocop:disable Style/Semicolon
52
+ uri: lambda{|v|UriReader.read(v)},
53
+ val: lambda{|v|v},
54
+ zlib: lambda{|v|Zlib::Inflate.inflate(v)}
59
55
  # other handlers can be set using set_handler, e.g. preset is reader in config plugin
60
56
  }
61
57
  end
62
58
 
63
59
  public
64
60
 
65
- def modifiers; @handlers.keys.map{|i|@handlers[i].keys}.flatten.map(&:to_s); end
61
+ def modifiers; @handlers.keys; end
66
62
 
67
- # add a new :reader or :decoder
68
- # decoder can be chained, reader is last one on right
69
- def set_handler(name, type, method)
70
- Log.log.debug{"setting #{type} handler for #{name}"}
63
+ # add a new handler
64
+ def set_handler(name, method)
65
+ Log.log.debug{"setting handler for #{name}"}
71
66
  raise 'name must be Symbol' unless name.is_a?(Symbol)
72
- raise "type #{type} must be one of #{@handlers.keys}" unless @handlers.key?(type)
73
- @handlers[type][name] = method
67
+ @handlers[name] = method
74
68
  end
75
69
 
76
70
  # parse an option value if it is a String using supported extended value modifiers
@@ -78,20 +72,13 @@ module Aspera
78
72
  def evaluate(value)
79
73
  return value if !value.is_a?(String)
80
74
  # first determine decoders, in reversed order
81
- decoders_reversed = []
82
- while (m = value.match(/^@([^:]+):(.*)/)) && @handlers[:decoder].include?(m[1].to_sym)
83
- decoders_reversed.unshift(m[1].to_sym)
75
+ handlers_reversed = []
76
+ while (m = value.match(/^@([^:]+):(.*)/)) && @handlers.include?(m[1].to_sym)
77
+ handlers_reversed.unshift(m[1].to_sym)
84
78
  value = m[2]
85
79
  end
86
- # then read value
87
- @handlers[:reader].each do |reader, method|
88
- if (m = value.match(/^@#{reader}:(.*)/))
89
- value = method.call(m[1])
90
- break
91
- end
92
- end
93
- decoders_reversed.each do |decoder|
94
- value = @handlers[:decoder][decoder].call(value)
80
+ handlers_reversed.each do |handler|
81
+ value = @handlers[handler].call(value)
95
82
  end
96
83
  return value
97
84
  end # parse
@@ -18,9 +18,10 @@ module Aspera
18
18
  # user output levels
19
19
  DISPLAY_LEVELS = %i[info data error].freeze
20
20
  CONF_OVERVIEW_KEYS = %w[config parameter value].freeze
21
+ KEY_VALUE = %w[key value].freeze
21
22
 
22
23
  private_constant :FIELDS_ALL, :FIELDS_DEFAULT, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR,
23
- :CONF_OVERVIEW_KEYS
24
+ :CONF_OVERVIEW_KEYS, :KEY_VALUE
24
25
 
25
26
  class << self
26
27
  # special for Aspera on Cloud display node
@@ -81,22 +82,14 @@ module Aspera
81
82
  @option_flat_hash = true
82
83
  @option_transpose_single = true
83
84
  @option_show_secrets = false
84
- opt_mgr.set_obj_attr(:format, self, :option_format)
85
- opt_mgr.set_obj_attr(:display, self, :option_display)
86
- opt_mgr.set_obj_attr(:fields, self, :option_fields)
87
- opt_mgr.set_obj_attr(:select, self, :option_select)
88
- opt_mgr.set_obj_attr(:table_style, self, :option_table_style)
89
- opt_mgr.set_obj_attr(:flat_hash, self, :option_flat_hash)
90
- opt_mgr.set_obj_attr(:transpose_single, self, :option_transpose_single)
91
- opt_mgr.set_obj_attr(:show_secrets, self, :option_show_secrets)
92
- opt_mgr.add_opt_list(:format, DISPLAY_FORMATS, 'output format')
93
- opt_mgr.add_opt_list(:display, DISPLAY_LEVELS, 'output only some information')
94
- opt_mgr.add_opt_simple(:fields, "comma separated list of fields, or #{FIELDS_ALL}, or #{FIELDS_DEFAULT}")
95
- opt_mgr.add_opt_simple(:select, 'select only some items in lists, extended value: hash (column, value)')
96
- opt_mgr.add_opt_simple(:table_style, 'table display style')
97
- opt_mgr.add_opt_boolean(:flat_hash, 'display hash values as additional keys')
98
- opt_mgr.add_opt_boolean(:transpose_single, 'single object fields output vertically')
99
- opt_mgr.add_opt_boolean(:show_secrets, 'show secrets on command output')
85
+ opt_mgr.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_format})
86
+ opt_mgr.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_display})
87
+ opt_mgr.declare(:fields, "Comma separated list of fields, or #{FIELDS_ALL}, or #{FIELDS_DEFAULT}", handler: {o: self, m: :option_fields})
88
+ opt_mgr.declare(:select, 'Select only some items in lists: column, value', types: Hash, handler: {o: self, m: :option_select})
89
+ opt_mgr.declare(:table_style, 'Table display style', handler: {o: self, m: :option_table_style})
90
+ opt_mgr.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_flat_hash})
91
+ opt_mgr.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_transpose_single})
92
+ opt_mgr.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_show_secrets})
100
93
  end
101
94
 
102
95
  # main output method
@@ -116,6 +109,12 @@ module Aspera
116
109
  display_message(:info, status)
117
110
  end
118
111
 
112
+ def display_item_count(number, total)
113
+ count_msg = "Items: #{number}/#{total}"
114
+ count_msg = count_msg.bg_red unless number.to_i.eql?(total.to_i)
115
+ display_status(count_msg)
116
+ end
117
+
119
118
  def result_default_fields(results, table_rows_hash_val)
120
119
  unless results[:fields].nil?
121
120
  raise "internal error: [fields] must be Array, not #{results[:fields].class}" unless results[:fields].is_a?(Array)
@@ -189,7 +188,7 @@ module Aspera
189
188
  when :single_object # goes to table display
190
189
  # :single_object is a simple hash table (can be nested)
191
190
  raise "internal error: expecting Hash: got #{res_data.class}: #{res_data}" unless res_data.is_a?(Hash)
192
- final_table_columns = results[:columns] || %w[key value]
191
+ final_table_columns = results[:columns] || KEY_VALUE
193
192
  if @option_flat_hash
194
193
  res_data = self.class.flattened_object(res_data, expand_last: results[:option_expand_last])
195
194
  self.class.flatten_name_value_list(res_data)
@@ -201,6 +200,11 @@ module Aspera
201
200
  else user_asked_fields_list_str.split(',')
202
201
  end
203
202
  table_rows_hash_val = asked_fields.map { |i| { final_table_columns.first => i, final_table_columns.last => res_data[i] } }
203
+ # if only one row, and columns are key/value, then display the value only
204
+ if table_rows_hash_val.length == 1 && final_table_columns.eql?(KEY_VALUE)
205
+ display_message(:data, res_data.values.first)
206
+ return
207
+ end
204
208
  when :value_list # goes to table display
205
209
  # :value_list is a simple array of values, name of column provided in the :name
206
210
  final_table_columns = [results[:name]]
@@ -229,7 +233,7 @@ module Aspera
229
233
  # here we expect: table_rows_hash_val and final_table_columns
230
234
  raise 'no field specified' if final_table_columns.nil?
231
235
  if table_rows_hash_val.empty?
232
- display_message(:info, 'empty'.gray) unless @option_format.eql?(:csv)
236
+ display_message(:info, 'empty'.gray) if @option_format.eql?(:table)
233
237
  return
234
238
  end
235
239
  # convert to string with special function. here table_rows_hash_val is an array of hash
@@ -246,12 +250,6 @@ module Aspera
246
250
  when :table
247
251
  style = @option_table_style.chars
248
252
  # display the table !
249
- # display_message(:data,Text::Table.new(
250
- # head: final_table_columns,
251
- # rows: final_table_rows,
252
- # horizontal_boundary: style[0],
253
- # vertical_boundary: style[1],
254
- # boundary_intersection: style[2]))
255
253
  display_message(:data, Terminal::Table.new(
256
254
  headings: final_table_columns,
257
255
  rows: final_table_rows,
@@ -10,9 +10,7 @@ module Aspera
10
10
  GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
11
11
  SRC_URL = 'https://github.com/IBM/aspera-cli'
12
12
  # set this to warn in advance when minimum required ruby version will increase
13
- # for example currently minimum version is 2.4 in gemspec, but future minimum will be different
14
- # set to current minimum if there is no deprecation
15
- # the actual current minimum required version is in gemspec at required_ruby_version
16
- RUBY_FUTURE_MINIMUM_VERSION = '2.7'
13
+ # see also required_ruby_version in gemspec file
14
+ RUBY_FUTURE_MINIMUM_VERSION = '3.0'
17
15
  end
18
16
  end