aspera-cli 4.12.0 → 4.13.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 (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +17 -0
  4. data/CONTRIBUTING.md +97 -22
  5. data/README.md +548 -394
  6. data/bin/ascli +3 -3
  7. data/docs/test_env.conf +12 -5
  8. data/lib/aspera/aoc.rb +42 -42
  9. data/lib/aspera/ascmd.rb +4 -3
  10. data/lib/aspera/cli/extended_value.rb +24 -37
  11. data/lib/aspera/cli/formatter.rb +6 -0
  12. data/lib/aspera/cli/info.rb +2 -4
  13. data/lib/aspera/cli/main.rb +6 -0
  14. data/lib/aspera/cli/manager.rb +15 -6
  15. data/lib/aspera/cli/plugin.rb +1 -5
  16. data/lib/aspera/cli/plugins/aoc.rb +23 -6
  17. data/lib/aspera/cli/plugins/config.rb +13 -6
  18. data/lib/aspera/cli/plugins/faspex.rb +4 -3
  19. data/lib/aspera/cli/plugins/faspex5.rb +175 -42
  20. data/lib/aspera/cli/plugins/node.rb +107 -50
  21. data/lib/aspera/cli/plugins/preview.rb +3 -3
  22. data/lib/aspera/cli/plugins/server.rb +11 -1
  23. data/lib/aspera/cli/plugins/sync.rb +3 -3
  24. data/lib/aspera/cli/transfer_agent.rb +24 -10
  25. data/lib/aspera/cli/version.rb +2 -1
  26. data/lib/aspera/command_line_builder.rb +2 -1
  27. data/lib/aspera/cos_node.rb +1 -1
  28. data/lib/aspera/fasp/agent_connect.rb +1 -1
  29. data/lib/aspera/fasp/agent_direct.rb +12 -12
  30. data/lib/aspera/fasp/agent_node.rb +14 -4
  31. data/lib/aspera/fasp/installation.rb +1 -0
  32. data/lib/aspera/fasp/parameters.rb +11 -3
  33. data/lib/aspera/fasp/parameters.yaml +3 -1
  34. data/lib/aspera/fasp/transfer_spec.rb +3 -1
  35. data/lib/aspera/faspex_gw.rb +1 -0
  36. data/lib/aspera/faspex_postproc.rb +2 -2
  37. data/lib/aspera/node.rb +11 -4
  38. data/lib/aspera/oauth.rb +3 -2
  39. data/lib/aspera/preview/file_types.rb +8 -6
  40. data/lib/aspera/preview/generator.rb +23 -11
  41. data/lib/aspera/preview/options.rb +3 -2
  42. data/lib/aspera/preview/terminal.rb +34 -0
  43. data/lib/aspera/preview/utils.rb +8 -8
  44. data/lib/aspera/rest.rb +5 -4
  45. data/lib/aspera/rest_call_error.rb +3 -1
  46. data/lib/aspera/secret_hider.rb +4 -4
  47. data/lib/aspera/sync.rb +39 -33
  48. data/lib/aspera/web_server_simple.rb +22 -18
  49. data.tar.gz.sig +0 -0
  50. metadata +39 -46
  51. metadata.gz.sig +0 -0
  52. data/examples/aoc.rb +0 -30
  53. data/examples/faspex4.rb +0 -94
  54. data/examples/node.rb +0 -96
  55. data/examples/server.rb +0 -93
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/docs/test_env.conf CHANGED
@@ -4,7 +4,7 @@ config:
4
4
  default:
5
5
  config: cli_default
6
6
  aoc: tst_aoc_jwt
7
- faspex: tst_faspex
7
+ faspex: tst_faspex4
8
8
  faspex5: tst_faspex5_jwt
9
9
  shares: tst_shares
10
10
  node: tst_node_simple
@@ -40,10 +40,18 @@ tst_aoc_web:
40
40
  redirect_uri: your value here
41
41
  client_id: your value here
42
42
  client_secret: your value here
43
- tst_faspex:
43
+ tst_faspex4:
44
44
  url: your value here
45
45
  username: your value here
46
46
  password: your value here
47
+ tst_faspex4_admin:
48
+ username: your value here
49
+ password: your value here
50
+ faspex4_publink:
51
+ link_recv_from_user: your value here
52
+ link_send_to_user: your value here
53
+ link_send_to_dropbox: your value here
54
+ tst_faspex4_storage:
47
55
  storage: your value here
48
56
  tst_hstsfaspex_ssh:
49
57
  url: your value here
@@ -65,6 +73,8 @@ tst_faspex5_jwt:
65
73
  client_secret: your value here
66
74
  private_key: your value here
67
75
  username: your value here
76
+ f5_admin:
77
+ username: your value here
68
78
  tst_shares:
69
79
  url: your value here
70
80
  username: your value here
@@ -133,9 +143,6 @@ nowss:
133
143
  misc:
134
144
  upload_folder: your value here
135
145
  syncuser: your value here
136
- faspex_publink_recv_from_fxuser: your value here
137
- faspex_publink_send_to_fxuser: your value here
138
- faspex_publink_send_to_dropbox: your value here
139
146
  faspex_dbx: your value here
140
147
  faspex_wkg: your value here
141
148
  faspex_src: your value here
data/lib/aspera/aoc.rb CHANGED
@@ -252,25 +252,30 @@ module Aspera
252
252
  return @cache_user_info
253
253
  end
254
254
 
255
- # @returns [Aspera::Node] a node API for access key
256
255
  # @param node_id [String] identifier of node in AoC
256
+ # @param workspace_id [String] workspace identifier
257
+ # @param workspace_name [String] workspace name
257
258
  # @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
259
+ # @param package_info [Hash] created package information
260
+ # @returns [Aspera::Node] a node API for access key
261
+ def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: SCOPE_NODE_USER, package_info: nil)
262
+ raise 'invalid type for node_id' unless node_id.is_a?(String)
263
+ node_info = read("nodes/#{node_id}")[:data]
264
+ if workspace_name.nil? && !workspace_id.nil?
265
+ workspace_name = read("workspaces/#{workspace_id}")[:data]['name']
265
266
  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
267
+ app_info = {
268
+ api: self, # for callback
269
+ app: package_info.nil? ? FILES_APP : PACKAGES_APP,
270
+ node_info: node_info,
271
+ workspace_id: workspace_id,
272
+ workspace_name: workspace_name
273
+ }
274
+ if PACKAGES_APP.eql?(app_info[:app])
275
+ raise 'package info required' if package_info.nil?
276
+ app_info[:package_id] = package_info['id']
277
+ app_info[:package_name] = package_info['name']
272
278
  end
273
- node_info = read("nodes/#{node_id}")[:data]
274
279
  node_rest_params = {base_url: node_info['url']}
275
280
  # if secret is available
276
281
  if scope.nil?
@@ -286,13 +291,6 @@ module Aspera
286
291
  # special header required for bearer token only
287
292
  node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
288
293
  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
294
  return Node.new(params: node_rest_params, app_info: app_info)
297
295
  end
298
296
 
@@ -433,7 +431,10 @@ module Aspera
433
431
  # create a new package container
434
432
  created_package = create('packages', package_data)[:data]
435
433
 
436
- package_node_api = node_api_from(package_info: created_package, scope: AoC::SCOPE_NODE_USER)
434
+ package_node_api = node_api_from(
435
+ node_id: created_package['node_id'],
436
+ workspace_id: created_package['workspace_id'],
437
+ package_info: created_package)
437
438
 
438
439
  # tell AoC what to expect in package: 1 transfer (can also be done after transfer)
439
440
  # TODO: if multi session was used we should probably tell
@@ -454,15 +455,14 @@ module Aspera
454
455
  transfer_type = Fasp::TransferSpec.action(transfer_spec)
455
456
  # Analytics tags
456
457
  ################
457
- ws_info = app_info[:workspace_info]
458
458
  transfer_spec.deep_merge!({
459
459
  'tags' => {
460
- 'aspera' => {
461
- 'usage_id' => "aspera.files.workspace.#{ws_info['id']}", # activity tracking
460
+ Fasp::TransferSpec::TAG_RESERVED => {
461
+ 'usage_id' => "aspera.files.workspace.#{app_info[:workspace_id]}", # activity tracking
462
462
  'files' => {
463
463
  'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
464
- 'workspace_name' => ws_info['name'], # activity tracking
465
- 'workspace_id' => ws_info['id']
464
+ 'workspace_name' => app_info[:workspace_name], # activity tracking
465
+ 'workspace_id' => app_info[:workspace_id]
466
466
  }
467
467
  }
468
468
  }
@@ -477,37 +477,37 @@ module Aspera
477
477
  ##################
478
478
  case app_info[:app]
479
479
  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}"}}}}) \
480
+ file_id = transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['node']['file_id']
481
+ transfer_spec.deep_merge!({'tags' => {Fasp::TransferSpec::TAG_RESERVED => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
482
482
  unless transfer_spec.key?('remote_access_key')
483
483
  when PACKAGES_APP
484
484
  transfer_spec.deep_merge!({
485
485
  'tags' => {
486
- 'aspera' => {
486
+ Fasp::TransferSpec::TAG_RESERVED => {
487
487
  'files' => {
488
- 'package_id' => app_info[:package_info]['id'],
489
- 'package_name' => app_info[:package_info]['name'],
488
+ 'package_id' => app_info[:package_id],
489
+ 'package_name' => app_info[:package_name],
490
490
  'package_operation' => transfer_type
491
491
  }}}})
492
492
  end
493
- transfer_spec['tags']['aspera']['files']['node_id'] = app_info[:node_info]['id']
494
- transfer_spec['tags']['aspera']['app'] = app_info[:app]
493
+ transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
494
+ transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
495
495
  end
496
496
 
497
497
  ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
498
498
  # Callback from Plugins::Node
499
499
  def permissions_create_params(create_param:, app_info:)
500
500
  # workspace shared folder:
501
- # access_id = "#{ID_AK_ADMIN}_WS_#{ app_info[:workspace_info]['id']}"
501
+ # access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
502
502
  default_params = {
503
503
  # 'access_type' => 'user', # mandatory: user or group
504
504
  # 'access_id' => access_id, # id of user or group
505
505
  'tags' => {
506
- 'aspera' => {
506
+ Fasp::TransferSpec::TAG_RESERVED => {
507
507
  'files' => {
508
508
  'workspace' => {
509
- 'id' => app_info[:workspace_info]['id'],
510
- 'workspace_name' => app_info[:workspace_info]['name'],
509
+ 'id' => app_info[:workspace_id],
510
+ 'workspace_name' => app_info[:workspace_name],
511
511
  'user_name' => current_user_info['name'],
512
512
  'shared_by_user_id' => current_user_info['id'],
513
513
  'shared_by_name' => current_user_info['name'],
@@ -520,11 +520,11 @@ module Aspera
520
520
  contact_info = lookup_entity_by_name(
521
521
  'contacts',
522
522
  create_param['with'],
523
- {'current_workspace_id' => app_info[:workspace_info]['id'], 'context' => 'share_folder'})
523
+ {'current_workspace_id' => app_info[:workspace_id], 'context' => 'share_folder'})
524
524
  create_param.delete('with')
525
525
  create_param['access_type'] = contact_info['source_type']
526
526
  create_param['access_id'] = contact_info['source_id']
527
- create_param['tags']['aspera']['files']['workspace']['shared_with_name'] = contact_info['email']
527
+ create_param['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['workspace']['shared_with_name'] = contact_info['email']
528
528
  end
529
529
  # optional
530
530
  app_info[:opt_link_name] = create_param.delete('link_name')
@@ -535,7 +535,7 @@ module Aspera
535
535
  event_creation = {
536
536
  'types' => ['permission.created'],
537
537
  'node_id' => app_info[:node_info]['id'],
538
- 'workspace_id' => app_info[:workspace_info]['id'],
538
+ 'workspace_id' => app_info[:workspace_id],
539
539
  'data' => created_data # Response from previous step
540
540
  }
541
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.
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)
@@ -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
@@ -116,6 +116,12 @@ module Aspera
116
116
  display_message(:info, status)
117
117
  end
118
118
 
119
+ def display_item_count(number, total)
120
+ count_msg = "Items: #{number}/#{total}"
121
+ count_msg = count_msg.bg_red unless number.to_i.eql?(total.to_i)
122
+ display_status(count_msg)
123
+ end
124
+
119
125
  def result_default_fields(results, table_rows_hash_val)
120
126
  unless results[:fields].nil?
121
127
  raise "internal error: [fields] must be Array, not #{results[:fields].class}" unless results[:fields].is_a?(Array)
@@ -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
@@ -335,6 +335,7 @@ module Aspera
335
335
  # finish
336
336
  @plugin_env[:transfer].shutdown
337
337
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
338
+ rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
338
339
  rescue CliBadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
339
340
  rescue CliNoSuchId => e; exception_info = {e: e, t: 'Identifier'}
340
341
  rescue CliError => e; exception_info = {e: e, t: 'Tool', usage: true}
@@ -351,10 +352,15 @@ module Aspera
351
352
  Log.log.warn(exception_info[:e].message) if Aspera::Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
352
353
  @formatter.display_message(:error, "#{ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
353
354
  @formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
355
+ # Provide hint on FASP errors
354
356
  if exception_info[:e].is_a?(Fasp::Error) && exception_info[:e].message.eql?('Remote host is not who we expected')
355
357
  @formatter.display_message(:error, "For this specific error, refer to:\n"\
356
358
  "#{SRC_URL}#error-remote-host-is-not-who-we-expected\nAdd this to arguments:\n--ts=@json:'{\"sshfp\":null}'")
357
359
  end
360
+ # Provide hint on SSL errors
361
+ if exception_info[:e].is_a?(OpenSSL::SSL::SSLError) && ['does not match the server certificate'].any?{|m|exception_info[:e].message.include?(m)}
362
+ @formatter.display_message(:error, "You can ignore SSL errors with option:\n--insecure=yes")
363
+ end
358
364
  end
359
365
  # 2- processing of command not processed (due to exception or bad command line)
360
366
  if execute_command || @option_show_config
@@ -52,9 +52,9 @@ module Aspera
52
52
  # option name separator on command line
53
53
  OPTION_SEP_LINE = '-'
54
54
  # option name separator in code (symbol)
55
- OPTION_SEP_NAME = '_'
55
+ OPTION_SEP_SYMB = '_'
56
56
 
57
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_NAME
57
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMB
58
58
 
59
59
  class << self
60
60
  def enum_to_bool(enum)
@@ -81,6 +81,15 @@ module Aspera
81
81
  def bad_arg_message_multi(error_msg, choices)
82
82
  return [error_msg, 'Use:'].concat(choices.map{|c|"- #{c}"}.sort).join("\n")
83
83
  end
84
+
85
+ # change option name with dash to name with underscore
86
+ def option_line_to_name(name)
87
+ return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMB)
88
+ end
89
+
90
+ def option_name_to_line(name)
91
+ return "--#{name.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)}"
92
+ end
84
93
  end
85
94
 
86
95
  attr_reader :parser
@@ -108,7 +117,7 @@ module Aspera
108
117
  @parser = OptionParser.new
109
118
  @parser.program_name = program_name
110
119
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
111
- env_prefix = program_name.upcase + OPTION_SEP_NAME
120
+ env_prefix = program_name.upcase + OPTION_SEP_SYMB
112
121
  ENV.each do |k, v|
113
122
  if k.start_with?(env_prefix)
114
123
  @unprocessed_env.push([k[env_prefix.length..-1].downcase.to_sym, v])
@@ -223,7 +232,7 @@ module Aspera
223
232
  end
224
233
  end
225
234
 
226
- # get an option value by name
235
+ # Get an option value by name
227
236
  # either return value or call handler, can return nil
228
237
  # ask interactively if requested/required
229
238
  def get_option(option_symbol, is_type: :optional)
@@ -347,7 +356,7 @@ module Aspera
347
356
  when /^--([^=]+)=(.*)$/
348
357
  name = Regexp.last_match(1)
349
358
  value = Regexp.last_match(2)
350
- name.gsub!(OPTION_SEP_LINE, OPTION_SEP_NAME)
359
+ name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMB)
351
360
  value = ExtendedValue.instance.evaluate(value)
352
361
  Log.log.debug{"option #{name}=#{value}"}
353
362
  result[name] = value
@@ -429,7 +438,7 @@ module Aspera
429
438
 
430
439
  # generate command line option from option symbol
431
440
  def symbol_to_option(symbol, opt_val)
432
- result = '--' + symbol.to_s.gsub(OPTION_SEP_NAME, OPTION_SEP_LINE)
441
+ result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)
433
442
  result = result + '=' + opt_val unless opt_val.nil?
434
443
  return result
435
444
  end
@@ -140,11 +140,7 @@ module Aspera
140
140
  if item_list_key
141
141
  item_list = data[item_list_key]
142
142
  total_count = data['total_count']
143
- if !total_count.nil?
144
- count_msg = "Items: #{item_list.length}/#{total_count}"
145
- count_msg = count_msg.bg_red unless item_list.length.eql?(total_count.to_i)
146
- formatter.display_status(count_msg)
147
- end
143
+ formatter.display_item_count(item_list.length, total_count) unless total_count.nil?
148
144
  data = item_list
149
145
  end
150
146
  case data
@@ -142,6 +142,13 @@ module Aspera
142
142
  end
143
143
  home_node_id ||= current_workspace_info['home_node_id'] || current_workspace_info['node_id']
144
144
  home_file_id ||= current_workspace_info['home_file_id']
145
+ if home_node_id.to_s.empty?
146
+ # not part of any workspace, but has some folder shared
147
+ user_info = aoc_api.current_user_info(exception: true)
148
+ home_node_id = user_info['read_only_home_node_id']
149
+ home_file_id = user_info['read_only_home_file_id']
150
+ end
151
+
145
152
  raise "Cannot get user's home node id, check your default workspace or specify one" if home_node_id.to_s.empty?
146
153
  @cache_home_node_file = {
147
154
  node_id: home_node_id,
@@ -214,7 +221,12 @@ module Aspera
214
221
  # @param file_id [String] root file id for the operation (can be AK root, or other, e.g. package, or link)
215
222
  # @param scope [String] node scope, or nil (admin)
216
223
  def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil)
217
- top_node_api = aoc_api.node_api_from(node_id: node_id, workspace_info: current_workspace_info, scope: scope)
224
+ top_node_api = aoc_api.node_api_from(
225
+ node_id: node_id,
226
+ workspace_id: current_workspace_info['id'],
227
+ workspace_name: current_workspace_info['name'],
228
+ scope: scope
229
+ )
218
230
  file_id = top_node_api.read("access_keys/#{top_node_api.app_info[:node_info]['access_key']}")[:data]['root_file_id'] if file_id.nil?
219
231
  node_plugin = Node.new(@agents.merge(
220
232
  skip_basic_auth_options: true,
@@ -440,9 +452,7 @@ module Aspera
440
452
  when :workspace_membership then default_fields.push(*%w[workspace_id member_type member_id])
441
453
  end
442
454
  items = read_with_paging(resource_class_path, option_url_query(default_query))
443
- count_msg = "Items: #{items[:list].length}/#{items[:total]}"
444
- count_msg = count_msg.bg_red unless items[:list].length.eql?(items[:total].to_i)
445
- formatter.display_status(count_msg)
455
+ formatter.display_item_count(items[:list].length, items[:total])
446
456
  return {type: :object_list, data: items[:list], fields: default_fields}
447
457
  when :show
448
458
  object = aoc_api.read(resource_instance_path)[:data]
@@ -588,7 +598,11 @@ module Aspera
588
598
  ids_to_download.each do |package_id|
589
599
  package_info = aoc_api.read("packages/#{package_id}")[:data]
590
600
  formatter.display_status("downloading package: #{package_info['name']}")
591
- package_node_api = aoc_api.node_api_from(package_info: package_info, scope: AoC::SCOPE_NODE_USER)
601
+ package_node_api = aoc_api.node_api_from(
602
+ node_id: package_info['node_id'],
603
+ workspace_id: current_workspace_info['id'],
604
+ workspace_name: current_workspace_info['name'],
605
+ package_info: package_info)
592
606
  statuses = transfer.start(
593
607
  package_node_api.transfer_spec_gen4(
594
608
  package_info['contents_file_id'],
@@ -653,7 +667,10 @@ module Aspera
653
667
  create_params = nil
654
668
  shared_apfid = nil
655
669
  if !folder_dest.nil?
656
- home_node_api = aoc_api.node_api_from(node_id: home_info[:node_id], workspace_info: current_workspace_info, scope: AoC::SCOPE_NODE_USER)
670
+ home_node_api = aoc_api.node_api_from(
671
+ node_id: home_info[:node_id],
672
+ workspace_id: current_workspace_info['id'],
673
+ workspace_name: current_workspace_info['name'])
657
674
  shared_apfid = home_node_api.resolve_api_fid(home_info[:file_id], folder_dest)
658
675
  create_params = {
659
676
  file_id: shared_apfid[:file_id],
@@ -74,6 +74,7 @@ module Aspera
74
74
  DEFAULT_CHECK_NEW_VERSION_DAYS = 7
75
75
  DEFAULT_PRIV_KEY_FILENAME = 'aspera_aoc_key' # pragma: allowlist secret
76
76
  DEFAULT_PRIVKEY_LENGTH = 4096
77
+ COFFEE_IMAGE = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
77
78
  private_constant :DEFAULT_CONFIG_FILENAME,
78
79
  :CONF_PRESET_CONFIG,
79
80
  :CONF_PRESET_VERSION,
@@ -99,7 +100,8 @@ module Aspera
99
100
  :DEFAULT_CHECK_NEW_VERSION_DAYS,
100
101
  :DEFAULT_PRIV_KEY_FILENAME,
101
102
  :SERVER_COMMAND,
102
- :PRESET_DIG_SEPARATOR
103
+ :PRESET_DIG_SEPARATOR,
104
+ :COFFEE_IMAGE
103
105
  def initialize(env, params)
104
106
  raise 'env and params must be Hash' unless env.is_a?(Hash) && params.is_a?(Hash)
105
107
  raise 'missing param' unless %i[name help version gem].sort.eql?(params.keys.sort)
@@ -126,9 +128,9 @@ module Aspera
126
128
  # read correct file (set @config_presets)
127
129
  read_config_file
128
130
  # add preset handler (needed for smtp)
129
- ExtendedValue.instance.set_handler(EXTV_PRESET, :reader, lambda{|v|preset_by_name(v)})
130
- ExtendedValue.instance.set_handler(EXTV_INCLUDE_PRESETS, :decoder, lambda{|v|expanded_with_preset_includes(v)})
131
- ExtendedValue.instance.set_handler(EXTV_VAULT, :decoder, lambda{|v|vault_value(v)})
131
+ ExtendedValue.instance.set_handler(EXTV_PRESET, lambda{|v|preset_by_name(v)})
132
+ ExtendedValue.instance.set_handler(EXTV_INCLUDE_PRESETS, lambda{|v|expanded_with_preset_includes(v)})
133
+ ExtendedValue.instance.set_handler(EXTV_VAULT, lambda{|v|vault_value(v)})
132
134
  # load defaults before it can be overridden
133
135
  add_plugin_default_preset(CONF_GLOBAL_SYM)
134
136
  options.parse_options!
@@ -350,7 +352,7 @@ module Aspera
350
352
  include_path = include_path.clone # avoid messing up if there are multiple branches
351
353
  current = @config_presets
352
354
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
353
- raise CliError, "Expecting Hash for subkey: #{include_path} (#{current.class})" unless current.is_a?(Hash)
355
+ raise CliError, "Expecting Hash for sub key: #{include_path} (#{current.class})" unless current.is_a?(Hash)
354
356
  include_path.push(name)
355
357
  current = current[name]
356
358
  raise CliError, "No such config preset: #{include_path}" if current.nil?
@@ -747,6 +749,7 @@ module Aspera
747
749
  when :set
748
750
  param_name = options.get_next_argument('parameter name')
749
751
  param_value = options.get_next_argument('parameter value')
752
+ param_name = Manager.option_line_to_name(param_name)
750
753
  if !@config_presets.key?(name)
751
754
  Log.log.debug{"no such config name: #{name}, initializing"}
752
755
  selected_preset = @config_presets[name] = {}
@@ -974,7 +977,11 @@ module Aspera
974
977
  BasicAuthPlugin.register_options(@agents)
975
978
  return {type: :single_object, data: identify_plugin_for_url(options.get_option(:url, is_type: :mandatory))}
976
979
  when :coffee
977
- OpenApplication.instance.uri('https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg')
980
+ if OpenApplication.instance.url_method.eql?(:text)
981
+ require 'aspera/preview/terminal'
982
+ return Main.result_status(Preview::Terminal.build(Rest.new(base_url: COFFEE_IMAGE).read('')[:http].body, reserved_lines: 3))
983
+ end
984
+ OpenApplication.instance.uri(COFFEE_IMAGE)
978
985
  return Main.result_nothing
979
986
  when :ascp
980
987
  execute_action_ascp
@@ -170,6 +170,7 @@ module Aspera
170
170
  stop_condition = false
171
171
  # results will be sorted in reverse id
172
172
  items.reverse_each do |package|
173
+ # create the package id, based on recipient's box
173
174
  package[PACKAGE_MATCH_FIELD] =
174
175
  case mailbox
175
176
  when :inbox, :archive
@@ -180,7 +181,7 @@ module Aspera
180
181
  end
181
182
  # if we look for a specific package
182
183
  stop_condition = true if !stop_at_id.nil? && stop_at_id.eql?(package[PACKAGE_MATCH_FIELD])
183
- # keep only those for the specified recipient,
184
+ # keep only those for the specified recipient
184
185
  result.push(package) unless package[PACKAGE_MATCH_FIELD].nil?
185
186
  end
186
187
  break if stop_condition
@@ -378,13 +379,13 @@ module Aspera
378
379
  # NOTE: only external users have token in faspe: link !
379
380
  if !transfer_spec.key?('token')
380
381
  sanitized = id_uri[:uri].gsub('&', '&amp;')
381
- xmlpayload =
382
+ xml_payload =
382
383
  %Q(<?xml version="1.0" encoding="UTF-8"?><url-list xmlns="http://schemas.asperasoft.com/xml/url-list"><url href="#{sanitized}"/></url-list>)
383
384
  transfer_spec['token'] = api_v3.call({
384
385
  operation: 'POST',
385
386
  subpath: 'issue-token?direction=down',
386
387
  headers: {'Accept' => 'text/plain', 'Content-Type' => 'application/vnd.aspera.url-list+xml'},
387
- text_body_params: xmlpayload})[:http].body
388
+ text_body_params: xml_payload})[:http].body
388
389
  end
389
390
  transfer_spec['direction'] = Fasp::TransferSpec::DIRECTION_RECEIVE
390
391
  statuses = transfer.start(transfer_spec)