aspera-cli 4.13.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 (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