aspera-cli 4.14.0 → 4.15.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 (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