aspera-cli 4.12.0 → 4.14.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +45 -5
  4. data/CONTRIBUTING.md +113 -22
  5. data/README.md +1289 -754
  6. data/bin/ascli +3 -3
  7. data/examples/dascli +1 -1
  8. data/examples/rubyc +24 -0
  9. data/lib/aspera/aoc.rb +63 -74
  10. data/lib/aspera/ascmd.rb +5 -3
  11. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  12. data/lib/aspera/cli/extended_value.rb +24 -37
  13. data/lib/aspera/cli/formatter.rb +23 -25
  14. data/lib/aspera/cli/info.rb +2 -4
  15. data/lib/aspera/cli/main.rb +27 -27
  16. data/lib/aspera/cli/manager.rb +143 -120
  17. data/lib/aspera/cli/plugin.rb +88 -43
  18. data/lib/aspera/cli/plugins/alee.rb +2 -2
  19. data/lib/aspera/cli/plugins/aoc.rb +235 -104
  20. data/lib/aspera/cli/plugins/ats.rb +16 -18
  21. data/lib/aspera/cli/plugins/bss.rb +3 -3
  22. data/lib/aspera/cli/plugins/config.rb +190 -373
  23. data/lib/aspera/cli/plugins/console.rb +4 -6
  24. data/lib/aspera/cli/plugins/cos.rb +12 -13
  25. data/lib/aspera/cli/plugins/faspex.rb +21 -21
  26. data/lib/aspera/cli/plugins/faspex5.rb +399 -150
  27. data/lib/aspera/cli/plugins/node.rb +260 -174
  28. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  29. data/lib/aspera/cli/plugins/preview.rb +40 -62
  30. data/lib/aspera/cli/plugins/server.rb +33 -16
  31. data/lib/aspera/cli/plugins/shares.rb +24 -33
  32. data/lib/aspera/cli/plugins/sync.rb +6 -6
  33. data/lib/aspera/cli/transfer_agent.rb +47 -30
  34. data/lib/aspera/cli/version.rb +2 -1
  35. data/lib/aspera/colors.rb +9 -7
  36. data/lib/aspera/command_line_builder.rb +2 -1
  37. data/lib/aspera/cos_node.rb +1 -1
  38. data/lib/aspera/data/6 +0 -0
  39. data/lib/aspera/environment.rb +7 -3
  40. data/lib/aspera/fasp/agent_connect.rb +6 -1
  41. data/lib/aspera/fasp/agent_direct.rb +17 -17
  42. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  43. data/lib/aspera/fasp/agent_node.rb +14 -4
  44. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  45. data/lib/aspera/fasp/error_info.rb +2 -0
  46. data/lib/aspera/fasp/installation.rb +19 -19
  47. data/lib/aspera/fasp/parameters.rb +29 -20
  48. data/lib/aspera/fasp/parameters.yaml +5 -2
  49. data/lib/aspera/fasp/resume_policy.rb +3 -3
  50. data/lib/aspera/fasp/transfer_spec.rb +8 -5
  51. data/lib/aspera/fasp/uri.rb +23 -21
  52. data/lib/aspera/faspex_gw.rb +1 -0
  53. data/lib/aspera/faspex_postproc.rb +3 -3
  54. data/lib/aspera/hash_ext.rb +12 -2
  55. data/lib/aspera/keychain/macos_security.rb +13 -13
  56. data/lib/aspera/log.rb +1 -0
  57. data/lib/aspera/node.rb +73 -84
  58. data/lib/aspera/oauth.rb +4 -3
  59. data/lib/aspera/persistency_action_once.rb +1 -1
  60. data/lib/aspera/preview/file_types.rb +8 -6
  61. data/lib/aspera/preview/generator.rb +23 -11
  62. data/lib/aspera/preview/options.rb +3 -2
  63. data/lib/aspera/preview/terminal.rb +80 -0
  64. data/lib/aspera/preview/utils.rb +11 -11
  65. data/lib/aspera/proxy_auto_config.js +2 -2
  66. data/lib/aspera/rest.rb +42 -4
  67. data/lib/aspera/rest_call_error.rb +3 -1
  68. data/lib/aspera/secret_hider.rb +10 -5
  69. data/lib/aspera/ssh.rb +1 -1
  70. data/lib/aspera/sync.rb +41 -33
  71. data/lib/aspera/web_server_simple.rb +22 -18
  72. data.tar.gz.sig +0 -0
  73. metadata +40 -48
  74. metadata.gz.sig +0 -0
  75. data/docs/test_env.conf +0 -179
  76. data/examples/aoc.rb +0 -30
  77. data/examples/faspex4.rb +0 -94
  78. data/examples/node.rb +0 -96
  79. data/examples/server.rb +0 -93
  80. data/lib/aspera/data/7 +0 -0
@@ -7,6 +7,7 @@ require 'aspera/hash_ext'
7
7
  require 'aspera/id_generator'
8
8
  require 'aspera/node'
9
9
  require 'aspera/aoc'
10
+ require 'aspera/sync'
10
11
  require 'aspera/fasp/transfer_spec'
11
12
  require 'base64'
12
13
  require 'zlib'
@@ -14,6 +15,51 @@ require 'zlib'
14
15
  module Aspera
15
16
  module Cli
16
17
  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
+
17
63
  class Node < Aspera::Cli::BasicAuthPlugin
18
64
  class << self
19
65
  def detect(base_url)
@@ -26,15 +72,10 @@ module Aspera
26
72
  end
27
73
 
28
74
  def register_node_options(env)
29
- env[:options].add_opt_simple(:validator, 'identifier of validator (optional for central)')
30
- env[:options].add_opt_simple(:asperabrowserurl, 'URL for simple aspera web ui')
31
- env[:options].add_opt_simple(:sync_name, 'sync name')
32
- env[:options].add_opt_simple(:path, 'file or folder path for gen4 operation "file"')
33
- env[:options].add_opt_list(:token_type, %i[aspera basic hybrid], 'Type of token used for transfers')
34
- env[:options].add_opt_boolean(:default_ports, 'use standard FASP ports or get from node api (gen4)')
35
- env[:options].set_option(:asperabrowserurl, 'https://asperabrowser.mybluemix.net')
36
- env[:options].set_option(:token_type, :aspera)
37
- env[:options].set_option(:default_ports, :yes)
75
+ env[:options].declare(:validator, 'Identifier of validator (optional for central)')
76
+ env[:options].declare(:asperabrowserurl, 'URL for simple aspera web ui', default: 'https://asperabrowser.mybluemix.net')
77
+ env[:options].declare(:sync_name, 'Sync name')
78
+ env[:options].declare(:default_ports, 'Use standard FASP ports or get from node api (gen4)', values: :bool, default: :yes)
38
79
  env[:options].parse_options!
39
80
  Aspera::Node.use_standard_ports = env[:options].get_option(:default_ports)
40
81
  end
@@ -53,7 +94,7 @@ module Aspera
53
94
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
54
95
 
55
96
  # actions in execute_command_gen3
56
- COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download]
97
+ COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download sync]
57
98
 
58
99
  BASE_ACTIONS = %i[api_details].concat(COMMANDS_GEN3).freeze
59
100
 
@@ -71,7 +112,7 @@ module Aspera
71
112
  NODE4_READ_ACTIONS = %i[bearer_token_node node_info browse find].freeze
72
113
 
73
114
  # commands for execute_command_gen4
74
- COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download file v3].concat(NODE4_READ_ACTIONS).freeze
115
+ COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
75
116
 
76
117
  COMMANDS_COS = %i[upload download info access_key api_details transfer].freeze
77
118
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
@@ -85,23 +126,23 @@ module Aspera
85
126
  if env.key?(:node_api)
86
127
  # this can be Aspera::Node or Aspera::Rest (shares)
87
128
  env[:node_api]
88
- elsif options.get_option(:password, is_type: :mandatory).start_with?('Bearer ')
129
+ elsif options.get_option(:password, mandatory: true).start_with?('Bearer ')
89
130
  # info is provided like node_info of aoc
90
131
  Aspera::Node.new(params: {
91
- base_url: options.get_option(:url, is_type: :mandatory),
132
+ base_url: options.get_option(:url, mandatory: true),
92
133
  headers: {
93
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options.get_option(:username, is_type: :mandatory),
94
- 'Authorization' => options.get_option(:password, is_type: :mandatory)
134
+ Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => options.get_option(:username, mandatory: true),
135
+ 'Authorization' => options.get_option(:password, mandatory: true)
95
136
  }
96
137
  })
97
138
  else
98
139
  # this is normal case
99
140
  Aspera::Node.new(params: {
100
- base_url: options.get_option(:url, is_type: :mandatory),
141
+ base_url: options.get_option(:url, mandatory: true),
101
142
  auth: {
102
143
  type: :basic,
103
- username: options.get_option(:username, is_type: :mandatory),
104
- password: options.get_option(:password, is_type: :mandatory)
144
+ username: options.get_option(:username, mandatory: true),
145
+ password: options.get_option(:password, mandatory: true)
105
146
  }})
106
147
  end
107
148
  end
@@ -145,7 +186,7 @@ module Aspera
145
186
  # translates paths results into CLI result, and removes prefix
146
187
  def c_result_translate_rem_prefix(response, type, success_msg, path_prefix)
147
188
  errors = []
148
- resres = { data: [], type: :object_list, fields: [type, 'result']}
189
+ final_result = { data: [], type: :object_list, fields: [type, 'result']}
149
190
  JSON.parse(response[:http].body)['paths'].each do |p|
150
191
  result = success_msg
151
192
  if p.key?('error')
@@ -153,13 +194,13 @@ module Aspera
153
194
  result = 'ERROR: ' + p['error']['user_message']
154
195
  errors.push([p['path'], p['error']['user_message']])
155
196
  end
156
- resres[:data].push({type => p['path'], 'result' => result})
197
+ final_result[:data].push({type => p['path'], 'result' => result})
157
198
  end
158
199
  # one error make all fail
159
200
  unless errors.empty?
160
201
  raise errors.map{|i|"#{i.first}: #{i.last}"}.join(', ')
161
202
  end
162
- return c_result_remove_prefix_path(resres, type, path_prefix)
203
+ return c_result_remove_prefix_path(final_result, type, path_prefix)
163
204
  end
164
205
 
165
206
  # get path arguments from command line, and add prefix
@@ -181,13 +222,13 @@ module Aspera
181
222
  when :search
182
223
  search_root = get_next_arg_add_prefix(prefix_path, 'search root')
183
224
  parameters = {'path' => search_root}
184
- other_options = options.get_option(:value)
225
+ other_options = value_or_query(allowed_types: Hash)
185
226
  parameters.merge!(other_options) unless other_options.nil?
186
227
  resp = @api_node.create('files/search', parameters)
187
228
  result = { type: :object_list, data: resp[:data]['items']}
188
229
  return Main.result_empty if result[:data].empty?
189
230
  result[:fields] = result[:data].first.keys.reject{|i|SEARCH_REMOVE_FIELDS.include?(i)}
190
- formatter.display_status("Items: #{resp[:data]['item_count']}/#{resp[:data]['total_count']}")
231
+ formatter.display_item_count(resp[:data]['item_count'], resp[:data]['total_count'])
191
232
  formatter.display_status("params: #{resp[:data]['parameters'].keys.map{|k|"#{k}:#{resp[:data]['parameters'][k]}"}.join(',')}")
192
233
  return c_result_remove_prefix_path(result, 'path', prefix_path)
193
234
  when :space
@@ -228,44 +269,33 @@ module Aspera
228
269
  case send_result['self']['type']
229
270
  when 'directory', 'container' # directory: node, container: shares
230
271
  result = { data: send_result['items'], type: :object_list, textify: lambda { |table_data| c_textify_browse(table_data) } }
231
- formatter.display_status("Items: #{send_result['item_count']}/#{send_result['total_count']}")
272
+ formatter.display_item_count(send_result['item_count'], send_result['total_count'])
232
273
  else # 'file','symbolic_link'
233
274
  result = { data: send_result['self'], type: :single_object}
234
275
  # result={ data: [send_result['self']] , type: :object_list, textify: lambda { |table_data| c_textify_browse(table_data) } }
235
276
  # raise "unknown type: #{send_result['self']['type']}"
236
277
  end
237
278
  return c_result_remove_prefix_path(result, 'path', prefix_path)
279
+ when :sync
280
+ node_sync = SyncSpecGen3.new(@api_node)
281
+ return Plugins::Sync.new(@agents, sync_spec: node_sync).execute_action
238
282
  when :upload, :download
239
- token_type = options.get_option(:token_type)
240
- # nil if Shares 1.x
241
- token_type = :aspera if token_type.nil?
242
- case token_type
243
- when :aspera, :hybrid
244
- # empty transfer spec for authorization request
245
- request_transfer_spec = {}
246
- # set requested paths depending on direction
247
- request_transfer_spec[:paths] = command.eql?(:download) ? transfer.ts_source_paths : [{ destination: transfer.destination_folder('send') }]
248
- # add fixed parameters if any (for COS)
249
- @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
250
- # prepare payload for single request
251
- setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
252
- # only one request, so only one answer
253
- transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
254
- # delete this part, as the returned value contains only destination, and not sources
255
- transfer_spec.delete('paths') if command.eql?(:upload)
256
- when :basic
257
- raise 'shall have auth' unless @api_node.params[:auth].is_a?(Hash)
258
- raise 'shall be basic auth' unless @api_node.params[:auth][:type].eql?(:basic)
259
- transfer_spec = {}.merge(Aspera::Fasp::TransferSpec::AK_TSPEC_BASE)
260
- transfer_spec['remote_host'] = URI.parse(@api_node.params[:base_url]).host
261
- Fasp::TransferSpec.action_to_direction(transfer_spec, command)
262
- transfer_spec['destination_root'] = transfer.destination_folder(transfer_spec['direction'])
263
- @api_node.add_tspec_info(transfer_spec) if @api_node.respond_to?(:add_tspec_info)
264
- else raise "ERROR: token_type #{tt}"
265
- end
266
- if %i[basic hybrid].include?(token_type)
267
- @api_node.ts_basic_token(transfer_spec)
283
+ # empty transfer spec for authorization request
284
+ request_transfer_spec = {}
285
+ # set requested paths depending on direction
286
+ request_transfer_spec[:paths] = if command.eql?(:download)
287
+ transfer.ts_source_paths
288
+ else
289
+ [{ destination: transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND) }]
268
290
  end
291
+ # add fixed parameters if any (for COS)
292
+ @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
293
+ # prepare payload for single request
294
+ setup_payload = {transfer_requests: [{transfer_request: request_transfer_spec}]}
295
+ # only one request, so only one answer
296
+ transfer_spec = @api_node.create("files/#{command}_setup", setup_payload)[:data]['transfer_specs'].first['transfer_spec']
297
+ # delete this part, as the returned value contains only destination, and not sources
298
+ transfer_spec.delete('paths') if command.eql?(:upload)
269
299
  return Main.result_transfer(transfer.start(transfer_spec))
270
300
  end
271
301
  raise 'INTERNAL ERROR'
@@ -314,7 +344,7 @@ module Aspera
314
344
  end
315
345
  return nagios.result
316
346
  when :events
317
- events = @api_node.read('events', options.get_option(:value))[:data]
347
+ events = @api_node.read('events', query_read_delete)[:data]
318
348
  return { type: :object_list, data: events}
319
349
  when :info
320
350
  nd_info = @api_node.read('info')[:data]
@@ -331,57 +361,16 @@ module Aspera
331
361
  end
332
362
  end
333
363
 
334
- def execute_node_gen4_file_command(command_node_file, top_file_id)
335
- file_path = options.get_option(:path)
336
- apifid =
337
- if file_path.nil?
338
- {api: @api_node, file_id: instance_identifier}
339
- else
340
- @api_node.resolve_api_fid(top_file_id, file_path) # TODO: allow follow link ?
341
- end
342
- case command_node_file
343
- when :show
344
- items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
345
- return {type: :single_object, data: items}
346
- when :modify
347
- update_param = options.get_next_argument('update data', type: Hash)
348
- apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
349
- return Main.result_status('Done')
350
- when :permission
351
- command_perm = options.get_next_command(%i[list create delete])
352
- case command_perm
353
- when :list
354
- # generic options : TODO: as arg ? option_url_query
355
- list_options ||= {'include' => [Rest::ARRAY_PARAMS, 'access_level', 'permission_count']}
356
- # add which one to get
357
- list_options['file_id'] = apifid[:file_id]
358
- list_options['inherited'] ||= false
359
- items = apifid[:api].read('permissions', list_options)[:data]
360
- return {type: :object_list, data: items}
361
- when :delete
362
- perm_id = instance_identifier
363
- return do_bulk_operation(perm_id, 'deleted') do |one_id|
364
- # TODO: notify event ?
365
- apifid[:api].delete("permissions/#{perm_id}")
366
- {'id' => one_id}
367
- end
368
- when :create
369
- create_param = options.get_next_argument('creation data', type: Hash)
370
- raise 'no file_id' if create_param.key?('file_id')
371
- create_param['file_id'] = apifid[:file_id]
372
- create_param['access_levels'] = Aspera::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
373
- # add application specific tags (AoC)
374
- the_app = apifid[:api].app_info
375
- the_app[:api].permissions_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
376
- # create permission
377
- created_data = apifid[:api].create('permissions', create_param)[:data]
378
- # bnotify application of creation
379
- the_app[:api].permissions_create_event(created_data: created_data, app_info: the_app) unless the_app.nil?
380
- return { type: :single_object, data: created_data}
381
- else raise "internal error:shall not reach here (#{command_perm})"
382
- end
383
- else raise "internal error:shall not reach here (#{command_node_file})"
364
+ # @return [Hash] api and main file id for given path or id
365
+ # Allows to specify a file by its path or by its id on the node
366
+ def apifid_from_next_arg(top_file_id)
367
+ file_path = instance_identifier(description: 'path or id') do |attribute, value|
368
+ raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
369
+ # directly return result for method
370
+ return {api: @api_node, file_id: value}
384
371
  end
372
+ # there was no selector, so it is a path
373
+ return @api_node.resolve_api_fid(top_file_id, file_path)
385
374
  end
386
375
 
387
376
  def execute_command_gen4(command_repo, top_file_id)
@@ -415,16 +404,16 @@ module Aspera
415
404
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
416
405
  file_info = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
417
406
  if file_info['type'].eql?('folder')
418
- result = apifid[:api].read("files/#{apifid[:file_id]}/files", options.get_option(:value))
407
+ result = apifid[:api].read("files/#{apifid[:file_id]}/files", old_query_read_delete)
419
408
  items = result[:data]
420
- formatter.display_status("Items: #{result[:data].length}/#{result[:http]['X-Total-Count']}")
409
+ formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
421
410
  else
422
411
  items = [file_info]
423
412
  end
424
413
  return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
425
414
  when :find
426
415
  apifid = @api_node.resolve_api_fid(top_file_id, options.get_next_argument('path'))
427
- test_block = Aspera::Node.file_matcher(options.get_option(:value))
416
+ test_block = Aspera::Node.file_matcher(value_or_query(allowed_types: String))
428
417
  return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
429
418
  when :mkdir
430
419
  containing_folder_path = options.get_next_argument('path').split(Aspera::Node::PATH_SEPARATOR)
@@ -434,10 +423,10 @@ module Aspera
434
423
  return Main.result_status("created: #{result['name']} (id=#{result['id']})")
435
424
  when :rename
436
425
  file_path = options.get_next_argument('source path')
437
- newname = options.get_next_argument('new name')
438
426
  apifid = @api_node.resolve_api_fid(top_file_id, file_path)
427
+ newname = options.get_next_argument('new name')
439
428
  result = apifid[:api].update("files/#{apifid[:file_id]}", {name: newname})[:data]
440
- return Main.result_status("renamed #{file_path} to #{newname}")
429
+ return Main.result_status("renamed to #{newname}")
441
430
  when :delete
442
431
  return do_bulk_operation(options.get_next_argument('path'), 'deleted', id_result: 'path') do |l_path|
443
432
  raise "expecting String (path), got #{l_path.class.name} (#{l_path})" unless l_path.is_a?(String)
@@ -446,12 +435,8 @@ module Aspera
446
435
  {'path' => l_path}
447
436
  end
448
437
  when :sync
449
- # remote is specified by option to_folder
450
- apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND))
451
- transfer_spec = apifid[:api].transfer_spec_gen4(apifid[:file_id], Fasp::TransferSpec::DIRECTION_SEND)
452
- Log.dump(:ts, transfer_spec)
453
- sync_plugin = Plugins::Sync.new(@agents, transfer_spec: transfer_spec)
454
- return sync_plugin.execute_action
438
+ node_sync = SyncSpecGen4.new(@api_node, top_file_id)
439
+ return Plugins::Sync.new(@agents, sync_spec: node_sync).execute_action
455
440
  when :upload
456
441
  apifid = @api_node.resolve_api_fid(top_file_id, transfer.destination_folder(Fasp::TransferSpec::DIRECTION_SEND))
457
442
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Fasp::TransferSpec::DIRECTION_SEND)))
@@ -498,9 +483,60 @@ module Aspera
498
483
  subpath: "files/#{apifid[:file_id]}/content",
499
484
  save_to_file: File.join(transfer.destination_folder(Fasp::TransferSpec::DIRECTION_RECEIVE), file_name))
500
485
  return Main.result_status("downloaded: #{file_name}")
501
- when :file
502
- command_node_file = options.get_next_command(%i[show modify permission])
503
- return execute_node_gen4_file_command(command_node_file, top_file_id)
486
+ when :show
487
+ apifid = apifid_from_next_arg(top_file_id)
488
+ items = apifid[:api].read("files/#{apifid[:file_id]}")[:data]
489
+ return {type: :single_object, data: items}
490
+ when :modify
491
+ apifid = apifid_from_next_arg(top_file_id)
492
+ update_param = options.get_next_argument('update data', type: Hash)
493
+ apifid[:api].update("files/#{apifid[:file_id]}", update_param)[:data]
494
+ return Main.result_status('Done')
495
+ when :thumbnail
496
+ apifid = apifid_from_next_arg(top_file_id)
497
+ result = apifid[:api].call(
498
+ operation: 'GET',
499
+ subpath: "files/#{apifid[:file_id]}/preview",
500
+ headers: {'Accept' => 'image/png'}
501
+ )
502
+ require 'aspera/preview/terminal'
503
+ return Main.result_status(Preview::Terminal.build(result[:http].body, reserved_lines: 3))
504
+ when :permission
505
+ apifid = apifid_from_next_arg(top_file_id)
506
+ command_perm = options.get_next_command(%i[list create delete])
507
+ case command_perm
508
+ when :list
509
+ # generic options : TODO: as arg ? query_read_delete
510
+ list_options ||= {'include' => Rest.array_params(%w[access_level permission_count])}
511
+ # add which one to get
512
+ list_options['file_id'] = apifid[:file_id]
513
+ list_options['inherited'] ||= false
514
+ items = apifid[:api].read('permissions', list_options)[:data]
515
+ return {type: :object_list, data: items}
516
+ when :delete
517
+ perm_id = instance_identifier
518
+ return do_bulk_operation(perm_id, 'deleted') do |one_id|
519
+ # TODO: notify event ?
520
+ apifid[:api].delete("permissions/#{perm_id}")
521
+ # notify application of deletion
522
+ the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app, types: ['permission.deleted']) unless the_app.nil?
523
+ {'id' => one_id}
524
+ end
525
+ when :create
526
+ create_param = options.get_next_argument('creation data', type: Hash)
527
+ raise 'no file_id' if create_param.key?('file_id')
528
+ create_param['file_id'] = apifid[:file_id]
529
+ create_param['access_levels'] = Aspera::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
530
+ # add application specific tags (AoC)
531
+ the_app = apifid[:api].app_info
532
+ the_app[:api].permissions_set_create_params(create_param: create_param, app_info: the_app) unless the_app.nil?
533
+ # create permission
534
+ created_data = apifid[:api].create('permissions', create_param)[:data]
535
+ # notify application of creation
536
+ the_app[:api].permissions_send_event(created_data: created_data, app_info: the_app) unless the_app.nil?
537
+ return { type: :single_object, data: created_data}
538
+ else raise "internal error:shall not reach here (#{command_perm})"
539
+ end
504
540
  else raise "INTERNAL ERROR: no case for #{command_repo}"
505
541
  end # command_repo
506
542
  # raise 'INTERNAL ERROR: missing return'
@@ -510,65 +546,65 @@ module Aspera
510
546
  def execute_async
511
547
  command = options.get_next_command(%i[list delete files show counters bandwidth])
512
548
  unless command.eql?(:list)
513
- asyncname = options.get_option(:sync_name)
514
- if asyncname.nil?
515
- asyncid = instance_identifier
516
- if asyncid.eql?(VAL_ALL) && %i[show delete].include?(command)
517
- asyncids = @api_node.read('async/list')[:data]['sync_ids']
549
+ async_name = options.get_option(:sync_name)
550
+ if async_name.nil?
551
+ async_id = instance_identifier
552
+ if async_id.eql?(VAL_ALL) && %i[show delete].include?(command)
553
+ async_ids = @api_node.read('async/list')[:data]['sync_ids']
518
554
  else
519
- Integer(asyncid) # must be integer
520
- asyncids = [asyncid]
555
+ Integer(async_id) # must be integer
556
+ async_ids = [async_id]
521
557
  end
522
558
  else
523
- asyncids = @api_node.read('async/list')[:data]['sync_ids']
524
- summaries = @api_node.create('async/summary', {'syncs' => asyncids})[:data]['sync_summaries']
525
- selected = summaries.find{|s|s['name'].eql?(asyncname)}
526
- raise "no such sync: #{asyncname}" if selected.nil?
527
- asyncid = selected['snid']
528
- asyncids = [asyncid]
559
+ async_ids = @api_node.read('async/list')[:data]['sync_ids']
560
+ summaries = @api_node.create('async/summary', {'syncs' => async_ids})[:data]['sync_summaries']
561
+ selected = summaries.find{|s|s['name'].eql?(async_name)}
562
+ raise "no such sync: #{async_name}" if selected.nil?
563
+ async_id = selected['snid']
564
+ async_ids = [async_id]
529
565
  end
530
- pdata = {'syncs' => asyncids}
566
+ post_data = {'syncs' => async_ids}
531
567
  end
532
568
  case command
533
569
  when :list
534
570
  resp = @api_node.read('async/list')[:data]['sync_ids']
535
571
  return { type: :value_list, data: resp, name: 'id' }
536
572
  when :show
537
- resp = @api_node.create('async/summary', pdata)[:data]['sync_summaries']
573
+ resp = @api_node.create('async/summary', post_data)[:data]['sync_summaries']
538
574
  return Main.result_empty if resp.empty?
539
- return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if asyncid.eql?(VAL_ALL)
575
+ return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(VAL_ALL)
540
576
  return { type: :single_object, data: resp.first }
541
577
  when :delete
542
- resp = @api_node.create('async/delete', pdata)[:data]
578
+ resp = @api_node.create('async/delete', post_data)[:data]
543
579
  return { type: :single_object, data: resp, name: 'id' }
544
580
  when :bandwidth
545
- pdata['seconds'] = 100 # TODO: as parameter with --value
546
- resp = @api_node.create('async/bandwidth', pdata)[:data]
581
+ post_data['seconds'] = 100 # TODO: as parameter with --value
582
+ resp = @api_node.create('async/bandwidth', post_data)[:data]
547
583
  data = resp['bandwidth_data']
548
584
  return Main.result_empty if data.empty?
549
- data = data.first[asyncid]['data']
585
+ data = data.first[async_id]['data']
550
586
  return { type: :object_list, data: data, name: 'id' }
551
587
  when :files
552
588
  # count int
553
589
  # filename str
554
590
  # skip int
555
591
  # status int
556
- filter = options.get_option(:value)
557
- pdata.merge!(filter) unless filter.nil?
558
- resp = @api_node.create('async/files', pdata)[:data]
592
+ filter = value_or_query(allowed_types: Hash)
593
+ post_data.merge!(filter) unless filter.nil?
594
+ resp = @api_node.create('async/files', post_data)[:data]
559
595
  data = resp['sync_files']
560
- data = data.first[asyncid] unless data.empty?
596
+ data = data.first[async_id] unless data.empty?
561
597
  iteration_data = []
562
598
  skip_ids_persistency = nil
563
- if options.get_option(:once_only, is_type: :mandatory)
599
+ if options.get_option(:once_only, mandatory: true)
564
600
  skip_ids_persistency = PersistencyActionOnce.new(
565
601
  manager: @agents[:persistency],
566
602
  data: iteration_data,
567
603
  id: IdGenerator.from_list([
568
604
  'sync_files',
569
- options.get_option(:url, is_type: :mandatory),
570
- options.get_option(:username, is_type: :mandatory),
571
- asyncid]))
605
+ options.get_option(:url, mandatory: true),
606
+ options.get_option(:username, mandatory: true),
607
+ async_id]))
572
608
  unless iteration_data.first.nil?
573
609
  data.select!{|l| l['fnid'].to_i > iteration_data.first}
574
610
  end
@@ -578,7 +614,7 @@ module Aspera
578
614
  skip_ids_persistency&.save
579
615
  return { type: :object_list, data: data, name: 'id' }
580
616
  when :counters
581
- resp = @api_node.create('async/counters', pdata)[:data]['sync_counters'].first[asyncid].last
617
+ resp = @api_node.create('async/counters', post_data)[:data]['sync_counters'].first[async_id].last
582
618
  return Main.result_empty if resp.nil?
583
619
  return { type: :single_object, data: resp }
584
620
  end
@@ -586,7 +622,7 @@ module Aspera
586
622
 
587
623
  ACTIONS = %i[
588
624
  async
589
- sync
625
+ ssync
590
626
  stream
591
627
  transfer
592
628
  service
@@ -599,18 +635,21 @@ module Aspera
599
635
  command ||= options.get_next_command(ACTIONS)
600
636
  case command
601
637
  when *COMMON_ACTIONS then return execute_simple_common(command, prefix_path)
602
- when :async then return execute_async
603
- when :sync
604
- # newer api
605
- sync_command = options.get_next_command(%i[bandwidth counters files start state stop summary].concat(Plugin::ALL_OPS) - %i[modify])
638
+ when :async then return execute_async # former API
639
+ when :ssync
640
+ # newer API
641
+ sync_command = options.get_next_command(%i[start stop bandwidth counters files state summary].concat(Plugin::ALL_OPS) - %i[modify])
606
642
  case sync_command
607
643
  when *Plugin::ALL_OPS then return entity_command(sync_command, @api_node, 'asyncs', item_list_key: 'ids')
608
644
  else
609
- parameters = options.get_option(:value)
610
645
  asyncs_id = instance_identifier
646
+ parameters = nil
611
647
  if %i[start stop].include?(sync_command)
612
648
  @api_node.create("asyncs/#{asyncs_id}/#{sync_command}", parameters)
613
- return Main.result_status('ok')
649
+ return Main.result_status('Done')
650
+ end
651
+ if %i[bandwidth counters files].include?(sync_command)
652
+ parameters = value_or_query(allowed_types: Hash, mandatory: false) || {}
614
653
  end
615
654
  return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters)[:data] }
616
655
  end
@@ -618,16 +657,16 @@ module Aspera
618
657
  command = options.get_next_command(%i[list create show modify cancel])
619
658
  case command
620
659
  when :list
621
- resp = @api_node.read('ops/transfers', options.get_option(:value))
660
+ resp = @api_node.read('ops/transfers', old_query_read_delete)
622
661
  return { type: :object_list, data: resp[:data], fields: %w[id status] } # TODO: useful?
623
662
  when :create
624
- resp = @api_node.create('streams', options.get_option(:value, is_type: :mandatory))
663
+ resp = @api_node.create('streams', value_create_modify(command: command, type: Hash))
625
664
  return { type: :single_object, data: resp[:data] }
626
665
  when :show
627
666
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
628
667
  return { type: :other_struct, data: resp[:data] }
629
668
  when :modify
630
- resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", options.get_option(:value, is_type: :mandatory))
669
+ resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command, type: Hash))
631
670
  return { type: :other_struct, data: resp[:data] }
632
671
  when :cancel
633
672
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
@@ -636,21 +675,21 @@ module Aspera
636
675
  raise 'error'
637
676
  end
638
677
  when :transfer
639
- command = options.get_next_command(%i[list cancel show])
678
+ command = options.get_next_command(%i[list cancel show modify bandwidth_average])
640
679
  res_class_path = 'ops/transfers'
641
- if %i[cancel show].include?(command)
680
+ if %i[cancel show modify].include?(command)
642
681
  one_res_id = instance_identifier
643
682
  one_res_path = "#{res_class_path}/#{one_res_id}"
644
683
  end
645
684
  case command
646
685
  when :list
647
686
  # could use ? subpath: 'transfers'
648
- query = options.get_option(:value) || options.get_option(:query)
687
+ query = query_read_delete
649
688
  raise 'Query must be a Hash' unless query.nil? || query.is_a?(Hash)
650
- resp = @api_node.read(res_class_path, query)
689
+ transfers_data = @api_node.read(res_class_path, query)[:data]
651
690
  return {
652
691
  type: :object_list,
653
- data: resp[:data],
692
+ data: transfers_data,
654
693
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
655
694
  }
656
695
  when :cancel
@@ -659,13 +698,61 @@ module Aspera
659
698
  when :show
660
699
  resp = @api_node.read(one_res_path)
661
700
  return { type: :other_struct, data: resp[:data] }
701
+ when :modify
702
+ resp = @api_node.update(one_res_path, options.get_next_argument('update value', type: Hash))
703
+ return { type: :other_struct, data: resp[:data] }
704
+ when :bandwidth_average
705
+ transfers_data = @api_node.read(res_class_path, query)[:data]
706
+ # collect all key dates
707
+ bandwidth_period = {}
708
+ dir_info = %i[avg_kbps sessions].freeze
709
+ transfers_data.each do |transfer|
710
+ session = transfer
711
+ # transfer['sessions'].each do |session|
712
+ next if session['avg_rate_kbps'].zero?
713
+ bandwidth_period[session['start_time_usec']] = 0
714
+ bandwidth_period[session['end_time_usec']] = 0
715
+ # end
716
+ end
717
+ result = []
718
+ # all dates sorted numerically
719
+ all_dates = bandwidth_period.keys.sort
720
+ all_dates.each_with_index do |start_date, index|
721
+ end_date = all_dates[index + 1]
722
+ # do not process last one
723
+ break if end_date.nil?
724
+ # init data for this period
725
+ period_bandwidth = Fasp::TransferSpec::DIRECTION_ENUM_VALUES.map(&:to_sym).each_with_object({}) do |direction, h|
726
+ h[direction] = dir_info.each_with_object({}) do |k2, h2|
727
+ h2[k2] = 0
728
+ end
729
+ end
730
+ # find all transfers that were active at this time
731
+ transfers_data.each do |transfer|
732
+ session = transfer
733
+ # transfer['sessions'].each do |session|
734
+ # skip if not information for this period
735
+ next if session['avg_rate_kbps'].zero?
736
+ # skip if not in this period
737
+ next if session['start_time_usec'] >= end_date || session['end_time_usec'] <= start_date
738
+ info = period_bandwidth[transfer['start_spec']['direction'].to_sym]
739
+ info[:avg_kbps] += session['avg_rate_kbps']
740
+ info[:sessions] += 1
741
+ # end
742
+ end
743
+ next if Fasp::TransferSpec::DIRECTION_ENUM_VALUES.map(&:to_sym).all? do |dir|
744
+ period_bandwidth[dir][:sessions].zero?
745
+ end
746
+ result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
747
+ end
748
+ return { type: :object_list, data: result }
662
749
  else
663
750
  raise 'error'
664
751
  end
665
752
  when :service
666
753
  command = options.get_next_command(%i[list create delete])
667
754
  if [:delete].include?(command)
668
- svcid = instance_identifier
755
+ service_id = instance_identifier
669
756
  end
670
757
  case command
671
758
  when :list
@@ -677,8 +764,8 @@ module Aspera
677
764
  resp = @api_node.create('rund/services', params)
678
765
  return Main.result_status("#{resp[:data]['id']} created")
679
766
  when :delete
680
- @api_node.delete("rund/services/#{svcid}")
681
- return Main.result_status("#{svcid} deleted")
767
+ @api_node.delete("rund/services/#{service_id}")
768
+ return Main.result_status("#{service_id} deleted")
682
769
  end
683
770
  when :watch_folder
684
771
  res_class_path = 'v3/watchfolders'
@@ -692,15 +779,15 @@ module Aspera
692
779
  @api_node.params[:headers]['X-aspera-WF-version'] = '2017_10_23'
693
780
  case command
694
781
  when :create
695
- resp = @api_node.create(res_class_path, options.get_option(:value, is_type: :mandatory))
782
+ resp = @api_node.create(res_class_path, value_create_modify(command: command, type: Hash))
696
783
  return Main.result_status("#{resp[:data]['id']} created")
697
784
  when :list
698
- resp = @api_node.read(res_class_path, options.get_option(:value))
785
+ resp = @api_node.read(res_class_path, old_query_read_delete)
699
786
  return { type: :value_list, data: resp[:data]['ids'], name: 'id' }
700
787
  when :show
701
788
  return { type: :single_object, data: @api_node.read(one_res_path)[:data]}
702
789
  when :modify
703
- @api_node.update(one_res_path, options.get_option(:value, is_type: :mandatory))
790
+ @api_node.update(one_res_path, value_or_query(mandatory: true, allowed_types: Hash))
704
791
  return Main.result_status("#{one_res_id} updated")
705
792
  when :delete
706
793
  @api_node.delete(one_res_path)
@@ -712,8 +799,7 @@ module Aspera
712
799
  command = options.get_next_command(%i[session file])
713
800
  validator_id = options.get_option(:validator)
714
801
  validation = {'validator_id' => validator_id} unless validator_id.nil?
715
- request_data = options.get_option(:value)
716
- request_data ||= {}
802
+ request_data = value_create_modify(default: {}, type: Hash)
717
803
  case command
718
804
  when :session
719
805
  command = options.get_next_command([:list])
@@ -744,16 +830,16 @@ module Aspera
744
830
  end
745
831
  when :asperabrowser
746
832
  browse_params = {
747
- 'nodeUser' => options.get_option(:username, is_type: :mandatory),
748
- 'nodePW' => options.get_option(:password, is_type: :mandatory),
749
- 'nodeURL' => options.get_option(:url, is_type: :mandatory)
833
+ 'nodeUser' => options.get_option(:username, mandatory: true),
834
+ 'nodePW' => options.get_option(:password, mandatory: true),
835
+ 'nodeURL' => options.get_option(:url, mandatory: true)
750
836
  }
751
837
  # encode parameters so that it looks good in url
752
838
  encoded_params = Base64.strict_encode64(Zlib::Deflate.deflate(JSON.generate(browse_params))).gsub(/=+$/, '').tr('+/', '-_').reverse
753
839
  OpenApplication.instance.uri(options.get_option(:asperabrowserurl) + '?goto=' + encoded_params)
754
840
  return Main.result_status('done')
755
841
  when :basic_token
756
- return Main.result_status(Rest.basic_creds(options.get_option(:username, is_type: :mandatory), options.get_option(:password, is_type: :mandatory)))
842
+ return Main.result_status(Rest.basic_creds(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
757
843
  end # case command
758
844
  raise 'ERROR: shall not reach this line'
759
845
  end # execute_action