aspera-cli 4.27.2 → 4.27.4

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 (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -9,8 +9,10 @@ require 'securerandom'
9
9
  require 'websocket'
10
10
  require 'base64'
11
11
  require 'json'
12
+ require 'aspera/rainbow'
13
+ using Rainbow
12
14
 
13
- # throw exception on error, instead of error code
15
+ # Throw exception on error, instead of error code
14
16
  WebSocket.should_raise = true
15
17
 
16
18
  module Aspera
@@ -29,7 +31,7 @@ module Aspera
29
31
  # 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
30
32
  # 2.. Binary File binary chunks "end upload" sent_general
31
33
  # last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
32
- class Httpgw < Aspera::Rest
34
+ class Httpgw < Aspera::Rest::Client
33
35
  DEFAULT_BASE_PATH = '/aspera/http-gwy'
34
36
  INFO_ENDPOINT = 'info'
35
37
  MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
@@ -40,27 +42,24 @@ module Aspera
40
42
  API_V1 = 'v1'
41
43
  API_V2 = 'v2'
42
44
  THR_RECV = 'recv'
43
- LOG_WS_SEND = 'ws: send: '.red
44
- LOG_WS_RECV = "ws: #{THR_RECV}: ".green
45
45
  private_constant :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL
46
- # send message on http gw web socket
46
+
47
+ # Send message on http gw web socket
48
+ # @param msg_type [String] type of message to send
49
+ # @param payload [Hash] Data to send
47
50
  def ws_snd_json(msg_type, payload)
48
51
  if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @upload_version.eql?(API_V2)
49
52
  @shared_info[:count][:sent_v2_delimiter] += 1
50
53
  else
51
54
  @shared_info[:count][:sent_general] += 1
52
55
  end
53
- Log.log.trace1 do
54
- log_data = payload.dup
55
- log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
56
- "#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
57
- end
56
+ Log.dump("#{log_ws_send} #{msg_type}", level: :trace1) { payload.dup.tap { |d| d[:data] = "[data #{d[:data].length} bytes]" if d.key?(:data) } }
58
57
  ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
59
58
  end
60
59
 
61
60
  # send data on http gw web socket
62
61
  def ws_send(ws_type:, data:)
63
- Log.log.trace1 { "#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)" }
62
+ Log.log.trace1 { "#{log_ws_send}sending: #{ws_type} (#{data&.length || 0} bytes)" }
64
63
  @shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
65
64
  frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
66
65
  @ws_io.write(frame_generator.to_s)
@@ -72,12 +71,12 @@ module Aspera
72
71
  @shared_info[:read_exception].nil? &&
73
72
  (((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
74
73
  ((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
75
- Log.log.trace1 { "#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}" } if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
74
+ Log.log.trace1 { "#{log_ws_send}#{'timeout'.blue}: #{@shared_info[:count]}" } if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
76
75
  end
77
76
  end
78
77
  end
79
78
  raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
80
- Log.log.trace2 { "#{LOG_WS_SEND}counts: #{@shared_info[:count]}" }
79
+ Log.log.trace2 { "#{log_ws_send}counts: #{@shared_info[:count]}" }
81
80
  end
82
81
 
83
82
  # Check header ourself and give precise error message, as websocket will only throw error without details
@@ -85,13 +84,13 @@ module Aspera
85
84
  # @return [String] Response Header
86
85
  def validated_ws_response_header(header)
87
86
  first_line = header.split("\r\n").first
88
- raise RestCallError.new({messages: ["Unexpected: #{first_line}", 'Expected: 101 Switching Protocols']}) unless first_line.split(/\s+/, 3)[1].eql?('101')
87
+ raise Rest::CallError.new({messages: ["Unexpected: #{first_line}", 'Expected: 101 Switching Protocols']}) unless first_line.split(/\s+/, 3)[1].eql?('101')
89
88
  header
90
89
  end
91
90
 
92
91
  # message processing for read thread
93
92
  def process_received_message(message)
94
- Log.log.debug { "#{LOG_WS_RECV}message: [#{message}] (#{message.class})" }
93
+ Log.log.debug { "#{log_ws_recv}message: [#{message}] (#{message.class})" }
95
94
  if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
96
95
  @shared_info[:mutex].synchronize do
97
96
  @shared_info[:count][:received_general] += 1
@@ -117,23 +116,23 @@ module Aspera
117
116
  end
118
117
  end
119
118
 
120
- # main function of read thread
119
+ # Main function of read thread
121
120
  def process_read_thread
122
- Log.log.debug { "#{LOG_WS_RECV}read thread started" }
121
+ Log.log.debug { "#{log_ws_recv}read thread started" }
123
122
  frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
124
123
  until @ws_io.eof?
125
124
  begin
126
125
  # ready byte by byte until frame is ready
127
126
  # blocking read
128
127
  byte = @ws_io.read(1)
129
- Log.log.trace2 { "#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}" }
128
+ Log.log.trace2 { "#{log_ws_recv}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}" }
130
129
  frame_parser << byte
131
130
  frame_ok = frame_parser.next
132
131
  next if frame_ok.nil?
133
132
  process_received_message(frame_ok.data.to_s)
134
- Log.log.trace2 { "#{LOG_WS_RECV}counts: #{@shared_info[:count]}" }
133
+ Log.log.trace2 { "#{log_ws_recv}counts: #{@shared_info[:count]}" }
135
134
  rescue => e
136
- Log.log.debug { "#{LOG_WS_RECV}Exception: #{e}" }
135
+ Log.log.debug { "#{log_ws_recv}Exception: #{e}" }
137
136
  @shared_info[:mutex].synchronize do
138
137
  @shared_info[:read_exception] = e
139
138
  @shared_info[:cond_var].signal
@@ -142,9 +141,9 @@ module Aspera
142
141
  end
143
142
  end
144
143
  Log.log.debug do
145
- "#{LOG_WS_RECV}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
144
+ "#{log_ws_recv}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
146
145
  end unless @shared_info[:read_exception].nil?
147
- Log.log.debug { "#{LOG_WS_RECV}read thread stopped (ws eof=#{@ws_io.eof?})" }
146
+ Log.log.debug { "#{log_ws_recv}read thread stopped (ws eof=#{@ws_io.eof?})" }
148
147
  end
149
148
 
150
149
  def upload(transfer_spec)
@@ -170,7 +169,7 @@ module Aspera
170
169
  # Get whole HTTP response header, Check and process
171
170
  # no need to check `finished?` or result of `<<` (true), as we give the whole header at once
172
171
  @ws_handshake << validated_ws_response_header(@ws_io.readuntil("\r\n\r\n"))
173
- Log.log.debug { "#{LOG_WS_SEND}handshake success" }
172
+ Log.log.debug { "#{log_ws_send}handshake success" }
174
173
  # data shared between main thread and read thread
175
174
  @shared_info = {
176
175
  read_exception: nil, # error message if any in callback
@@ -224,7 +223,7 @@ module Aspera
224
223
  # send once, before data, at beginning
225
224
  ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
226
225
  ws_send(ws_type: :binary, data: slice_bin_data)
227
- Log.log.trace1 { "#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}" }
226
+ Log.log.trace1 { "#{log_ws_send}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}" }
228
227
  # send once, after data, at end
229
228
  ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(last_slice)
230
229
  end
@@ -255,12 +254,11 @@ module Aspera
255
254
  # session no more used
256
255
  @ws_io = nil
257
256
  http_session&.finish
258
- @notify_cb&.call(:end)
259
257
  end
260
258
 
261
259
  def download(transfer_spec)
262
260
  transfer_spec['source_root'] ||= '/'
263
- default_file_name = transfer_spec['paths'].first['source']
261
+ default_file_name = transfer_spec['paths'].first&.fetch('source')
264
262
  source_is_folder = %w[. /].include?(default_file_name)
265
263
  default_file_name = 'http_download' if source_is_folder
266
264
  transfer_spec['zip_required'] ||= source_is_folder || transfer_spec['paths'].length > 1
@@ -283,17 +281,20 @@ module Aspera
283
281
  # it is a plain file if we don't require zip and there is only one file
284
282
  File.basename(default_file_name)
285
283
  end
286
- file_path = File.join(transfer_spec['destination_root'], file_name)
287
- call(operation: 'GET', subpath: "download/#{transfer_uuid}", save_to: file_path)
284
+ call(
285
+ operation: 'GET',
286
+ subpath: "download/#{transfer_uuid}",
287
+ save_to: File.join(transfer_spec['destination_root'], file_name)
288
+ )
288
289
  end
289
290
 
290
291
  def info
291
- return @api_info
292
+ @api_info
292
293
  end
293
294
 
294
295
  # @return [String] the base url of the gateway
295
296
  def base_url
296
- return @gw_root_url
297
+ @gw_root_url
297
298
  end
298
299
 
299
300
  # @param url [String] URL of the HTTP Gateway, without version
@@ -306,10 +307,10 @@ module Aspera
306
307
  **opts
307
308
  )
308
309
  Log.dump(:gw_url, url)
309
- # add scheme if missing
310
+ # Add scheme if missing
310
311
  url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
311
312
  Aspera.assert(url.start_with?('https://'), type: Error) { 'GW URL shall be with scheme https' }
312
- # remove trailing slash and version (o=only once) if present
313
+ # Remove trailing slash and version (o=only once) if present
313
314
  # TODO: issue warning ?
314
315
  url = url.chomp('/').gsub(%r{/#{API_V1}$}o, '')
315
316
  # assume GW is always under specific path (TODO: remove this ?)
@@ -339,6 +340,10 @@ module Aspera
339
340
 
340
341
  private
341
342
 
343
+ # Log prefixes (evaluated on each call, as colors can be enabled or disabled by option)
344
+ def log_ws_send = 'ws: send: '.red
345
+ def log_ws_recv = "ws: #{THR_RECV}: ".green
346
+
342
347
  # compute total size of files to upload (for progress)
343
348
  # modify transfer spec to be suitable for HTTPGW
344
349
  # @param transfer_spec [Hash] transfer specification
@@ -372,8 +377,7 @@ module Aspera
372
377
  until folders_to_process.empty?
373
378
  folder = folders_to_process.shift
374
379
  # read all entries
375
- Dir.entries(folder).each do |entry|
376
- next if entry.eql?('.') || entry.eql?('..')
380
+ Dir.each_child(folder) do |entry|
377
381
  entry_path = File.join(folder, entry)
378
382
  if File.directory?(entry_path)
379
383
  folders_to_process.push(entry_path)
@@ -396,7 +400,6 @@ module Aspera
396
400
  end
397
401
  transfer_spec['paths'] = files_to_send.map { |i| {'source' => i[:name]} }
398
402
  files_to_send.push(total_bytes_to_transfer)
399
- return files_to_send
400
403
  end
401
404
  end
402
405
  end
@@ -12,6 +12,8 @@ require 'openssl'
12
12
  require 'pathname'
13
13
  require 'zlib'
14
14
  require 'net/ssh/buffer'
15
+ require 'aspera/rainbow'
16
+ using Rainbow
15
17
 
16
18
  module Aspera
17
19
  module Api
@@ -33,7 +35,7 @@ module Aspera
33
35
 
34
36
  # Aspera Node API client
35
37
  # with gen4 extensions (access keys)
36
- class Node < Rest
38
+ class Node < Rest::Client
37
39
  # Format of node scope : node.<access key>:<scope>
38
40
  module Scope
39
41
  # Node sub-scopes
@@ -42,6 +44,26 @@ module Aspera
42
44
  # Separator between node.AK and user:all
43
45
  SEPARATOR = ':'
44
46
  NODE_PREFIX = 'node.'
47
+
48
+ extend self
49
+
50
+ # Encode node API scope
51
+ # @param access_key [String] Access Key
52
+ # @param scope [String] internal scope
53
+ # @return [String] full node scope
54
+ def join(access_key:, scope:)
55
+ ["#{NODE_PREFIX}#{access_key}", scope].join(SEPARATOR)
56
+ end
57
+
58
+ # Decode node scope into access key and scope
59
+ # @param scope [String] scope string to split
60
+ # @return [Hash] decoded scope with access key and scope parts
61
+ def split(scope)
62
+ items = scope.split(Scope::SEPARATOR, 2)
63
+ Aspera.assert(items.length.eql?(2)) { "invalid scope: #{scope}" }
64
+ Aspera.assert(items[0].start_with?(NODE_PREFIX)) { "invalid scope: #{scope}" }
65
+ {access_key: items[0].delete_prefix(NODE_PREFIX), scope: items[1]}
66
+ end
45
67
  end
46
68
  # Delimiter in decoded node token
47
69
  SIGNATURE_DELIMITER = '==SIGNATURE=='
@@ -54,10 +76,10 @@ module Aspera
54
76
  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
55
77
  HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
56
78
  HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
79
+ HEADER_X_TOTAL_COUNT = 'X-Total-Count'
57
80
  HEADER_ACCEPT_VERSION = 'Accept-Version'
58
81
  # / in cloud
59
82
  PATH_SEPARATOR = '/'
60
- HEADER_X_TOTAL_COUNT = 'X-Total-Count'
61
83
 
62
84
  # Register node special token decoder
63
85
  OAuth::Factory.instance.register_decoder(lambda { |token| Node.decode_bearer_token(token) })
@@ -133,22 +155,6 @@ module Aspera
133
155
  [folder.join(PATH_SEPARATOR), inside]
134
156
  end
135
157
 
136
- # Node API scopes
137
- # @return [String] node scope
138
- def token_scope(access_key, scope)
139
- return [Scope::NODE_PREFIX, access_key, Scope::SEPARATOR, scope].join('')
140
- end
141
-
142
- # Decode node scope into access key and scope
143
- # @param scope [String] scope string to decode
144
- # @return [Hash] decoded scope with access key and scope parts
145
- def decode_scope(scope)
146
- items = scope.split(Scope::SEPARATOR, 2)
147
- Aspera.assert(items.length.eql?(2)) { "invalid scope: #{scope}" }
148
- Aspera.assert(items[0].start_with?(Scope::NODE_PREFIX)) { "invalid scope: #{scope}" }
149
- return {access_key: items[0].delete_prefix(Scope::NODE_PREFIX), scope: items[1]}
150
- end
151
-
152
158
  # Create an Aspera Node bearer token
153
159
  # @param access_key [String] Access key identifier
154
160
  # @param payload [String] JSON payload to be included in the token
@@ -162,9 +168,8 @@ module Aspera
162
168
  # Manage convenience parameters
163
169
  expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
164
170
  payload.delete('_validity')
165
- scope = payload['_scope'] || Scope::USER
166
- payload.delete('_scope')
167
- payload['scope'] ||= token_scope(access_key, scope)
171
+ scope = payload.delete('_scope') || Scope::USER
172
+ payload['scope'] ||= Scope.join(access_key: access_key, scope: scope)
168
173
  payload['auth_type'] ||= 'access_key'
169
174
  payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
170
175
  payload_json = JSON.generate(payload)
@@ -185,7 +190,7 @@ module Aspera
185
190
  def bearer_headers(bearer_auth, access_key: nil)
186
191
  # If username is not provided, use the access key from the token
187
192
  if access_key.nil?
188
- access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
193
+ access_key = Node::Scope.split(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
189
194
  Aspera.assert(!access_key.nil?, 'access_key could not be determined from token')
190
195
  end
191
196
  return {
@@ -233,7 +238,7 @@ module Aspera
233
238
  end
234
239
  Log.log.warn { "Cannot resolve link with node id #{node_id}, no resolver" }
235
240
  return
236
- rescue RestCallError => e
241
+ rescue Rest::CallError => e
237
242
  Log.log.warn { "Cannot resolve link with node id #{node_id}: #{e.message}" }
238
243
  return
239
244
  end
@@ -287,7 +292,7 @@ module Aspera
287
292
  query['page'] = 0
288
293
  end
289
294
  loop do
290
- RestParameters.instance.spinner_cb.call(folder_items.count)
295
+ Rest::Parameters.instance.spinner_cb.call(folder_items.count)
291
296
  data, http = read("files/#{file_id}/files", query, headers: headers, ret: :both)
292
297
  folder_items.concat(data)
293
298
  if use_v4
@@ -304,7 +309,7 @@ module Aspera
304
309
  Log.log.warn { "#{path || file_id}: #{e.class} #{e.message}" }
305
310
  Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
306
311
  ensure
307
- RestParameters.instance.spinner_cb.call(folder_items.count, action: :success)
312
+ Rest::Parameters.instance.spinner_cb.call(folder_items.count, action: :success)
308
313
  end
309
314
  folder_items
310
315
  end
@@ -326,7 +331,7 @@ module Aspera
326
331
  until folders_to_explore.empty?
327
332
  # Consume first in job list
328
333
  current_item = folders_to_explore.shift
329
- Log.log.debug { "Exploring #{current_item[:path]}".bg_green }
334
+ Log.log.debug { "Exploring #{current_item[:path]}".bg(:green) }
330
335
  # Get folder content
331
336
  folder_contents = read_folder_content(current_item[:id], query, exception: false, path: current_item[:path])
332
337
  Log.dump(:folder_contents, folder_contents)
@@ -452,7 +457,7 @@ module Aspera
452
457
  create(
453
458
  'files/download_setup',
454
459
  {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
455
- )['transfer_specs'].first['transfer_spec']
460
+ )['transfer_specs'].first&.fetch('transfer_spec')
456
461
  end
457
462
 
458
463
  # Get generic part of transfer spec with transport parameters only
@@ -527,7 +532,7 @@ module Aspera
527
532
 
528
533
  # Executes `GET` call in loop using `iteration_token` (`/ops/transfers`)
529
534
  # @param iteration [Array] a single element array with the iteration token or nil
530
- # @param call_args [Hash] additional arguments to pass to `Rest.call`
535
+ # @param call_args [Hash] additional arguments to pass to `Rest::Client#call`
531
536
  # @return [Array] list of items returned by the API call
532
537
  def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
533
538
  Aspera.assert_type(iteration, Array, NilClass) { 'iteration' }
@@ -535,7 +540,7 @@ module Aspera
535
540
  Aspera.assert(!call_args.key?(:query), ':query must not be in call_args (use query parameter)')
536
541
  query = {} if query.nil?
537
542
  query[:iteration_token] = iteration[0] unless iteration.nil? || iteration[0].nil?
538
- max = query.delete(RestList::MAX_ITEMS)
543
+ max = query.delete(Rest::List::MAX_ITEMS)
539
544
  # Return empty list immediately if max is 0
540
545
  return [] if max&.zero?
541
546
  item_list = []
@@ -551,7 +556,7 @@ module Aspera
551
556
  break
552
557
  end
553
558
  # Update progress spinner
554
- RestParameters.instance.spinner_cb.call(item_list.length)
559
+ Rest::Parameters.instance.spinner_cb.call(item_list.length)
555
560
  # Parse Link header according to RFC 8288 to extract next iteration token
556
561
  next_url = LinkHeader.parse(http['Link']).find_href(rel: 'next')
557
562
  next_iteration_token = nil
@@ -572,7 +577,7 @@ module Aspera
572
577
  query[:iteration_token] = next_iteration_token
573
578
  end
574
579
  # Signal completion
575
- RestParameters.instance.spinner_cb.call(action: :success)
580
+ Rest::Parameters.instance.spinner_cb.call(action: :success)
576
581
  # save iteration token if needed
577
582
  iteration[0] = query[:iteration_token] unless iteration.nil?
578
583
  item_list
@@ -582,7 +587,7 @@ module Aspera
582
587
  #
583
588
  # @param subpath [String] API Path
584
589
  # @param query [Hash, nil] Optional query parameters for the API request
585
- # @param kwargs [Hash] Other parameters for Rest.read
590
+ # @param kwargs [Hash] Other parameters for Rest::Client#read
586
591
  #
587
592
  # @return [Array<Hash>] List of folder entries (files, folders, links)
588
593
  def read_with_pages(subpath, query = nil, **kwargs)
@@ -592,7 +597,7 @@ module Aspera
592
597
  query['page'] ||= 1
593
598
  suffix = nil
594
599
  loop do
595
- RestParameters.instance.spinner_cb.call("#{items.count}#{suffix}")
600
+ Rest::Parameters.instance.spinner_cb.call("#{items.count}#{suffix}")
596
601
  data, http = read(subpath, query, **kwargs, ret: :both)
597
602
  items.concat(data)
598
603
  break if data.length < query['per_page']
@@ -603,7 +608,7 @@ module Aspera
603
608
  Log.log.warn { "#{e.class} #{e.message}" }
604
609
  Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
605
610
  ensure
606
- RestParameters.instance.spinner_cb.call(items.count, action: :success)
611
+ Rest::Parameters.instance.spinner_cb.call(items.count, action: :success)
607
612
  return items # rubocop:disable Lint/EnsureReturn
608
613
  end
609
614
 
data/lib/aspera/ascmd.rb CHANGED
@@ -3,9 +3,11 @@
3
3
  # cspell:ignore ascmd smode errstr zstr zmode zuid zgid zctime zatime zmtime fcount dcount btype blist codeset lc_ctype ascmdtypes
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
+ require 'aspera/rainbow'
7
+ using Rainbow
6
8
 
7
9
  module Aspera
8
- # Run +ascmd+ commands using specified executor (usually, remotely on transfer node)
10
+ # Run `ascmd` commands using specified executor (usually, remotely on transfer node)
9
11
  # Equivalent of SDK "command client"
10
12
  # execute: "ascmd -h" to get syntax
11
13
  # Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
@@ -42,6 +42,14 @@ module Aspera
42
42
  # policy for product selection
43
43
  FIRST_FOUND = 'FIRST'
44
44
 
45
+ class << self
46
+ # @param value [String, nil] value of option `sdk_folder`
47
+ # @return [Boolean] true if value selects a product instead of a folder
48
+ def product_selector?(value)
49
+ value.is_a?(String) && value.start_with?(USE_PRODUCT_PREFIX)
50
+ end
51
+ end
52
+
45
53
  # Loads YAML from cloud with locations of SDK archives for all platforms
46
54
  # @return [Hash] location structure
47
55
  def sdk_locations
@@ -55,30 +63,25 @@ module Aspera
55
63
  end
56
64
  end
57
65
 
58
- # Set `ascp` executable "location"
66
+ # Set `ascp` executable "location" (option `sdk_folder`)
59
67
  # It can be:
60
- # - Full path to folder where `ascp` executable is located
61
- # - "product:PRODUCT_NAME" to use ascp from named product
68
+ # - Full path to folder where `ascp` executable is located, also used as SDK folder (keys, conf)
69
+ # - "product:PRODUCT_NAME" to use ascp from named product, SDK folder stays the default one
62
70
  # - "product:FIRST" to use ascp from first found product
71
+ # @param ascp_location [String] folder or product selector
72
+ # @return [nil]
63
73
  def sdk_folder=(ascp_location)
64
74
  Aspera.assert_type(ascp_location, String) { 'ascp_location' }
65
75
  Aspera.assert(!ascp_location.empty?, 'ascp location cannot be empty: check your config file')
66
- folder =
67
- if ascp_location.start_with?(USE_PRODUCT_PREFIX)
68
- product_name = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
69
- if product_name.eql?(FIRST_FOUND)
70
- pl = installed_products.first
71
- Aspera.assert(!pl.nil?) { "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" }
72
- else
73
- pl = installed_products.find { |i| i[:name].eql?(product_name) }
74
- Aspera.assert(!pl.nil?) { "No such product installed: #{product_name}" }
75
- end
76
- File.dirname(pl[:ascp_path])
77
- else
78
- ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
79
- end
80
- Log.log.debug { "ascp_folder=#{folder}" }
81
- Products::Transferd.sdk_directory = folder
76
+ @ascp_folder = nil
77
+ if self.class.product_selector?(ascp_location)
78
+ # Resolved on first use: product scan requires the SDK folder, which is set later
79
+ @ascp_product = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
80
+ Log.log.debug { "ascp_product=#{@ascp_product}" }
81
+ else
82
+ @ascp_product = nil
83
+ Products::Transferd.sdk_directory = ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
84
+ end
82
85
  nil
83
86
  end
84
87
 
@@ -114,8 +117,9 @@ module Aspera
114
117
  case file_type
115
118
  when *EXE_FILES
116
119
  file_is_required = file_type.eql?(:ascp)
117
- file = Products::Transferd.transferd_path
118
- file = File.join(File.dirname(file), Environment.instance.exe_file(file_type.to_s)) unless file_type.eql?(:transferd)
120
+ file = sdk_exe_path(file_type)
121
+ # transferd always comes from SDK
122
+ file = File.join(product_ascp_folder, File.basename(file)) unless @ascp_product.nil? || file_type.eql?(:transferd)
119
123
  when :ssh_private_dsa, :ssh_private_rsa
120
124
  # assume last 3 letters are type
121
125
  type = file_type.to_s[-3..].to_sym
@@ -282,7 +286,7 @@ module Aspera
282
286
  end
283
287
  end
284
288
 
285
- # Downloads and extracts the SDK archive for the current platform into +folder+
289
+ # Downloads and extracts the SDK archive for the current platform into `folder`
286
290
  # @param folder [String] Destination folder path
287
291
  # @param url [nil, String] URL to SDK archive, if nil: default url for version
288
292
  # @param version [nil, String] Specific version, if nil: latest version
@@ -306,9 +310,9 @@ module Aspera
306
310
  if UriReader.file?(url)
307
311
  archive_io.write(File.binread(UriReader.file_path(url)))
308
312
  else
309
- Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
310
- archive_io.rewind
313
+ Rest::Client.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
311
314
  end
315
+ archive_io.rewind
312
316
  extract_archive_files(url, archive_io) do |entry_name, entry_stream, link_target|
313
317
  dest_folder = if block_given?
314
318
  yield(entry_name)
@@ -385,10 +389,13 @@ module Aspera
385
389
  folder = Products::Transferd.sdk_directory
386
390
  download_sdk(folder: folder, url: url, version: version)
387
391
  # Ensure necessary files are there, or generate them, restrict file access on SDK executables
392
+ # (executables of SDK, even if `ascp` of another product is selected)
388
393
  SDK_FILES.each do |file_id_sym|
389
- file_path = path(file_id_sym)
390
- if file_path && EXE_FILES.include?(file_id_sym)
394
+ if EXE_FILES.include?(file_id_sym)
395
+ file_path = sdk_exe_path(file_id_sym)
391
396
  Environment.restrict_file_access(file_path, mode: 0o755) if File.exist?(file_path)
397
+ else
398
+ path(file_id_sym)
392
399
  end
393
400
  end
394
401
  # Generate meta data XML file in SDK if needed, based on actual binaries versions
@@ -399,8 +406,8 @@ module Aspera
399
406
  sdk_name = meta_data.scan(%r{<name>(.*?)</name>}).flatten.first || 'unknown'
400
407
  sdk_version = meta_data.scan(%r{<version>(.*?)</version>}).flatten.first || 'unknown'
401
408
  else
402
- sdk_ascp_version = get_ascp_version(path(:ascp))
403
- transferd_version = get_exe_version(path(:transferd), 'version')
409
+ sdk_ascp_version = get_ascp_version(sdk_exe_path(:ascp))
410
+ transferd_version = get_exe_version(sdk_exe_path(:transferd), 'version')
404
411
  sdk_name = 'IBM Aspera Transfer SDK'
405
412
  sdk_version = transferd_version || sdk_ascp_version
406
413
  File.write(meta_data_file, "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
@@ -435,6 +442,34 @@ module Aspera
435
442
  # cache for installed products found
436
443
  @found_products = nil
437
444
  @transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
445
+ # product selected with option `sdk_folder`, or nil to use SDK folder
446
+ @ascp_product = nil
447
+ # cache for folder of `ascp` of selected product
448
+ @ascp_folder = nil
449
+ end
450
+
451
+ # @param file_type [Symbol] one of EXE_FILES
452
+ # @return [String] path to executable in SDK folder
453
+ def sdk_exe_path(file_type)
454
+ file = Products::Transferd.transferd_path
455
+ file = File.join(File.dirname(file), Environment.instance.exe_file(file_type.to_s)) unless file_type.eql?(:transferd)
456
+ return file
457
+ end
458
+
459
+ # @return [String] folder of `ascp` of product selected with option `sdk_folder`
460
+ def product_ascp_folder
461
+ if @ascp_folder.nil?
462
+ if @ascp_product.eql?(FIRST_FOUND)
463
+ pl = installed_products.first
464
+ Aspera.assert(!pl.nil?) { "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli config ascp install" }
465
+ else
466
+ pl = installed_products.find { |i| i[:name].eql?(@ascp_product) }
467
+ Aspera.assert(!pl.nil?) { "No such product installed: #{@ascp_product}" }
468
+ end
469
+ @ascp_folder = File.dirname(pl[:ascp_path])
470
+ Log.log.debug { "ascp_folder=#{@ascp_folder}" }
471
+ end
472
+ return @ascp_folder
438
473
  end
439
474
 
440
475
  public
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/assert'
4
+ require 'aspera/string_ext'
4
5
 
5
6
  module Aspera
6
7
  module Ascp
data/lib/aspera/assert.rb CHANGED
@@ -17,6 +17,10 @@ module Aspera
17
17
  class AssertError < Error
18
18
  end
19
19
 
20
+ # Raised when a looked up entity is not found
21
+ class EntityNotFound < Error
22
+ end
23
+
20
24
  class << self
21
25
  # Replaces `raise` in assertion
22
26
  # Allows sending exception, or just error log, when type is `:error`