aspera-cli 4.18.1 → 4.20.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -22,7 +22,7 @@ module Aspera
22
22
  next unless base_url.match?('https?://')
23
23
  api = Rest.new(base_url: base_url)
24
24
  test_endpoint = 'api/remote_node_ping'
25
- result = api.read(test_endpoint, {format: :json})
25
+ result = api.call(operation: 'GET', subpath: test_endpoint, headers: {'Accept' => 'application/json'}, query: {format: :json})
26
26
  next unless result[:data]['remote_orchestrator_info']
27
27
  url = result[:http].uri.to_s
28
28
  return {
@@ -69,7 +69,7 @@ module Aspera
69
69
  # @param format [String] the format to request, 'json', 'xml', nil
70
70
  # @param args [Hash] the arguments to pass
71
71
  # @param xml_arrays [Boolean] if true, force arrays in xml parsing
72
- def call_ao(endpoint, prefix: 'api', id: nil, ret_style: nil, format: 'json', args: nil, xml_arrays: true)
72
+ def call_ao(endpoint, prefix: 'api', id: nil, ret_style: nil, format: 'json', args: nil, xml_arrays: true, http: false)
73
73
  # calls are GET
74
74
  call_args = {operation: 'GET', subpath: endpoint}
75
75
  # specify prefix if necessary
@@ -91,9 +91,10 @@ module Aspera
91
91
  end
92
92
  end
93
93
  result = @api_orch.call(**call_args)
94
+ return result[:http] if http
94
95
  result[:data] = XmlSimple.xml_in(result[:http].body, {'ForceArray' => xml_arrays}) if format.eql?('xml')
95
96
  Log.log.debug{Log.dump(:data, result[:data])}
96
- return result
97
+ return result[:data]
97
98
  end
98
99
 
99
100
  def execute_action
@@ -126,7 +127,7 @@ module Aspera
126
127
  when :health
127
128
  nagios = Nagios.new
128
129
  begin
129
- info = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)[:data]
130
+ info = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
130
131
  nagios.add_ok('api', 'accessible')
131
132
  nagios.check_product_version('api', 'orchestrator', info['orchestrator-version'])
132
133
  rescue StandardError => e
@@ -134,15 +135,15 @@ module Aspera
134
135
  end
135
136
  return nagios.result
136
137
  when :info
137
- result = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)[:data]
138
+ result = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
138
139
  return {type: :single_object, data: result}
139
140
  when :processes
140
141
  # TODO: Bug ? API has only XML format
141
- result = call_ao('processes_status', format: 'xml')[:data]
142
+ result = call_ao('processes_status', format: 'xml')
142
143
  return {type: :object_list, data: result['process']}
143
144
  when :plugins
144
145
  # TODO: Bug ? only json format on url
145
- result = call_ao('plugin_version')[:data]
146
+ result = call_ao('plugin_version')
146
147
  return {type: :object_list, data: result['Plugin']}
147
148
  when :workflow
148
149
  command = options.get_next_command(%i[list status inputs details start export])
@@ -152,23 +153,23 @@ module Aspera
152
153
  case command
153
154
  when :status
154
155
  wf_id = nil if wf_id.eql?(SpecialValues::ALL)
155
- result = call_ao('workflows_status', id: wf_id)[:data]
156
+ result = call_ao('workflows_status', id: wf_id)
156
157
  return {type: :object_list, data: result['workflows']['workflow']}
157
158
  when :list
158
- result = call_ao('workflows_list', id: 0)[:data]
159
+ result = call_ao('workflows_list', id: 0)
159
160
  return {
160
161
  type: :object_list,
161
162
  data: result['workflows']['workflow'],
162
163
  fields: %w[id portable_id name published_status published_revision_id latest_revision_id last_modification]
163
164
  }
164
165
  when :details
165
- result = call_ao('workflow_details', id: wf_id)[:data]
166
+ result = call_ao('workflow_details', id: wf_id)
166
167
  return {type: :object_list, data: result['workflows']['workflow']['statuses']}
167
168
  when :inputs
168
- result = call_ao('workflow_inputs_spec', id: wf_id)[:data]
169
+ result = call_ao('workflow_inputs_spec', id: wf_id)
169
170
  return {type: :single_object, data: result['workflow_inputs_spec']}
170
171
  when :export
171
- result = call_ao('export_workflow', id: wf_id, format: nil)[:http]
172
+ result = call_ao('export_workflow', id: wf_id, format: nil, http: true)
172
173
  return {type: :text, data: result.body}
173
174
  when :start
174
175
  result = {
@@ -196,7 +197,7 @@ module Aspera
196
197
  if call_params['synchronous']
197
198
  result[:type] = :text
198
199
  end
199
- result[:data] = call_ao('initiate', id: wf_id, args: call_params)[:data]
200
+ result[:data] = call_ao('initiate', id: wf_id, args: call_params)
200
201
  return result
201
202
  end
202
203
  else Aspera.error_unexpected_value(command)
@@ -133,7 +133,6 @@ module Aspera
133
133
  subpath: "files/#{file_id}/files",
134
134
  headers: headers,
135
135
  query: request_args)[:data]
136
- # return @api_node.read("files/#{file_id}/files",request_args)[:data]
137
136
  end
138
137
 
139
138
  # old version based on folders
@@ -146,7 +145,7 @@ module Aspera
146
145
  # optionally add iteration token from persistency
147
146
  events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
148
147
  begin
149
- events = @api_node.read('events', events_filter)[:data]
148
+ events = @api_node.read('events', events_filter)
150
149
  rescue RestCallError => e
151
150
  if e.message.include?('Invalid iteration_token')
152
151
  Log.log.warn{"Retrying without iteration token: #{e}"}
@@ -164,7 +163,7 @@ module Aspera
164
163
  folder_id = event.dig('data', 'tags', Transfer::Spec::TAG_RESERVED, 'node', 'file_id')
165
164
  folder_id ||= event.dig('data', 'file_id')
166
165
  if !folder_id.nil?
167
- folder_entry = @api_node.read("files/#{folder_id}")[:data] rescue nil
166
+ folder_entry = @api_node.read("files/#{folder_id}") rescue nil
168
167
  scan_folder_files(folder_entry) unless folder_entry.nil?
169
168
  end
170
169
  end
@@ -188,12 +187,12 @@ module Aspera
188
187
  }
189
188
  # optionally add iteration token from persistency
190
189
  events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
191
- events = @api_node.read('events', events_filter)[:data]
190
+ events = @api_node.read('events', events_filter)
192
191
  return if events.empty?
193
192
  events.each do |event|
194
193
  # process only files
195
194
  if event.dig('data', 'type').eql?('file')
196
- file_entry = @api_node.read("files/#{event['data']['id']}")[:data] rescue nil
195
+ file_entry = @api_node.read("files/#{event['data']['id']}") rescue nil
197
196
  if !file_entry.nil? &&
198
197
  @option_skip_folders.none?{|d|file_entry['path'].start_with?(d)}
199
198
  file_entry['parent_file_id'] = event['data']['parent_file_id']
@@ -248,7 +247,7 @@ module Aspera
248
247
  # original_mtime=DateTime.parse(entry['modified_time'])
249
248
  # out: where previews are generated
250
249
  local_entry_preview_dir = File.join(@tmp_folder, entry_preview_folder_name(entry))
251
- file_info = @api_node.read("files/#{entry['id']}")[:data]
250
+ file_info = @api_node.read("files/#{entry['id']}")
252
251
  # TODO: this does not work because previews is hidden in api (gen4)
253
252
  # this_preview_folder_entries=get_folder_entries(@previews_folder_entry['id'],{name: @entry_preview_folder_name})
254
253
  # TODO: use gen3 api to list files and get date
@@ -410,10 +409,10 @@ module Aspera
410
409
  @api_node = Api::Node.new(**basic_auth_params)
411
410
  @transfer_server_address = URI.parse(@api_node.base_url).host
412
411
  # get current access key
413
- @access_key_self = @api_node.read('access_keys/self')[:data]
412
+ @access_key_self = @api_node.read('access_keys/self')
414
413
  # TODO: check events is activated here:
415
414
  # note that docroot is good to look at as well
416
- node_info = @api_node.read('info')[:data]
415
+ node_info = @api_node.read('info')
417
416
  Log.log.debug{"root: #{node_info['docroot']}"}
418
417
  @access_remote = @option_file_access.eql?(:remote)
419
418
  Log.log.debug{"remote: #{@access_remote}"}
@@ -463,7 +462,7 @@ module Aspera
463
462
  'type' => 'folder',
464
463
  'path' => '/' }
465
464
  else
466
- @api_node.read("files/#{scan_id}")[:data]
465
+ @api_node.read("files/#{scan_id}")
467
466
  end
468
467
  @filter_block = Api::Node.file_matcher_from_argument(options)
469
468
  scan_folder_files(folder_info, scan_path)
@@ -9,6 +9,7 @@ require 'aspera/ssh'
9
9
  require 'aspera/nagios'
10
10
  require 'aspera/log'
11
11
  require 'aspera/assert'
12
+ require 'aspera/environment'
12
13
  require 'tempfile'
13
14
  require 'open3'
14
15
 
@@ -32,14 +33,8 @@ module Aspera
32
33
  private_constant :SSH_SCHEME, :URI_SCHEMES, :ASCMD_ALIASES, :TRANSFER_COMMANDS
33
34
 
34
35
  class LocalExecutor
35
- def execute(cmd, line)
36
- # concatenate arguments, enclose in double quotes
37
- cmd = cmd.map{|v|%Q("#{v}")}.join(' ') if cmd.is_a?(Array)
38
- Log.log.debug{"Executing: #{cmd} with '#{line}'"}
39
- stdout_str, stderr_str, status = Open3.capture3(cmd, stdin_data: line, binmode: true)
40
- Log.log.debug{"exec status: #{status} -> #{stderr_str}"}
41
- raise "command #{cmd} failed with code #{status.exitstatus} #{stderr_str}" unless status.success?
42
- return stdout_str
36
+ def execute(ascmd_path, line)
37
+ return Environment.secure_capture(exec: ascmd_path, stdin_data: line, binmode: true)
43
38
  end
44
39
  end
45
40
 
@@ -157,7 +152,8 @@ module Aspera
157
152
  Log.log.debug{"SSH keys=#{ssh_key_list}"}
158
153
  if !ssh_key_list.empty?
159
154
  @ssh_opts[:keys] = ssh_key_list
160
- server_transfer_spec['ssh_private_key'] = File.read(ssh_key_list.first)
155
+ # PEM as per RFC 7468
156
+ server_transfer_spec['ssh_private_key'] = File.read(ssh_key_list.first).strip
161
157
  Log.log.warn{'Using only first SSH key for transfers'} unless ssh_key_list.length.eql?(1)
162
158
  cred_set = true
163
159
  end
@@ -176,7 +172,7 @@ module Aspera
176
172
  def execute_transfer(command, transfer_spec)
177
173
  case command
178
174
  when :upload, :download
179
- Transfer::Spec.action_to_direction(transfer_spec, command)
175
+ transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(command)
180
176
  return Main.result_transfer(transfer.start(transfer_spec))
181
177
  when :sync
182
178
  # lets ignore the arguments provided by execute_sync_action, we just give the transfer spec
@@ -7,7 +7,10 @@ module Aspera
7
7
  module Plugins
8
8
  # Plugin for Aspera Shares v1
9
9
  class Shares < Cli::BasicAuthPlugin
10
- NODE_API_PREFIX = 'node_api'
10
+ # path for node API after base url
11
+ NODE_API_PATH = 'node_api'
12
+ # path for node admin after base url
13
+ ADMIN_API_PATH = 'api/v1'
11
14
  class << self
12
15
  def detect(address_or_url)
13
16
  address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
@@ -16,7 +19,7 @@ module Aspera
16
19
  begin
17
20
  # shall fail: shares requires auth, but we check error message
18
21
  # TODO: use ping instead ?
19
- api.read("#{NODE_API_PREFIX}/app")
22
+ api.read("#{NODE_API_PATH}/app")
20
23
  rescue RestCallError => e
21
24
  if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
22
25
  found = true
@@ -65,7 +68,7 @@ module Aspera
65
68
  nagios = Nagios.new
66
69
  begin
67
70
  res = Rest
68
- .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PREFIX}")
71
+ .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PATH}")
69
72
  .call(
70
73
  operation: 'GET',
71
74
  subpath: 'ping',
@@ -77,13 +80,13 @@ module Aspera
77
80
  end
78
81
  return nagios.result
79
82
  when :repository, :files
80
- api_shares_node = basic_auth_api(NODE_API_PREFIX)
83
+ api_shares_node = basic_auth_api(NODE_API_PATH)
81
84
  repo_command = options.get_next_command(Node::COMMANDS_SHARES)
82
85
  return Node
83
86
  .new(**init_params, api: api_shares_node)
84
87
  .execute_action(repo_command)
85
88
  when :admin
86
- api_shares_admin = basic_auth_api('api/v1')
89
+ api_shares_admin = basic_auth_api(ADMIN_API_PATH)
87
90
  admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
88
91
  case admin_command
89
92
  when :node
@@ -92,8 +95,9 @@ module Aspera
92
95
  share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
93
96
  case share_command
94
97
  when *Plugin::ALL_OPS
95
- return entity_command(share_command, api_shares_admin, 'data/shares')
96
- # return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
98
+ return entity_command(
99
+ share_command, api_shares_admin, 'data/shares',
100
+ display_fields: %w[id name node_id directory percent_free])
97
101
  when :user_permissions, :group_permissions
98
102
  share_id = instance_identifier
99
103
  return entity_action(api_shares_admin, "data/shares/#{share_id}/#{share_command}")
@@ -140,11 +144,11 @@ module Aspera
140
144
  entity_parameters.each_key do |p|
141
145
  raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
142
146
  end
143
- api_shares_admin.create("#{entities_path}/import", entity_parameters)[:data]
147
+ api_shares_admin.create("#{entities_path}/import", entity_parameters)
144
148
  end
145
149
  when :add # ldap
146
150
  return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
147
- api_shares_admin.create(entities_path, {entity_type=>entity_name})[:data]
151
+ api_shares_admin.create(entities_path, {entity_type=>entity_name})
148
152
  end
149
153
  when :users # group
150
154
  return entity_action(api_shares_admin, "#{entities_path}/#{instance_identifier}/#{entities_prefix}users")
@@ -7,11 +7,30 @@ module Aspera
7
7
  module Cli
8
8
  # Module for sync actions
9
9
  module SyncActions
10
- SIMPLE_ARGUMENTS_SYNC = {
11
- direction: Transfer::Sync::DIRECTIONS,
12
- local_dir: String,
13
- remote_dir: String
14
- }.stringify_keys.freeze
10
+ # optional simple command line arguments for sync
11
+ # in Array to keep option order
12
+ # conf: key in option --conf
13
+ # args: key for command line args
14
+ # values: possible values for argument
15
+ # type: type for validation
16
+ SYNC_ARGUMENTS_INFO = [
17
+ {
18
+ conf: 'direction',
19
+ args: 'direction',
20
+ values: Transfer::Sync::DIRECTIONS
21
+ }, {
22
+ conf: 'remote.path',
23
+ args: 'remote_dir',
24
+ type: String
25
+ }, {
26
+ conf: 'local.path',
27
+ args: 'local_dir',
28
+ type: String
29
+ }
30
+ ].freeze
31
+ # name of minimal arguments required, also used to generate a session name
32
+ SYNC_SIMPLE_ARGS = SYNC_ARGUMENTS_INFO.map{|i|i[:conf]}.freeze
33
+ private_constant :SYNC_ARGUMENTS_INFO, :SYNC_SIMPLE_ARGS
15
34
 
16
35
  class << self
17
36
  def declare_options(options)
@@ -19,38 +38,60 @@ module Aspera
19
38
  end
20
39
  end
21
40
 
41
+ # Read command line arguments (3) and converts to sync_info format
42
+ def sync_args_to_params(async_params)
43
+ # sync session parameters can be provided on command line instead of sync_info
44
+ arguments = {}
45
+ SYNC_ARGUMENTS_INFO.each do |info|
46
+ value = options.get_next_argument(
47
+ info[:conf],
48
+ mandatory: false,
49
+ validation: info[:type],
50
+ accept_list: info[:values])
51
+ break if value.nil?
52
+ arguments[info[:conf]] = value.to_s
53
+ end
54
+ Log.log.debug{Log.dump('arguments', arguments)}
55
+ raise Cli::BadArgument, "Provide 0 or 3 arguments, not #{arguments.keys.length} for: #{SYNC_SIMPLE_ARGS.join(', ')}" unless
56
+ [0, 3].include?(arguments.keys.length)
57
+ if !arguments.empty?
58
+ session_info = async_params
59
+ param_path = :conf
60
+ if async_params.key?('sessions') || async_params.key?('instance')
61
+ async_params['sessions'] ||= [{}]
62
+ Aspera.assert(async_params['sessions'].length == 1){'Only one session is supported with arguments'}
63
+ session_info = async_params['sessions'][0]
64
+ param_path = :args
65
+ end
66
+ SYNC_ARGUMENTS_INFO.each do |info|
67
+ key_path = info[param_path].split('.')
68
+ hash_for_key = session_info
69
+ if key_path.length > 1
70
+ first = key_path.shift
71
+ async_params[first] ||= {}
72
+ hash_for_key = async_params[first]
73
+ end
74
+ raise "Parameter #{info[:conf]} is also set in sync_info, remove from sync_info" if hash_for_key.key?(key_path.last)
75
+ hash_for_key[key_path.last] = arguments[info[:conf]]
76
+ end
77
+ if !session_info.key?('name')
78
+ # if no name is specified, generate one from simple arguments
79
+ session_info['name'] = SYNC_SIMPLE_ARGS.filter_map do |arg_name|
80
+ arguments[arg_name]&.gsub(/[^a-zA-Z0-9]/, '')
81
+ end.reject(&:empty?).join('_')
82
+ end
83
+ end
84
+ end
85
+
22
86
  def execute_sync_action(&block)
23
87
  Aspera.assert(block){'No block given'}
24
88
  command = options.get_next_command(%i[start admin])
25
89
  # try to get 3 arguments as simple arguments
26
90
  case command
27
91
  when :start
28
- simple_session_args = {}
29
- SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
30
- value = options.get_next_argument(
31
- arg,
32
- mandatory: false,
33
- validation: check.is_a?(Class) ? check : nil,
34
- accept_list: check.is_a?(Class) ? nil : check)
35
- break if value.nil?
36
- simple_session_args[arg] = value.to_s
37
- end
38
- async_params = nil
39
- if simple_session_args.empty?
40
- async_params = options.get_option(:sync_info, mandatory: true)
41
- else
42
- raise Cli::BadArgument,
43
- "Provide zero or 3 arguments: #{SIMPLE_ARGUMENTS_SYNC.keys.join(',')}" unless simple_session_args.keys.sort == SIMPLE_ARGUMENTS_SYNC.keys.sort
44
- async_params = options.get_option(
45
- :sync_info,
46
- mandatory: false,
47
- default: {'sessions' => [{'name' => File.basename(simple_session_args['local_dir'])}]})
48
- Aspera.assert_type(async_params, Hash){'sync_info'}
49
- Aspera.assert_type(async_params['sessions'], Array){'sync_info[sessions]'}
50
- Aspera.assert_type(async_params['sessions'].first, Hash){'sync_info[sessions][0]'}
51
- async_params['sessions'].first.merge!(simple_session_args)
52
- end
53
- Log.log.debug{Log.dump('async_params', async_params)}
92
+ # possibilities are:
93
+ async_params = options.get_option(:sync_info, default: {})
94
+ sync_args_to_params(async_params)
54
95
  Transfer::Sync.start(async_params, &block)
55
96
  return Main.result_success
56
97
  when :admin
@@ -22,7 +22,7 @@ module Aspera
22
22
  To: <<%=to%>>
23
23
  Subject: <%=subject%>
24
24
 
25
- Transfer is: <%=global_transfer_status%>
25
+ Transfer is: <%=status%>
26
26
 
27
27
  <%=ts.to_yaml%>
28
28
  END_OF_TEMPLATE
@@ -65,6 +65,16 @@ module Aspera
65
65
  @opt_mgr.declare(:transfer, 'Type of transfer agent', values: TRANSFER_AGENTS, default: :direct)
66
66
  @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :transfer_info})
67
67
  @opt_mgr.parse_options!
68
+ @notification_cb = nil
69
+ if !@opt_mgr.get_option(:notify_to).nil?
70
+ @notification_cb = ->(transfer_spec, global_status) do
71
+ @config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: {
72
+ subject: "#{Info::CMD_NAME} transfer: #{global_status}",
73
+ status: global_status,
74
+ ts: transfer_spec
75
+ })
76
+ end
77
+ end
68
78
  end
69
79
 
70
80
  def option_transfer_spec; @transfer_spec_command_line; end
@@ -116,6 +126,7 @@ module Aspera
116
126
  # by default do not display ascp native progress bar
117
127
  agent_options[:quiet] = true unless agent_options.key?(:quiet)
118
128
  agent_options[:check_ignore_cb] = ->(host, port){@config.ignore_cert?(host, port)}
129
+ # JRuby
119
130
  agent_options[:trusted_certs] = @config.trusted_cert_locations unless agent_options.key?(:trusted_certs)
120
131
  when :httpgw
121
132
  unless agent_options.key?(:url) || @httpgw_url_lambda.nil?
@@ -250,22 +261,10 @@ module Aspera
250
261
  agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
251
262
  # list of: :success or "error message string"
252
263
  result = agent_instance.wait_for_completion
253
- send_email_transfer_notification(transfer_spec, result)
264
+ @notification_cb&.call(transfer_spec, self.class.session_status(result))
254
265
  return result
255
266
  end
256
267
 
257
- def send_email_transfer_notification(transfer_spec, statuses)
258
- return if @opt_mgr.get_option(:notify_to).nil?
259
- global_status = self.class.session_status(statuses)
260
- email_vars = {
261
- global_transfer_status: global_status,
262
- subject: "#{PROGRAM_NAME} transfer: #{global_status}",
263
- body: "Transfer is: #{global_status}",
264
- ts: transfer_spec
265
- }
266
- @config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: email_vars)
267
- end
268
-
269
268
  # shut down if agent requires it
270
269
  def shutdown
271
270
  @agent.shutdown if @agent.respond_to?(:shutdown)
@@ -6,27 +6,28 @@ require 'ruby-progressbar'
6
6
 
7
7
  module Aspera
8
8
  module Cli
9
- # progress bar for transfers, supports multi-session
9
+ # Progress bar for transfers.
10
+ # Supports multi-session.
10
11
  class TransferProgress
11
12
  def initialize
12
13
  reset
13
14
  end
14
15
 
16
+ # Reset progress bar, to re-use it.
15
17
  def reset
16
18
  @progress_bar = nil
17
19
  # key is session id
18
20
  @sessions = {}
19
21
  @completed = false
20
- @title = ''
22
+ @title = nil
21
23
  end
22
24
 
23
- def total(key)
24
- @sessions.values.inject(0){|m, s|m + s[key]}
25
- end
26
-
27
- def event(session_id:, type:, info: nil)
28
- Log.log.debug{"progress: #{type} #{session_id} #{info}"}
29
- Aspera.assert(!session_id.nil? || type.eql?(:pre_start)){'session_id is nil'}
25
+ # Called by user of progress bar with a status on a transfer session
26
+ # @param session_id the unique identifier of a transfer session
27
+ # @param type one of: pre_start, session_start, session_size, transfer, end
28
+ # @param info optional specific additional info for the given event type
29
+ def event(type, session_id:, info: nil)
30
+ Log.log.trace1{"progress: #{type} #{session_id} #{info}"}
30
31
  return if @completed
31
32
  if @progress_bar.nil?
32
33
  @progress_bar = ProgressBar.create(
@@ -35,38 +36,55 @@ module Aspera
35
36
  title: '',
36
37
  total: nil)
37
38
  end
38
- need_increment = true
39
+ progress_provided = false
39
40
  case type
40
41
  when :pre_start
42
+ # give opportunity to show progress of initialization with multiple status
43
+ Aspera.assert(session_id.nil?)
44
+ Aspera.assert_type(info, String)
45
+ # initialization of progress bar
41
46
  @title = info
42
47
  when :session_start
48
+ Aspera.assert_type(session_id, String)
49
+ Aspera.assert(info.nil?)
43
50
  raise "Session #{session_id} already started" if @sessions[session_id]
44
51
  @sessions[session_id] = {
45
52
  job_size: 0, # total size of transfer (pre-calc)
46
53
  current: 0
47
54
  }
48
- @title = ''
55
+ # remove last pre-start message if any
56
+ @title = nil
49
57
  when :session_size
58
+ Aspera.assert_type(session_id, String)
59
+ Aspera.assert(!info.nil?)
50
60
  @sessions[session_id][:job_size] = info.to_i
51
61
  current_total = total(:job_size)
52
62
  @progress_bar.total = current_total unless current_total.eql?(@progress_bar.total) || current_total < @progress_bar.progress
53
63
  when :transfer
54
- if !@progress_bar.total.nil?
55
- need_increment = false
64
+ Aspera.assert_type(session_id, String)
65
+ if !@progress_bar.total.nil? && !info.nil?
66
+ progress_provided = true
56
67
  @sessions[session_id][:current] = info.to_i
57
68
  current_total = total(:current)
58
69
  @progress_bar.progress = current_total unless @progress_bar.progress.eql?(current_total)
59
70
  end
60
71
  when :end
61
- @title = ''
72
+ Aspera.assert(session_id, String)
73
+ Aspera.assert(info.nil?)
74
+ @title = nil
62
75
  @completed = true
63
76
  @progress_bar.finish
64
- else
65
- raise "Unknown event type #{type}"
77
+ else Aspera.error_unexpected_value(type){'event type'}
66
78
  end
67
- new_title = @sessions.length < 2 ? @title : "[#{@sessions.length}] #{@title}"
79
+ new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.length}] #{@title}"
68
80
  @progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
69
- @progress_bar.increment if need_increment && !@completed
81
+ @progress_bar.increment if !progress_provided && !@completed
82
+ end
83
+
84
+ private
85
+
86
+ def total(key)
87
+ @sessions.values.inject(0){|m, s|m + s[key]}
70
88
  end
71
89
  end
72
90
  end
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # for beta add extension : .beta1
6
6
  # for dev version add extension : .pre
7
- VERSION = '4.18.1'
7
+ VERSION = '4.20.0'
8
8
  end
9
9
  end
@@ -14,7 +14,7 @@ module Aspera
14
14
  OPTIONS_KEYS = %i[desc accepted_types default enum agents required cli ts deprecation].freeze
15
15
  CLI_KEYS = %i[type switch convert variable].freeze
16
16
 
17
- private_constant :CLI_OPTION_TYPE_SWITCH, :OPTIONS_KEYS, :CLI_KEYS
17
+ private_constant :CLI_OPTION_TYPE_SWITCH, :CLI_OPTION_TYPES, :OPTIONS_KEYS, :CLI_KEYS
18
18
 
19
19
  class << self
20
20
  # transform yes/no to true/false
@@ -22,8 +22,8 @@ module Aspera
22
22
  case value
23
23
  when 'yes' then return true
24
24
  when 'no' then return false
25
+ else Aspera.error_unexpected_value(value){'only: yes or no: '}
25
26
  end
26
- raise "unsupported value: #{value}"
27
27
  end
28
28
 
29
29
  # Called by provider of definition before constructor of this class so that params_definition has all mandatory fields
@@ -177,8 +177,7 @@ module Aspera
177
177
  parameter_value = [parameter_value] unless parameter_value.is_a?(Array)
178
178
  # if transfer_spec value is an array, applies option many times
179
179
  parameter_value.each{|v|add_command_line_options([options[:cli][:switch], v])}
180
- else
181
- raise "ERROR: unknown option processing type: #{processing_type}/#{processing_type.class}"
180
+ else Aspera.error_unexpected_value(processing_type){processing_type.class.name}
182
181
  end
183
182
  end
184
183
  end
@@ -17,5 +17,17 @@ if ENV.key?('ENABLE_COVERAGE')
17
17
  SimpleCov.result.format!
18
18
  $stdout.reopen(original_file_descriptor)
19
19
  end
20
- SimpleCov.start
20
+ # lines with those words are ignored from coverage
21
+ no_cov_functions = %w[error_unreachable_line error_unexpected_value Log.log.trace].freeze
22
+ SimpleCov.start do
23
+ add_filter 'lib/aspera/cli/plugins/faspex.rb'
24
+ add_filter 'lib/aspera/node_simulator.rb'
25
+ add_filter 'lib/aspera/keychain/macos_security.rb'
26
+ add_filter do |source_file|
27
+ source_file.lines.each do |line|
28
+ line.skipped! if no_cov_functions.any?{|i|line.src.include?(i)}
29
+ end
30
+ false
31
+ end
32
+ end
21
33
  end