aspera-cli 4.13.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
@@ -1,97 +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].add_opt_simple(:validator, 'identifier of validator (optional for central)')
76
- env[:options].add_opt_simple(:asperabrowserurl, 'URL for simple aspera web ui')
77
- env[:options].add_opt_simple(:sync_name, 'sync name')
78
- env[:options].add_opt_simple(:path, 'file or folder path for gen4 operation "file"')
79
- env[:options].add_opt_list(:token_type, %i[aspera basic hybrid], 'type of token used for transfers')
80
- env[:options].add_opt_boolean(:default_ports, 'use standard FASP ports or get from node api (gen4)')
81
- env[:options].set_option(:asperabrowserurl, 'https://asperabrowser.mybluemix.net')
82
- env[:options].set_option(:token_type, :aspera)
83
- env[:options].set_option(:default_ports, :yes)
84
- env[:options].parse_options!
85
- 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!
86
79
  end
87
80
  end
88
81
 
89
82
  # spellchecker: disable
90
83
  # SOAP API call to test central API
91
- CENTRAL_SOAP_API_TEST = '<?xml version="1.0" encoding="UTF-8"?>'\
92
- '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:typ="urn:Aspera:XML:FASPSessionNET:2009/11:Types">'\
93
- '<soapenv:Header></soapenv:Header>'\
94
- '<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>' \
95
88
  '</soapenv:Envelope>'
96
89
  # spellchecker: enable
97
90
 
@@ -99,17 +92,17 @@ module Aspera
99
92
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
100
93
 
101
94
  # actions in execute_command_gen3
102
- 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]
103
96
 
104
97
  BASE_ACTIONS = %i[api_details].concat(COMMANDS_GEN3).freeze
105
98
 
106
- SPECIAL_ACTIONS = %i[health events info license].freeze
99
+ SPECIAL_ACTIONS = %i[health events info slash license].freeze
107
100
 
108
101
  # actions available in v3 in gen4
109
- 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
110
103
 
111
104
  # actions used commonly when a node is involved
112
- 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
113
106
 
114
107
  private_constant(*%i[CENTRAL_SOAP_API_TEST SEARCH_REMOVE_FIELDS BASE_ACTIONS SPECIAL_ACTIONS V3_IN_V4_ACTIONS COMMON_ACTIONS])
115
108
 
@@ -117,61 +110,37 @@ module Aspera
117
110
  NODE4_READ_ACTIONS = %i[bearer_token_node node_info browse find].freeze
118
111
 
119
112
  # commands for execute_command_gen4
120
- COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download file v3].concat(NODE4_READ_ACTIONS).freeze
113
+ COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
121
114
 
122
- 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
123
116
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
124
117
  COMMANDS_FASPEX = COMMON_ACTIONS
125
118
 
126
- def initialize(env)
119
+ def initialize(env, api: nil)
127
120
  super(env)
128
- self.class.register_node_options(env) unless env[:skip_node_options]
129
- return if env[:man_only]
121
+ Node.declare_options(options, force: env[:all_manuals])
130
122
  @api_node =
131
- if env.key?(:node_api)
123
+ if !api.nil? || env[:all_manuals]
132
124
  # this can be Aspera::Node or Aspera::Rest (shares)
133
- env[:node_api]
134
- elsif options.get_option(:password, is_type: :mandatory).start_with?('Bearer ')
125
+ api
126
+ elsif Oauth.bearer?(options.get_option(:password, mandatory: true))
135
127
  # info is provided like node_info of aoc
136
128
  Aspera::Node.new(params: {
137
- base_url: options.get_option(:url, is_type: :mandatory),
138
- headers: {
139
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options.get_option(:username, is_type: :mandatory),
140
- 'Authorization' => options.get_option(:password, is_type: :mandatory)
141
- }
129
+ base_url: options.get_option(:url, mandatory: true),
130
+ headers: Aspera::Node.bearer_headers(options.get_option(:password, mandatory: true))
142
131
  })
143
132
  else
144
133
  # this is normal case
145
134
  Aspera::Node.new(params: {
146
- base_url: options.get_option(:url, is_type: :mandatory),
135
+ base_url: options.get_option(:url, mandatory: true),
147
136
  auth: {
148
137
  type: :basic,
149
- username: options.get_option(:username, is_type: :mandatory),
150
- password: options.get_option(:password, is_type: :mandatory)
138
+ username: options.get_option(:username, mandatory: true),
139
+ password: options.get_option(:password, mandatory: true)
151
140
  }})
152
141
  end
153
142
  end
154
143
 
155
- def c_textify_browse(table_data)
156
- return table_data.map do |i|
157
- i['permissions'] = i['permissions'].map { |x| x['name'] }.join(',')
158
- i
159
- end
160
- end
161
-
162
- # key/value is defined in main in hash_table
163
- def c_textify_bool_list_result(list, name_list)
164
- list.each_index do |i|
165
- next unless name_list.include?(list[i]['key'])
166
- list[i]['value'].each do |item|
167
- list.push({'key' => item['name'], 'value' => item['value']})
168
- end
169
- list.delete_at(i)
170
- # continue at same index because we delete current one
171
- redo
172
- end
173
- end
174
-
175
144
  # reduce the path from a result on given named column
176
145
  def c_result_remove_prefix_path(result, column, path_prefix)
177
146
  if !path_prefix.nil?
@@ -227,7 +196,7 @@ module Aspera
227
196
  when :search
228
197
  search_root = get_next_arg_add_prefix(prefix_path, 'search root')
229
198
  parameters = {'path' => search_root}
230
- other_options = options.get_option(:value)
199
+ other_options = query_option
231
200
  parameters.merge!(other_options) unless other_options.nil?
232
201
  resp = @api_node.create('files/search', parameters)
233
202
  result = { type: :object_list, data: resp[:data]['items']}
@@ -273,53 +242,66 @@ module Aspera
273
242
  # if there is no items
274
243
  case send_result['self']['type']
275
244
  when 'directory', 'container' # directory: node, container: shares
276
- 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 }
277
246
  formatter.display_item_count(send_result['item_count'], send_result['total_count'])
278
247
  else # 'file','symbolic_link'
279
248
  result = { data: send_result['self'], type: :single_object}
280
- # result={ data: [send_result['self']] , type: :object_list, textify: lambda { |table_data| c_textify_browse(table_data) } }
281
- # raise "unknown type: #{send_result['self']['type']}"
282
249
  end
283
250
  return c_result_remove_prefix_path(result, 'path', prefix_path)
284
251
  when :sync
285
- node_sync = SyncSpecGen3.new(@api_node)
286
- return Plugins::Sync.new(@agents, sync_spec: node_sync).execute_action
287
- when :upload, :download
288
- token_type = options.get_option(:token_type)
289
- # nil if Shares 1.x
290
- token_type = :aspera if token_type.nil?
291
- case token_type
292
- when :aspera, :hybrid
252
+ return execute_sync_action do |sync_direction, local_path, remote_path|
253
+ # Gen3 API
293
254
  # empty transfer spec for authorization request
294
- request_transfer_spec = {}
295
- # set requested paths depending on direction
296
- request_transfer_spec[:paths] = if command.eql?(:download)
297
- transfer.ts_source_paths
298
- else
299
- [{ destination: transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND) }]
300
- end
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
+ }
301
266
  # add fixed parameters if any (for COS)
302
267
  @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
303
268
  # prepare payload for single request
304
269
  setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
305
270
  # only one request, so only one answer
306
- transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
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? }
307
274
  # delete this part, as the returned value contains only destination, and not sources
308
- transfer_spec.delete('paths') if command.eql?(:upload)
309
- when :basic
310
- raise 'shall have auth' unless @api_node.params[:auth].is_a?(Hash)
311
- raise 'shall be basic auth' unless @api_node.params[:auth][:type].eql?(:basic)
312
- transfer_spec = {}.merge(Aspera::Fasp::TransferSpec::AK_TSPEC_BASE)
313
- transfer_spec['remote_host'] = URI.parse(@api_node.params[:base_url]).host
314
- Fasp::TransferSpec.action_to_direction(transfer_spec, command)
315
- transfer_spec['destination_root'] = transfer.destination_folder(transfer_spec['direction'])
316
- @api_node.add_tspec_info(transfer_spec) if @api_node.respond_to?(:add_tspec_info)
317
- else raise "ERROR: token_type #{tt}"
275
+ # transfer_spec.delete('paths') if command.eql?(:upload)
276
+ Log.log.debug{Log.dump(:ts, transfer_spec)}
277
+ transfer_spec
318
278
  end
319
- if %i[basic hybrid].include?(token_type)
320
- @api_node.ts_basic_token(transfer_spec)
279
+ when :upload, :download
280
+ # empty transfer spec for authorization request
281
+ request_transfer_spec = {}
282
+ # set requested paths depending on direction
283
+ request_transfer_spec[:paths] = if command.eql?(:download)
284
+ transfer.ts_source_paths
285
+ else
286
+ [{ destination: transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND) }]
321
287
  end
288
+ # add fixed parameters if any (for COS)
289
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
290
+ # prepare payload for single request
291
+ setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
292
+ # only one request, so only one answer
293
+ transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
294
+ # delete this part, as the returned value contains only destination, and not sources
295
+ transfer_spec.delete('paths') if command.eql?(:upload)
322
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}")
323
305
  end
324
306
  raise 'INTERNAL ERROR'
325
307
  end
@@ -330,20 +312,37 @@ module Aspera
330
312
  case command
331
313
  when *COMMANDS_GEN3
332
314
  execute_command_gen3(command, prefix_path)
333
- when :access_key
334
- 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))
335
317
  case ak_command
336
- 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
337
323
  when :do
338
- access_key = options.get_next_argument('access key id')
339
- ak_info = @api_node.read("access_keys/#{access_key}")[:data]
340
- # change API credentials if different access key
341
- if !access_key.eql?('self')
342
- @api_node.params[:auth][:username] = ak_info['id']
343
- @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']
344
334
  end
345
335
  command_repo = options.get_next_command(COMMANDS_GEN4)
346
- 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')
347
346
  end
348
347
  when :health
349
348
  nagios = Nagios.new
@@ -367,11 +366,14 @@ module Aspera
367
366
  end
368
367
  return nagios.result
369
368
  when :events
370
- events = @api_node.read('events', options.get_option(:value))[:data]
371
- return { type: :object_list, data: events}
369
+ events = @api_node.read('events', query_read_delete)[:data]
370
+ return { type: :object_list, data: events, fields: ->(f){!f.start_with?('data')} }
372
371
  when :info
373
372
  nd_info = @api_node.read('info')[:data]
374
- 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}
375
377
  when :license
376
378
  # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
377
379
  node_license = @api_node.read('license')[:data]
@@ -383,66 +385,17 @@ module Aspera
383
385
  return { type: :single_object, data: @api_node.params }
384
386
  end
385
387
  end
386
- GEN4_FILE_COMMANDS = %i[show modify permission thumbnail].freeze
387
- def execute_node_gen4_file_command(command_node_file, top_file_id)
388
- file_path = options.get_option(:path)
389
- apifid =
390
- if file_path.nil?
391
- {api: @api_node, file_id: instance_identifier}
392
- else
393
- @api_node.resolve_api_fid(top_file_id, file_path) # TODO: allow follow link ?
394
- end
395
- case command_node_file
396
- when :show
397
- items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
398
- return {type: :single_object, data: items}
399
- when :modify
400
- update_param = options.get_next_argument('update data', type: Hash)
401
- apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
402
- return Main.result_status('Done')
403
- when :thumbnail
404
- result = apifid[:api].call(
405
- operation: 'GET',
406
- subpath: "files/#{apifid[:file_id]}/preview",
407
- headers: {'Accept' => 'image/png'}
408
- )
409
- require 'aspera/preview/terminal'
410
- return Main.result_status(Preview::Terminal.build(result[:http].body, reserved_lines: 3))
411
- when :permission
412
- command_perm = options.get_next_command(%i[list create delete])
413
- case command_perm
414
- when :list
415
- # generic options : TODO: as arg ? option_url_query
416
- list_options ||= {'include' => [Rest::ARRAY_PARAMS, 'access_level', 'permission_count']}
417
- # add which one to get
418
- list_options['file_id'] = apifid[:file_id]
419
- list_options['inherited'] ||= false
420
- items = apifid[:api].read('permissions', list_options)[:data]
421
- return {type: :object_list, data: items}
422
- when :delete
423
- perm_id = instance_identifier
424
- return do_bulk_operation(perm_id, 'deleted') do |one_id|
425
- # TODO: notify event ?
426
- apifid[:api].delete("permissions/#{perm_id}")
427
- {'id' => one_id}
428
- end
429
- when :create
430
- create_param = options.get_next_argument('creation data', type: Hash)
431
- raise 'no file_id' if create_param.key?('file_id')
432
- create_param['file_id'] = apifid[:file_id]
433
- create_param['access_levels'] = Aspera::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
434
- # add application specific tags (AoC)
435
- the_app = apifid[:api].app_info
436
- the_app[:api].permissions_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
437
- # create permission
438
- created_data = apifid[:api].create('permissions', create_param)[:data]
439
- # notify application of creation
440
- the_app[:api].permissions_create_event(created_data: created_data, app_info: the_app) unless the_app.nil?
441
- return { type: :single_object, data: created_data}
442
- else raise "internal error:shall not reach here (#{command_perm})"
443
- end
444
- else raise "internal error:shall not reach here (#{command_node_file})"
388
+
389
+ # @return [Hash] api and main file id for given path or id
390
+ # Allows to specify a file by its path or by its id on the node
391
+ def apifid_from_next_arg(top_file_id)
392
+ file_path = instance_identifier(description: 'path or %id:<id>') do |attribute, value|
393
+ raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
394
+ # directly return result for method
395
+ return {api: @api_node, file_id: value}
445
396
  end
397
+ # there was no selector, so it is a path
398
+ return @api_node.resolve_api_fid(top_file_id, file_path)
446
399
  end
447
400
 
448
401
  def execute_command_gen4(command_repo, top_file_id)
@@ -452,7 +405,7 @@ module Aspera
452
405
  command_legacy = options.get_next_command(V3_IN_V4_ACTIONS)
453
406
  # TODO: shall we support all methods here ? what if there is a link ?
454
407
  apifid = @api_node.resolve_api_fid(top_file_id, '')
455
- 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)
456
409
  when :node_info, :bearer_token_node
457
410
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
458
411
  result = {
@@ -467,16 +420,17 @@ module Aspera
467
420
  when :oauth2
468
421
  result[:username] = apifid[:api].params[:headers][Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY]
469
422
  result[:password] = apifid[:api].oauth_token
470
- else raise 'unknown'
423
+ else raise 'internal error: unknown auth type'
471
424
  end
472
425
  return {type: :single_object, data: result} if command_repo.eql?(:node_info)
473
- raise 'not bearer token' unless result[:password].start_with?('Bearer ')
426
+ # check format of bearer token
427
+ Oauth.bearer_extract(result[:password])
474
428
  return Main.result_status(result[:password])
475
429
  when :browse
476
430
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
477
431
  file_info = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
478
432
  if file_info['type'].eql?('folder')
479
- result = apifid[:api].read("files/#{apifid[:file_id]}/files", options.get_option(:value))
433
+ result = apifid[:api].read("files/#{apifid[:file_id]}/files", old_query_read_delete)
480
434
  items = result[:data]
481
435
  formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
482
436
  else
@@ -485,7 +439,7 @@ module Aspera
485
439
  return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
486
440
  when :find
487
441
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
488
- test_block = Aspera::Node.file_matcher(options.get_option(:value))
442
+ test_block = Aspera::Node.file_matcher_from_argument(options)
489
443
  return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
490
444
  when :mkdir
491
445
  containing_folder_path = options.get_next_argument('path').split(Aspera::Node::PATH_SEPARATOR)
@@ -495,20 +449,31 @@ module Aspera
495
449
  return Main.result_status("created: #{result['name']} (id=#{result['id']})")
496
450
  when :rename
497
451
  file_path = options.get_next_argument('source path')
498
- newname = options.get_next_argument('new name')
499
452
  apifid = @api_node.resolve_api_fid(top_file_id, file_path)
453
+ newname = options.get_next_argument('new name')
500
454
  result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})[:data]
501
- return Main.result_status("renamed #{file_path} to #{newname}")
455
+ return Main.result_status("renamed to #{newname}")
502
456
  when :delete
503
- return do_bulk_operation(options.get_next_argument('path'), 'deleted', id_result: 'path') do |l_path|
504
- 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|
505
458
  apifid = @api_node.resolve_api_fid(top_file_id, l_path)
506
459
  result = apifid[:api].delete("files/#{apifid[:file_id]}")[:data]
507
460
  {'path' => l_path}
508
461
  end
509
462
  when :sync
510
- node_sync = SyncSpecGen4.new(@api_node, top_file_id)
511
- 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
512
477
  when :upload
513
478
  apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND))
514
479
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Fasp::TransferSpec::DIRECTION_SEND)))
@@ -547,7 +512,7 @@ module Aspera
547
512
  source_paths = [{'source' => source_folder.pop}]
548
513
  source_folder = source_folder.join(Aspera::Node::PATH_SEPARATOR)
549
514
  end
550
- 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
551
516
  file_name = source_paths.first['source']
552
517
  apifid = @api_node.resolve_api_fid(top_file_id, File.join(source_folder, file_name))
553
518
  apifid[:api].call(
@@ -555,9 +520,62 @@ module Aspera
555
520
  subpath: "files/#{apifid[:file_id]}/content",
556
521
  save_to_file: File.join(transfer.destination_folder(Fasp::TransferSpec::DIRECTION_RECEIVE), file_name))
557
522
  return Main.result_status("downloaded: #{file_name}")
558
- when :file
559
- command_node_file = options.get_next_command(GEN4_FILE_COMMANDS)
560
- return execute_node_gen4_file_command(command_node_file, top_file_id)
523
+ when :show
524
+ apifid = apifid_from_next_arg(top_file_id)
525
+ items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
526
+ return {type: :single_object, data: items}
527
+ when :modify
528
+ apifid = apifid_from_next_arg(top_file_id)
529
+ update_param = options.get_next_argument('update data', type: Hash)
530
+ apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
531
+ return Main.result_status('Done')
532
+ when :thumbnail
533
+ apifid = apifid_from_next_arg(top_file_id)
534
+ result = apifid[:api].call(
535
+ operation: 'GET',
536
+ subpath: "files/#{apifid[:file_id]}/preview",
537
+ headers: {'Accept' => 'image/png'}
538
+ )
539
+ require 'aspera/preview/terminal'
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))
545
+ when :permission
546
+ apifid = apifid_from_next_arg(top_file_id)
547
+ command_perm = options.get_next_command(%i[list create delete])
548
+ case command_perm
549
+ when :list
550
+ # generic options : TODO: as arg ? query_read_delete
551
+ list_options ||= {'include' => Rest.array_params(%w[access_level permission_count])}
552
+ # add which one to get
553
+ list_options['file_id'] = apifid[:file_id]
554
+ list_options['inherited'] ||= false
555
+ items = apifid[:api].read('permissions', list_options)[:data]
556
+ return {type: :object_list, data: items}
557
+ when :delete
558
+ return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
559
+ apifid[:api].delete("permissions/#{one_id}")
560
+ # notify application of deletion
561
+ the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app, types: ['permission.deleted']) unless the_app.nil?
562
+ {'id' => one_id}
563
+ end
564
+ when :create
565
+ create_param = options.get_next_argument('creation data', type: Hash)
566
+ raise 'no file_id' if create_param.key?('file_id')
567
+ create_param['file_id'] = apifid[:file_id]
568
+ create_param['access_levels'] = Aspera::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
569
+ # add application specific tags (AoC)
570
+ the_app = apifid[:api].app_info
571
+ the_app[:api].permissions_set_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
572
+ # create permission
573
+ created_data = apifid[:api].create('permissions', create_param)[:data]
574
+ # notify application of creation
575
+ the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app) unless the_app.nil?
576
+ return { type: :single_object, data: created_data}
577
+ else raise "internal error:shall not reach here (#{command_perm})"
578
+ end
561
579
  else raise "INTERNAL ERROR: no case for #{command_repo}"
562
580
  end # command_repo
563
581
  # raise 'INTERNAL ERROR: missing return'
@@ -570,7 +588,7 @@ module Aspera
570
588
  async_name = options.get_option(:sync_name)
571
589
  if async_name.nil?
572
590
  async_id = instance_identifier
573
- if async_id.eql?(VAL_ALL) && %i[show delete].include?(command)
591
+ if async_id.eql?(ExtendedValue::ALL) && %i[show delete].include?(command)
574
592
  async_ids = @api_node.read('async/list')[:data]['sync_ids']
575
593
  else
576
594
  Integer(async_id) # must be integer
@@ -593,7 +611,7 @@ module Aspera
593
611
  when :show
594
612
  resp = @api_node.create('async/summary', post_data)[:data]['sync_summaries']
595
613
  return Main.result_empty if resp.empty?
596
- 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)
597
615
  return { type: :single_object, data: resp.first }
598
616
  when :delete
599
617
  resp = @api_node.create('async/delete', post_data)[:data]
@@ -610,21 +628,21 @@ module Aspera
610
628
  # filename str
611
629
  # skip int
612
630
  # status int
613
- filter = options.get_option(:value)
631
+ filter = query_option
614
632
  post_data.merge!(filter) unless filter.nil?
615
633
  resp = @api_node.create('async/files', post_data)[:data]
616
634
  data = resp['sync_files']
617
635
  data = data.first[async_id] unless data.empty?
618
636
  iteration_data = []
619
637
  skip_ids_persistency = nil
620
- if options.get_option(:once_only, is_type: :mandatory)
638
+ if options.get_option(:once_only, mandatory: true)
621
639
  skip_ids_persistency = PersistencyActionOnce.new(
622
640
  manager: @agents[:persistency],
623
641
  data: iteration_data,
624
642
  id: IdGenerator.from_list([
625
643
  'sync_files',
626
- options.get_option(:url, is_type: :mandatory),
627
- options.get_option(:username, is_type: :mandatory),
644
+ options.get_option(:url, mandatory: true),
645
+ options.get_option(:username, mandatory: true),
628
646
  async_id]))
629
647
  unless iteration_data.first.nil?
630
648
  data.select!{|l| l['fnid'].to_i > iteration_data.first}
@@ -641,6 +659,20 @@ module Aspera
641
659
  end
642
660
  end
643
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
+
644
676
  ACTIONS = %i[
645
677
  async
646
678
  ssync
@@ -650,7 +682,8 @@ module Aspera
650
682
  watch_folder
651
683
  central
652
684
  asperabrowser
653
- basic_token].concat(COMMON_ACTIONS).freeze
685
+ basic_token
686
+ bearer_token].concat(COMMON_ACTIONS).freeze
654
687
 
655
688
  def execute_action(command=nil, prefix_path=nil)
656
689
  command ||= options.get_next_command(ACTIONS)
@@ -659,32 +692,33 @@ module Aspera
659
692
  when :async then return execute_async # former API
660
693
  when :ssync
661
694
  # newer API
662
- sync_command = options.get_next_command(%i[bandwidth counters files start state stop summary].concat(Plugin::ALL_OPS) - %i[modify])
695
+ sync_command = options.get_next_command(%i[start stop bandwidth counters files state summary].concat(Plugin::ALL_OPS) - %i[modify])
663
696
  case sync_command
664
- 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)}
665
698
  else
666
- parameters = options.get_option(:value)
667
- asyncs_id = instance_identifier
699
+ asyncs_id = instance_identifier {|field, value|ssync_lookup(field, value)}
700
+ parameters = nil
668
701
  if %i[start stop].include?(sync_command)
669
702
  @api_node.create("asyncs/#{asyncs_id}/#{sync_command}", parameters)
670
- return Main.result_status('ok')
703
+ return Main.result_status('Done')
671
704
  end
705
+ parameters = query_option(default: {}) if %i[bandwidth counters files].include?(sync_command)
672
706
  return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters)[:data] }
673
707
  end
674
708
  when :stream
675
709
  command = options.get_next_command(%i[list create show modify cancel])
676
710
  case command
677
711
  when :list
678
- resp = @api_node.read('ops/transfers', options.get_option(:value))
712
+ resp = @api_node.read('ops/transfers', old_query_read_delete)
679
713
  return { type: :object_list, data: resp[:data], fields: %w[id status] } # TODO: useful?
680
714
  when :create
681
- resp = @api_node.create('streams', options.get_option(:value, is_type: :mandatory))
715
+ resp = @api_node.create('streams', value_create_modify(command: command))
682
716
  return { type: :single_object, data: resp[:data] }
683
717
  when :show
684
718
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
685
719
  return { type: :other_struct, data: resp[:data] }
686
720
  when :modify
687
- resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", options.get_option(:value, is_type: :mandatory))
721
+ resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
688
722
  return { type: :other_struct, data: resp[:data] }
689
723
  when :cancel
690
724
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
@@ -693,29 +727,86 @@ module Aspera
693
727
  raise 'error'
694
728
  end
695
729
  when :transfer
696
- command = options.get_next_command(%i[list cancel show])
730
+ command = options.get_next_command(%i[list cancel show modify bandwidth_average sessions])
697
731
  res_class_path = 'ops/transfers'
698
- if %i[cancel show].include?(command)
732
+ if %i[cancel show modify].include?(command)
699
733
  one_res_id = instance_identifier
700
734
  one_res_path = "#{res_class_path}/#{one_res_id}"
701
735
  end
702
736
  case command
703
737
  when :list
704
- # could use ? subpath: 'transfers'
705
- query = options.get_option(:value) || options.get_option(:query)
706
- raise 'Query must be a Hash' unless query.nil? || query.is_a?(Hash)
707
- resp = @api_node.read(res_class_path, query)
738
+ transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
708
739
  return {
709
740
  type: :object_list,
710
- data: resp[:data],
741
+ data: transfers_data,
711
742
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
712
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
+ }
713
756
  when :cancel
714
757
  resp = @api_node.cancel(one_res_path)
715
758
  return { type: :other_struct, data: resp[:data] }
716
759
  when :show
717
760
  resp = @api_node.read(one_res_path)
718
761
  return { type: :other_struct, data: resp[:data] }
762
+ when :modify
763
+ resp = @api_node.update(one_res_path, options.get_next_argument('update value', type: Hash))
764
+ return { type: :other_struct, data: resp[:data] }
765
+ when :bandwidth_average
766
+ transfers_data = @api_node.read(res_class_path, query_read_delete)[:data]
767
+ # collect all key dates
768
+ bandwidth_period = {}
769
+ dir_info = %i[avg_kbps sessions].freeze
770
+ transfers_data.each do |transfer|
771
+ session = transfer
772
+ # transfer['sessions'].each do |session|
773
+ next if session['avg_rate_kbps'].zero?
774
+ bandwidth_period[session['start_time_usec']] = 0
775
+ bandwidth_period[session['end_time_usec']] = 0
776
+ # end
777
+ end
778
+ result = []
779
+ # all dates sorted numerically
780
+ all_dates = bandwidth_period.keys.sort
781
+ all_dates.each_with_index do |start_date, index|
782
+ end_date = all_dates[index + 1]
783
+ # do not process last one
784
+ break if end_date.nil?
785
+ # init data for this period
786
+ period_bandwidth = Fasp::TransferSpec::DIRECTION_ENUM_VALUES.map(&:to_sym).each_with_object({}) do |direction, h|
787
+ h[direction] = dir_info.each_with_object({}) do |k2, h2|
788
+ h2[k2] = 0
789
+ end
790
+ end
791
+ # find all transfers that were active at this time
792
+ transfers_data.each do |transfer|
793
+ session = transfer
794
+ # transfer['sessions'].each do |session|
795
+ # skip if not information for this period
796
+ next if session['avg_rate_kbps'].zero?
797
+ # skip if not in this period
798
+ next if session['start_time_usec'] >= end_date || session['end_time_usec'] <= start_date
799
+ info = period_bandwidth[transfer['start_spec']['direction'].to_sym]
800
+ info[:avg_kbps] += session['avg_rate_kbps']
801
+ info[:sessions] += 1
802
+ # end
803
+ end
804
+ next if Fasp::TransferSpec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? do |dir|
805
+ period_bandwidth[dir][:sessions].zero?
806
+ end
807
+ result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
808
+ end
809
+ return { type: :object_list, data: result }
719
810
  else
720
811
  raise 'error'
721
812
  end
@@ -749,15 +840,15 @@ module Aspera
749
840
  @api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
750
841
  case command
751
842
  when :create
752
- resp = @api_node.create(res_class_path, options.get_option(:value, is_type: :mandatory))
843
+ resp = @api_node.create(res_class_path, value_create_modify(command: command))
753
844
  return Main.result_status("#{resp[:data]['id']} created")
754
845
  when :list
755
- resp = @api_node.read(res_class_path, options.get_option(:value))
846
+ resp = @api_node.read(res_class_path, old_query_read_delete)
756
847
  return { type: :value_list, data: resp[:data]['ids'], name: 'id' }
757
848
  when :show
758
849
  return { type: :single_object, data: @api_node.read(one_res_path)[:data]}
759
850
  when :modify
760
- @api_node.update(one_res_path, options.get_option(:value, is_type: :mandatory))
851
+ @api_node.update(one_res_path, query_option(mandatory: true))
761
852
  return Main.result_status("#{one_res_id} updated")
762
853
  when :delete
763
854
  @api_node.delete(one_res_path)
@@ -769,8 +860,7 @@ module Aspera
769
860
  command = options.get_next_command(%i[session file])
770
861
  validator_id = options.get_option(:validator)
771
862
  validation = {'validator_id' => validator_id} unless validator_id.nil?
772
- request_data = options.get_option(:value)
773
- request_data ||= {}
863
+ request_data = query_option(default: {})
774
864
  case command
775
865
  when :session
776
866
  command = options.get_next_command([:list])
@@ -791,7 +881,7 @@ module Aspera
791
881
  request_data.deep_merge!({'validation' => validation}) unless validation.nil?
792
882
  resp = @api_node.create('services/rest/transfers/v1/files', request_data)[:data]
793
883
  resp = JSON.parse(resp) if resp.is_a?(String)
794
- Log.dump(:resp, resp)
884
+ Log.log.debug{Log.dump(:resp, resp)}
795
885
  return { type: :object_list, data: resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path]}
796
886
  when :modify
797
887
  request_data.deep_merge!(validation) unless validation.nil?
@@ -801,16 +891,21 @@ module Aspera
801
891
  end
802
892
  when :asperabrowser
803
893
  browse_params = {
804
- 'nodeUser' => options.get_option(:username, is_type: :mandatory),
805
- 'nodePW' => options.get_option(:password, is_type: :mandatory),
806
- 'nodeURL' => options.get_option(:url, is_type: :mandatory)
894
+ 'nodeUser' => options.get_option(:username, mandatory: true),
895
+ 'nodePW' => options.get_option(:password, mandatory: true),
896
+ 'nodeURL' => options.get_option(:url, mandatory: true)
807
897
  }
808
898
  # encode parameters so that it looks good in url
809
899
  encoded_params = Base64.strict_encode64(Zlib::Deflate.deflate(JSON.generate(browse_params))).gsub(/=+$/, '').tr('+/', '-_').reverse
810
900
  OpenApplication.instance.uri(options.get_option(:asperabrowserurl) + '?goto=' + encoded_params)
811
901
  return Main.result_status('done')
812
902
  when :basic_token
813
- return Main.result_status(Rest.basic_creds(options.get_option(:username, is_type: :mandatory), options.get_option(:password, is_type: :mandatory)))
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))
814
909
  end # case command
815
910
  raise 'ERROR: shall not reach this line'
816
911
  end # execute_action