aspera-cli 4.14.0 → 4.15.0

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