aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,92 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore snid fnid bidi ssync asyncs rund asnodeadmin mkfile mklink asperabrowser asperabrowserurl watchfolders watchfolderd entsrv
3
4
  require 'aspera/cli/basic_auth_plugin'
4
- require 'aspera/cli/plugins/sync'
5
+ require 'aspera/cli/sync_actions'
6
+ require 'aspera/fasp/transfer_spec'
5
7
  require 'aspera/nagios'
6
8
  require 'aspera/hash_ext'
7
9
  require 'aspera/id_generator'
8
10
  require 'aspera/node'
9
11
  require 'aspera/aoc'
10
- require 'aspera/sync'
11
- require 'aspera/fasp/transfer_spec'
12
+ require 'aspera/oauth'
13
+ require 'aspera/node_simulator'
14
+ require 'aspera/assert'
12
15
  require 'base64'
13
16
  require 'zlib'
14
17
 
15
18
  module Aspera
16
19
  module Cli
17
20
  module Plugins
18
- class SyncSpecGen3
19
- def initialize(api_node)
20
- @api_node = api_node
21
- end
22
-
23
- def transfer_spec(direction, local_path, remote_path)
24
- # empty transfer spec for authorization request
25
- direction_sym = direction.to_sym
26
- request_transfer_spec = {
27
- type: Aspera::Sync::DIRECTION_TO_REQUEST_TYPE[direction_sym],
28
- paths: {
29
- source: remote_path,
30
- destination: local_path
31
- }
32
- }
33
- # add fixed parameters if any (for COS)
34
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
35
- # prepare payload for single request
36
- setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
37
- # only one request, so only one answer
38
- transfer_spec = @api_node.create('files/sync_setup', setup_payload)[:data]['transfer_specs'].first['transfer_spec']
39
- # API returns null tag... but async does not like it
40
- transfer_spec.delete_if{ |_k, v| v.nil? }
41
- # delete this part, as the returned value contains only destination, and not sources
42
- # transfer_spec.delete('paths') if command.eql?(:upload)
43
- Log.dump(:ts, transfer_spec)
44
- return transfer_spec
45
- end
46
- end
47
-
48
- class SyncSpecGen4
49
- def initialize(api_node, top_file_id)
50
- @api_node = api_node
51
- @top_file_id = top_file_id
52
- end
53
-
54
- def transfer_spec(direction, local_path, remote_path)
55
- # remote is specified by option to_folder
56
- apifid = @api_node.resolve_api_fid(@top_file_id, remote_path)
57
- transfer_spec = apifid[:api].transfer_spec_gen4(apifid[:file_id], Fasp::TransferSpec::DIRECTION_SEND)
58
- Log.dump(:ts, transfer_spec)
59
- return transfer_spec
60
- end
61
- end
62
-
63
21
  class Node < Aspera::Cli::BasicAuthPlugin
22
+ include SyncActions
64
23
  class << self
65
- def detect(base_url)
66
- api = Rest.new({ base_url: base_url})
67
- result = api.call({ operation: 'GET', subpath: 'ping'})
68
- if result[:http].body.eql?('')
69
- return { product: :node, version: 'unknown'}
24
+ @@node_options_declared = false # rubocop:disable Style/ClassVars
25
+ def application_name
26
+ 'HSTS Node API'
27
+ end
28
+
29
+ def detect(address_or_url)
30
+ urls = if address_or_url.match?(%r{^[a-z]{1,6}://})
31
+ [address_or_url]
32
+ else
33
+ [
34
+ "https://#{address_or_url}",
35
+ "https://#{address_or_url}:9092",
36
+ "http://#{address_or_url}:9091"
37
+ ]
38
+ end
39
+
40
+ urls.each do |base_url|
41
+ next unless base_url.match?('https?://')
42
+ api = Rest.new(base_url: base_url)
43
+ test_endpoint = 'ping'
44
+ result = api.call(operation: 'GET', subpath: test_endpoint)
45
+ next unless result[:http].body.eql?('')
46
+ url_length = -2 - test_endpoint.length
47
+ return {
48
+ url: result[:http].uri.to_s[0..url_length]
49
+ }
50
+ rescue StandardError => e
51
+ Log.log.debug{"detect error: #{e}"}
70
52
  end
71
53
  return nil
72
54
  end
73
55
 
74
- def register_node_options(env)
75
- env[:options].declare(:validator, 'Identifier of validator (optional for central)')
76
- env[:options].declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
77
- env[:options].declare(:sync_name, 'Sync name')
78
- env[:options].declare(:default_ports, 'Use standard FASP ports or get from node api (gen4)', values: :bool, default: :yes)
79
- env[:options].parse_options!
80
- Aspera::Node.use_standard_ports = env[:options].get_option(:default_ports)
56
+ def wizard(object:, private_key_path: nil, pub_key_pem: nil)
57
+ options = object.options
58
+ return {
59
+ preset_value: {
60
+ url: options.get_option(:url, mandatory: true),
61
+ username: options.get_option(:username, mandatory: true),
62
+ password: options.get_option(:password, mandatory: true)
63
+ },
64
+ test_args: 'info'
65
+ }
66
+ end
67
+
68
+ def declare_options(options, force: false)
69
+ return if @@node_options_declared && !force
70
+ @@node_options_declared = true # rubocop:disable Style/ClassVars
71
+ options.declare(:validator, 'Identifier of validator (optional for central)')
72
+ options.declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
73
+ options.declare(:sync_name, 'Sync name')
74
+ options.declare(
75
+ :default_ports, 'Use standard FASP ports or get from node api (gen4)', values: :bool, default: :yes,
76
+ handler: {o: Aspera::Node, m: :use_standard_ports})
77
+ options.declare(:root_id, 'File id of top folder if using bearer tokens')
78
+ SyncActions.declare_options(options)
79
+ options.parse_options!
81
80
  end
82
81
  end
83
82
 
84
83
  # spellchecker: disable
85
84
  # SOAP API call to test central API
86
- CENTRAL_SOAP_API_TEST = '<?xml version="1.0" encoding="UTF-8"?>'\
87
- '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:typ="urn:Aspera:XML:FASPSessionNET:2009/11:Types">'\
88
- '<soapenv:Header></soapenv:Header>'\
89
- '<soapenv:Body><typ:GetSessionInfoRequest><SessionFilter><SessionStatus>running</SessionStatus></SessionFilter></typ:GetSessionInfoRequest></soapenv:Body>'\
85
+ CENTRAL_SOAP_API_TEST = '<?xml version="1.0" encoding="UTF-8"?>' \
86
+ '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:typ="urn:Aspera:XML:FASPSessionNET:2009/11:Types">' \
87
+ '<soapenv:Header></soapenv:Header>' \
88
+ '<soapenv:Body><typ:GetSessionInfoRequest><SessionFilter><SessionStatus>running</SessionStatus></SessionFilter></typ:GetSessionInfoRequest></soapenv:Body>' \
90
89
  '</soapenv:Envelope>'
91
90
  # spellchecker: enable
92
91
 
@@ -94,17 +93,17 @@ module Aspera
94
93
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
95
94
 
96
95
  # actions in execute_command_gen3
97
- COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download sync]
96
+ COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download http_node_download sync]
98
97
 
99
98
  BASE_ACTIONS = %i[api_details].concat(COMMANDS_GEN3).freeze
100
99
 
101
- SPECIAL_ACTIONS = %i[health events info license].freeze
100
+ SPECIAL_ACTIONS = %i[health events info slash license].freeze
102
101
 
103
102
  # actions available in v3 in gen4
104
- V3_IN_V4_ACTIONS = %i[access_key].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
103
+ V3_IN_V4_ACTIONS = %i[access_keys].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
105
104
 
106
105
  # actions used commonly when a node is involved
107
- COMMON_ACTIONS = %i[access_key].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
106
+ COMMON_ACTIONS = %i[access_keys].concat(BASE_ACTIONS).concat(SPECIAL_ACTIONS).freeze
108
107
 
109
108
  private_constant(*%i[CENTRAL_SOAP_API_TEST SEARCH_REMOVE_FIELDS BASE_ACTIONS SPECIAL_ACTIONS V3_IN_V4_ACTIONS COMMON_ACTIONS])
110
109
 
@@ -114,26 +113,22 @@ module Aspera
114
113
  # commands for execute_command_gen4
115
114
  COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
116
115
 
117
- COMMANDS_COS = %i[upload download info access_key api_details transfer].freeze
116
+ COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
118
117
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
119
118
  COMMANDS_FASPEX = COMMON_ACTIONS
120
119
 
121
- def initialize(env)
120
+ def initialize(env, api: nil)
122
121
  super(env)
123
- self.class.register_node_options(env) unless env[:skip_node_options]
124
- return if env[:man_only]
122
+ Node.declare_options(options, force: env[:all_manuals])
125
123
  @api_node =
126
- if env.key?(:node_api)
124
+ if !api.nil? || env[:all_manuals]
127
125
  # this can be Aspera::Node or Aspera::Rest (shares)
128
- env[:node_api]
129
- elsif options.get_option(:password, mandatory: true).start_with?('Bearer ')
126
+ api
127
+ elsif Oauth.bearer?(options.get_option(:password, mandatory: true))
130
128
  # info is provided like node_info of aoc
131
129
  Aspera::Node.new(params: {
132
130
  base_url: options.get_option(:url, mandatory: true),
133
- headers: {
134
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options.get_option(:username, mandatory: true),
135
- 'Authorization' => options.get_option(:password, mandatory: true)
136
- }
131
+ headers: Aspera::Node.bearer_headers(options.get_option(:password, mandatory: true))
137
132
  })
138
133
  else
139
134
  # this is normal case
@@ -147,26 +142,6 @@ module Aspera
147
142
  end
148
143
  end
149
144
 
150
- def c_textify_browse(table_data)
151
- return table_data.map do |i|
152
- i['permissions'] = i['permissions'].map { |x| x['name'] }.join(',')
153
- i
154
- end
155
- end
156
-
157
- # key/value is defined in main in hash_table
158
- def c_textify_bool_list_result(list, name_list)
159
- list.each_index do |i|
160
- next unless name_list.include?(list[i]['key'])
161
- list[i]['value'].each do |item|
162
- list.push({'key' => item['name'], 'value' => item['value']})
163
- end
164
- list.delete_at(i)
165
- # continue at same index because we delete current one
166
- redo
167
- end
168
- end
169
-
170
145
  # reduce the path from a result on given named column
171
146
  def c_result_remove_prefix_path(result, column, path_prefix)
172
147
  if !path_prefix.nil?
@@ -222,7 +197,7 @@ module Aspera
222
197
  when :search
223
198
  search_root = get_next_arg_add_prefix(prefix_path, 'search root')
224
199
  parameters = {'path' => search_root}
225
- other_options = value_or_query(allowed_types: Hash)
200
+ other_options = query_option
226
201
  parameters.merge!(other_options) unless other_options.nil?
227
202
  resp = @api_node.create('files/search', parameters)
228
203
  result = { type: :object_list, data: resp[:data]['items']}
@@ -268,17 +243,40 @@ module Aspera
268
243
  # if there is no items
269
244
  case send_result['self']['type']
270
245
  when 'directory', 'container' # directory: node, container: shares
271
- result = { data: send_result['items'], type: :object_list, textify: lambda { |table_data| c_textify_browse(table_data) } }
246
+ result = { data: send_result['items'], type: :object_list }
272
247
  formatter.display_item_count(send_result['item_count'], send_result['total_count'])
273
248
  else # 'file','symbolic_link'
274
249
  result = { data: send_result['self'], type: :single_object}
275
- # result={ data: [send_result['self']] , type: :object_list, textify: lambda { |table_data| c_textify_browse(table_data) } }
276
- # raise "unknown type: #{send_result['self']['type']}"
277
250
  end
278
251
  return c_result_remove_prefix_path(result, 'path', prefix_path)
279
252
  when :sync
280
- node_sync = SyncSpecGen3.new(@api_node)
281
- return Plugins::Sync.new(@agents, sync_spec: node_sync).execute_action
253
+ return execute_sync_action do |sync_direction, local_path, remote_path|
254
+ # Gen3 API
255
+ # empty transfer spec for authorization request
256
+ request_transfer_spec = {
257
+ type: case sync_direction
258
+ when :push then :sync_upload
259
+ when :pull then :sync_download
260
+ when :bidi then :sync
261
+ end,
262
+ paths: [{
263
+ source: remote_path,
264
+ destination: local_path
265
+ }]
266
+ }
267
+ # add fixed parameters if any (for COS)
268
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
269
+ # prepare payload for single request
270
+ setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
271
+ # only one request, so only one answer
272
+ transfer_spec = @api_node.create('files/sync_setup', setup_payload)[:data]['transfer_specs'].first['transfer_spec']
273
+ # API returns null tag... but async does not like it
274
+ transfer_spec.delete_if{ |_k, v| v.nil? }
275
+ # delete this part, as the returned value contains only destination, and not sources
276
+ # transfer_spec.delete('paths') if command.eql?(:upload)
277
+ Log.log.debug{Log.dump(:ts, transfer_spec)}
278
+ transfer_spec
279
+ end
282
280
  when :upload, :download
283
281
  # empty transfer spec for authorization request
284
282
  request_transfer_spec = {}
@@ -297,8 +295,16 @@ module Aspera
297
295
  # delete this part, as the returned value contains only destination, and not sources
298
296
  transfer_spec.delete('paths') if command.eql?(:upload)
299
297
  return Main.result_transfer(transfer.start(transfer_spec))
298
+ when :http_node_download
299
+ remote_path = get_next_arg_add_prefix(prefix_path, 'remote path')
300
+ file_name = File.basename(remote_path)
301
+ @api_node.call(
302
+ operation: 'GET',
303
+ subpath: "files/#{URI.encode_www_form_component(remote_path)}/contents",
304
+ save_to_file: File.join(transfer.destination_folder(Fasp::TransferSpec::DIRECTION_RECEIVE), file_name))
305
+ return Main.result_status("downloaded: #{file_name}")
300
306
  end
301
- raise 'INTERNAL ERROR'
307
+ error_unreachable_line
302
308
  end
303
309
 
304
310
  # common API to node and Shares
@@ -307,20 +313,37 @@ module Aspera
307
313
  case command
308
314
  when *COMMANDS_GEN3
309
315
  execute_command_gen3(command, prefix_path)
310
- when :access_key
311
- ak_command = options.get_next_command([:do].concat(Plugin::ALL_OPS))
316
+ when :access_keys
317
+ ak_command = options.get_next_command(%i[do set_bearer_key].concat(Plugin::ALL_OPS))
312
318
  case ak_command
313
- when *Plugin::ALL_OPS then return entity_command(ak_command, @api_node, 'access_keys', id_default: 'self')
319
+ when *Plugin::ALL_OPS
320
+ return entity_command(ak_command, @api_node, 'access_keys') do |field, value|
321
+ raise 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
322
+ @api_node.read('access_keys/self')[:data]['id']
323
+ end
314
324
  when :do
315
- access_key = options.get_next_argument('access key id')
316
- ak_info = @api_node.read("access_keys/#{access_key}")[:data]
317
- # change API credentials if different access key
318
- if !access_key.eql?('self')
319
- @api_node.params[:auth][:username] = ak_info['id']
320
- @api_node.params[:auth][:password] = config.lookup_secret(url: @api_node.params[:base_url], username: ak_info['id'], mandatory: true)
325
+ access_key_id = options.get_next_argument('access key id')
326
+ root_file_id = options.get_option(:root_id)
327
+ if root_file_id.nil?
328
+ ak_info = @api_node.read("access_keys/#{access_key_id}")[:data]
329
+ # change API credentials if different access key
330
+ if !access_key_id.eql?('self')
331
+ @api_node.params[:auth][:username] = ak_info['id']
332
+ @api_node.params[:auth][:password] = config.lookup_secret(url: @api_node.params[:base_url], username: ak_info['id'], mandatory: true)
333
+ end
334
+ root_file_id = ak_info['root_file_id']
321
335
  end
322
336
  command_repo = options.get_next_command(COMMANDS_GEN4)
323
- return execute_command_gen4(command_repo, ak_info['root_file_id'])
337
+ return execute_command_gen4(command_repo, root_file_id)
338
+ when :set_bearer_key
339
+ access_key_id = options.get_next_argument('access key id')
340
+ access_key_id = @api_node.read('access_keys/self')[:data]['id'] if access_key_id.eql?('self')
341
+ bearer_key_pem = options.get_next_argument('public or private RSA key PEM value', type: String)
342
+ key = OpenSSL::PKey.read(bearer_key_pem)
343
+ key = key.public_key if key.private?
344
+ bearer_key_pem = key.to_pem
345
+ @api_node.update("access_keys/#{access_key_id}", {token_verification_key: bearer_key_pem})
346
+ return Main.result_status('public key updated')
324
347
  end
325
348
  when :health
326
349
  nagios = Nagios.new
@@ -345,10 +368,13 @@ module Aspera
345
368
  return nagios.result
346
369
  when :events
347
370
  events = @api_node.read('events', query_read_delete)[:data]
348
- return { type: :object_list, data: events}
371
+ return { type: :object_list, data: events, fields: ->(f){!f.start_with?('data')} }
349
372
  when :info
350
373
  nd_info = @api_node.read('info')[:data]
351
- return { type: :single_object, data: nd_info, textify: lambda { |table_data| c_textify_bool_list_result(table_data, %w[capabilities settings])}}
374
+ return { type: :single_object, data: nd_info}
375
+ when :slash
376
+ nd_info = @api_node.read('')[:data]
377
+ return { type: :single_object, data: nd_info}
352
378
  when :license
353
379
  # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
354
380
  node_license = @api_node.read('license')[:data]
@@ -364,7 +390,7 @@ module Aspera
364
390
  # @return [Hash] api and main file id for given path or id
365
391
  # Allows to specify a file by its path or by its id on the node
366
392
  def apifid_from_next_arg(top_file_id)
367
- file_path = instance_identifier(description: 'path or id') do |attribute, value|
393
+ file_path = instance_identifier(description: 'path or %id:<id>') do |attribute, value|
368
394
  raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
369
395
  # directly return result for method
370
396
  return {api: @api_node, file_id: value}
@@ -380,14 +406,14 @@ module Aspera
380
406
  command_legacy = options.get_next_command(V3_IN_V4_ACTIONS)
381
407
  # TODO: shall we support all methods here ? what if there is a link ?
382
408
  apifid = @api_node.resolve_api_fid(top_file_id, '')
383
- return Node.new(@agents.merge(skip_basic_auth_options: true, skip_node_options: true, node_api: apifid[:api])).execute_action(command_legacy)
409
+ return Node.new(@agents, api: apifid[:api]).execute_action(command_legacy)
384
410
  when :node_info, :bearer_token_node
385
411
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
386
412
  result = {
387
413
  url: apifid[:api].params[:base_url],
388
414
  root_id: apifid[:file_id]
389
415
  }
390
- raise 'No auth for node' if apifid[:api].params[:auth].nil?
416
+ assert_values(apifid[:api].params[:auth][:type], %i[basic oauth2])
391
417
  case apifid[:api].params[:auth][:type]
392
418
  when :basic
393
419
  result[:username] = apifid[:api].params[:auth][:username]
@@ -395,10 +421,11 @@ module Aspera
395
421
  when :oauth2
396
422
  result[:username] = apifid[:api].params[:headers][Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY]
397
423
  result[:password] = apifid[:api].oauth_token
398
- else raise 'unknown'
424
+ else error_unreachable_line
399
425
  end
400
426
  return {type: :single_object, data: result} if command_repo.eql?(:node_info)
401
- raise 'not bearer token' unless result[:password].start_with?('Bearer ')
427
+ # check format of bearer token
428
+ Oauth.bearer_extract(result[:password])
402
429
  return Main.result_status(result[:password])
403
430
  when :browse
404
431
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
@@ -413,7 +440,7 @@ module Aspera
413
440
  return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
414
441
  when :find
415
442
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
416
- test_block = Aspera::Node.file_matcher(value_or_query(allowed_types: String))
443
+ test_block = Aspera::Node.file_matcher_from_argument(options)
417
444
  return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
418
445
  when :mkdir
419
446
  containing_folder_path = options.get_next_argument('path').split(Aspera::Node::PATH_SEPARATOR)
@@ -428,15 +455,27 @@ module Aspera
428
455
  result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})[:data]
429
456
  return Main.result_status("renamed to #{newname}")
430
457
  when :delete
431
- return do_bulk_operation(options.get_next_argument('path'), 'deleted', id_result: 'path') do |l_path|
432
- raise "expecting String (path), got #{l_path.class.name} (#{l_path})" unless l_path.is_a?(String)
458
+ return do_bulk_operation(command: command_repo, descr: 'path', values: String, id_result: 'path') do |l_path|
433
459
  apifid = @api_node.resolve_api_fid(top_file_id, l_path)
434
460
  result = apifid[:api].delete("files/#{apifid[:file_id]}")[:data]
435
461
  {'path' => l_path}
436
462
  end
437
463
  when :sync
438
- node_sync = SyncSpecGen4.new(@api_node, top_file_id)
439
- return Plugins::Sync.new(@agents, sync_spec: node_sync).execute_action
464
+ return execute_sync_action do |sync_direction, _local_path, remote_path|
465
+ # Gen4 API
466
+ # direction is push pull, bidi
467
+ assert_values(sync_direction, %i[push pull bidi])
468
+ ts_direction = case sync_direction
469
+ when :push, :bidi then Fasp::TransferSpec::DIRECTION_SEND
470
+ when :pull then Fasp::TransferSpec::DIRECTION_RECEIVE
471
+ else error_unreachable_line
472
+ end
473
+ # remote is specified by option to_folder
474
+ apifid = @api_node.resolve_api_fid(top_file_id, remote_path)
475
+ transfer_spec = apifid[:api].transfer_spec_gen4(apifid[:file_id], ts_direction)
476
+ Log.log.debug{Log.dump(:ts, transfer_spec)}
477
+ transfer_spec
478
+ end
440
479
  when :upload
441
480
  apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND))
442
481
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Fasp::TransferSpec::DIRECTION_SEND)))
@@ -475,7 +514,7 @@ module Aspera
475
514
  source_paths = [{'source' => source_folder.pop}]
476
515
  source_folder = source_folder.join(Aspera::Node::PATH_SEPARATOR)
477
516
  end
478
- raise CliBadArgument, 'one file at a time only in HTTP mode' if source_paths.length > 1
517
+ raise Cli::BadArgument, 'one file at a time only in HTTP mode' if source_paths.length > 1
479
518
  file_name = source_paths.first['source']
480
519
  apifid = @api_node.resolve_api_fid(top_file_id, File.join(source_folder, file_name))
481
520
  apifid[:api].call(
@@ -499,8 +538,7 @@ module Aspera
499
538
  subpath: "files/#{apifid[:file_id]}/preview",
500
539
  headers: {'Accept' => 'image/png'}
501
540
  )
502
- require 'aspera/preview/terminal'
503
- return Main.result_status(Preview::Terminal.build(result[:http].body, reserved_lines: 3))
541
+ return Main.result_picture_in_terminal(options, result[:http].body)
504
542
  when :permission
505
543
  apifid = apifid_from_next_arg(top_file_id)
506
544
  command_perm = options.get_next_command(%i[list create delete])
@@ -514,10 +552,8 @@ module Aspera
514
552
  items = apifid[:api].read('permissions', list_options)[:data]
515
553
  return {type: :object_list, data: items}
516
554
  when :delete
517
- perm_id = instance_identifier
518
- return do_bulk_operation(perm_id, 'deleted') do |one_id|
519
- # TODO: notify event ?
520
- apifid[:api].delete("permissions/#{perm_id}")
555
+ return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
556
+ apifid[:api].delete("permissions/#{one_id}")
521
557
  # notify application of deletion
522
558
  the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app, types: ['permission.deleted']) unless the_app.nil?
523
559
  {'id' => one_id}
@@ -535,11 +571,11 @@ module Aspera
535
571
  # notify application of creation
536
572
  the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app) unless the_app.nil?
537
573
  return { type: :single_object, data: created_data}
538
- else raise "internal error:shall not reach here (#{command_perm})"
574
+ else error_unreachable_line
539
575
  end
540
- else raise "INTERNAL ERROR: no case for #{command_repo}"
576
+ else error_unreachable_line
541
577
  end # command_repo
542
- # raise 'INTERNAL ERROR: missing return'
578
+ error_unreachable_line
543
579
  end # execute_command_gen4
544
580
 
545
581
  # This is older API
@@ -549,7 +585,7 @@ module Aspera
549
585
  async_name = options.get_option(:sync_name)
550
586
  if async_name.nil?
551
587
  async_id = instance_identifier
552
- if async_id.eql?(VAL_ALL) && %i[show delete].include?(command)
588
+ if async_id.eql?(ExtendedValue::ALL) && %i[show delete].include?(command)
553
589
  async_ids = @api_node.read('async/list')[:data]['sync_ids']
554
590
  else
555
591
  Integer(async_id) # must be integer
@@ -572,7 +608,7 @@ module Aspera
572
608
  when :show
573
609
  resp = @api_node.create('async/summary', post_data)[:data]['sync_summaries']
574
610
  return Main.result_empty if resp.empty?
575
- return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(VAL_ALL)
611
+ return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(ExtendedValue::ALL)
576
612
  return { type: :single_object, data: resp.first }
577
613
  when :delete
578
614
  resp = @api_node.create('async/delete', post_data)[:data]
@@ -589,7 +625,7 @@ module Aspera
589
625
  # filename str
590
626
  # skip int
591
627
  # status int
592
- filter = value_or_query(allowed_types: Hash)
628
+ filter = query_option
593
629
  post_data.merge!(filter) unless filter.nil?
594
630
  resp = @api_node.create('async/files', post_data)[:data]
595
631
  data = resp['sync_files']
@@ -620,6 +656,20 @@ module Aspera
620
656
  end
621
657
  end
622
658
 
659
+ # @return [Integer] id of the sync
660
+ # @raise [Cli::BadArgument] if no such sync, or not by name
661
+ # @param [String] field name of the field to search
662
+ # @param [String] value value of the field to search
663
+ def ssync_lookup(field, value)
664
+ raise Cli::BadArgument, "Only search by name is supported (#{field})" unless field.eql?('name')
665
+ @api_node.read('asyncs')[:data]['ids'].each do |id|
666
+ sync_info = @api_node.read("asyncs/#{id}")[:data]['configuration']
667
+ # name is unique, so we can return
668
+ return id if sync_info[field].eql?(value)
669
+ end
670
+ raise Cli::BadArgument, "no such sync: #{field}=#{value}"
671
+ end
672
+
623
673
  ACTIONS = %i[
624
674
  async
625
675
  ssync
@@ -629,7 +679,9 @@ module Aspera
629
679
  watch_folder
630
680
  central
631
681
  asperabrowser
632
- basic_token].concat(COMMON_ACTIONS).freeze
682
+ basic_token
683
+ bearer_token
684
+ simulator].concat(COMMON_ACTIONS).freeze
633
685
 
634
686
  def execute_action(command=nil, prefix_path=nil)
635
687
  command ||= options.get_next_command(ACTIONS)
@@ -640,17 +692,15 @@ module Aspera
640
692
  # newer API
641
693
  sync_command = options.get_next_command(%i[start stop bandwidth counters files state summary].concat(Plugin::ALL_OPS) - %i[modify])
642
694
  case sync_command
643
- when *Plugin::ALL_OPS then return entity_command(sync_command, @api_node, 'asyncs', item_list_key: 'ids')
695
+ when *Plugin::ALL_OPS then return entity_command(sync_command, @api_node, 'asyncs', item_list_key: 'ids'){|field, value|ssync_lookup(field, value)}
644
696
  else
645
- asyncs_id = instance_identifier
697
+ asyncs_id = instance_identifier {|field, value|ssync_lookup(field, value)}
646
698
  parameters = nil
647
699
  if %i[start stop].include?(sync_command)
648
700
  @api_node.create("asyncs/#{asyncs_id}/#{sync_command}", parameters)
649
701
  return Main.result_status('Done')
650
702
  end
651
- if %i[bandwidth counters files].include?(sync_command)
652
- parameters = value_or_query(allowed_types: Hash, mandatory: false) || {}
653
- end
703
+ parameters = query_option(default: {}) if %i[bandwidth counters files].include?(sync_command)
654
704
  return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters)[:data] }
655
705
  end
656
706
  when :stream
@@ -660,13 +710,13 @@ module Aspera
660
710
  resp = @api_node.read('ops/transfers', old_query_read_delete)
661
711
  return { type: :object_list, data: resp[:data], fields: %w[id status] } # TODO: useful?
662
712
  when :create
663
- resp = @api_node.create('streams', value_create_modify(command: command, type: Hash))
713
+ resp = @api_node.create('streams', value_create_modify(command: command))
664
714
  return { type: :single_object, data: resp[:data] }
665
715
  when :show
666
716
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
667
717
  return { type: :other_struct, data: resp[:data] }
668
718
  when :modify
669
- resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command, type: Hash))
719
+ resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
670
720
  return { type: :other_struct, data: resp[:data] }
671
721
  when :cancel
672
722
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
@@ -675,7 +725,7 @@ module Aspera
675
725
  raise 'error'
676
726
  end
677
727
  when :transfer
678
- command = options.get_next_command(%i[list cancel show modify bandwidth_average])
728
+ command = options.get_next_command(%i[list cancel show modify bandwidth_average sessions])
679
729
  res_class_path = 'ops/transfers'
680
730
  if %i[cancel show modify].include?(command)
681
731
  one_res_id = instance_identifier
@@ -683,15 +733,24 @@ module Aspera
683
733
  end
684
734
  case command
685
735
  when :list
686
- # could use ? subpath: 'transfers'
687
- query = query_read_delete
688
- raise 'Query must be a Hash' unless query.nil? || query.is_a?(Hash)
689
- transfers_data = @api_node.read(res_class_path, query)[:data]
736
+ transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
690
737
  return {
691
738
  type: :object_list,
692
739
  data: transfers_data,
693
740
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
694
741
  }
742
+ when :sessions
743
+ transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
744
+ sessions = transfers_data.map{|t|t['sessions']}.flatten
745
+ sessions.each do |session|
746
+ session['start_time'] = Time.at(session['start_time_usec'] / 1_000_000.0).utc.iso8601(0)
747
+ session['end_time'] = Time.at(session['end_time_usec'] / 1_000_000.0).utc.iso8601(0)
748
+ end
749
+ return {
750
+ type: :object_list,
751
+ data: sessions,
752
+ fields: %w[id status start_time end_time target_rate_kbps]
753
+ }
695
754
  when :cancel
696
755
  resp = @api_node.cancel(one_res_path)
697
756
  return { type: :other_struct, data: resp[:data] }
@@ -702,7 +761,7 @@ module Aspera
702
761
  resp = @api_node.update(one_res_path, options.get_next_argument('update value', type: Hash))
703
762
  return { type: :other_struct, data: resp[:data] }
704
763
  when :bandwidth_average
705
- transfers_data = @api_node.read(res_class_path, query)[:data]
764
+ transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
706
765
  # collect all key dates
707
766
  bandwidth_period = {}
708
767
  dir_info = %i[avg_kbps sessions].freeze
@@ -779,7 +838,7 @@ module Aspera
779
838
  @api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
780
839
  case command
781
840
  when :create
782
- resp = @api_node.create(res_class_path, value_create_modify(command: command, type: Hash))
841
+ resp = @api_node.create(res_class_path, value_create_modify(command: command))
783
842
  return Main.result_status("#{resp[:data]['id']} created")
784
843
  when :list
785
844
  resp = @api_node.read(res_class_path, old_query_read_delete)
@@ -787,7 +846,7 @@ module Aspera
787
846
  when :show
788
847
  return { type: :single_object, data: @api_node.read(one_res_path)[:data]}
789
848
  when :modify
790
- @api_node.update(one_res_path, value_or_query(mandatory: true, allowed_types: Hash))
849
+ @api_node.update(one_res_path, query_option(mandatory: true))
791
850
  return Main.result_status("#{one_res_id} updated")
792
851
  when :delete
793
852
  @api_node.delete(one_res_path)
@@ -799,7 +858,7 @@ module Aspera
799
858
  command = options.get_next_command(%i[session file])
800
859
  validator_id = options.get_option(:validator)
801
860
  validation = {'validator_id' => validator_id} unless validator_id.nil?
802
- request_data = value_create_modify(default: {}, type: Hash)
861
+ request_data = query_option(default: {})
803
862
  case command
804
863
  when :session
805
864
  command = options.get_next_command([:list])
@@ -820,7 +879,7 @@ module Aspera
820
879
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
821
880
  resp = @api_node.create('services/rest/transfers/v1/files', request_data)[:data]
822
881
  resp = JSON.parse(resp) if resp.is_a?(String)
823
- Log.dump(:resp, resp)
882
+ Log.log.debug{Log.dump(:resp, resp)}
824
883
  return { type: :object_list, data: resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path]}
825
884
  when :modify
826
885
  request_data.deep_merge!(validation) unless validation.nil?
@@ -839,7 +898,27 @@ module Aspera
839
898
  OpenApplication.instance.uri(options.get_option(:asperabrowserurl) + '?goto=' + encoded_params)
840
899
  return Main.result_status('done')
841
900
  when :basic_token
842
- return Main.result_status(Rest.basic_creds(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
901
+ return Main.result_status(Rest.basic_token(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
902
+ when :bearer_token
903
+ private_key = OpenSSL::PKey::RSA.new(options.get_next_argument('private RSA key PEM value', type: String))
904
+ token_info = options.get_next_argument('user and group identification', type: Hash)
905
+ access_key = options.get_option(:username, mandatory: true)
906
+ return Main.result_status(Aspera::Node.bearer_token(payload: token_info, access_key: access_key, private_key: private_key))
907
+ when :simulator
908
+ require 'aspera/node_simulator'
909
+ parameters = value_create_modify(command: command)
910
+ parameters = parameters.symbolize_keys
911
+ raise 'Missing key: url' unless parameters.key?(:url)
912
+ uri = URI.parse(parameters[:url])
913
+ server = WebServerSimple.new(uri, certificate: parameters[:certificate])
914
+ server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], transfer)
915
+ # on ctrl-c, tell server main loop to exit
916
+ trap('INT') { server.shutdown }
917
+ formatter.display_status("Node Simulator listening on #{uri.port}")
918
+ Log.log.info{"Listening on #{uri.port}"}
919
+ # this is blocking until server exits
920
+ server.start
921
+ return Main.result_status('Simulator terminated')
843
922
  end # case command
844
923
  raise 'ERROR: shall not reach this line'
845
924
  end # execute_action