aspera-cli 4.17.0 → 4.18.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 (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -4
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +620 -378
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/lib/aspera/agent/alpha.rb +6 -4
  9. data/lib/aspera/agent/base.rb +9 -6
  10. data/lib/aspera/agent/connect.rb +4 -4
  11. data/lib/aspera/agent/direct.rb +56 -37
  12. data/lib/aspera/agent/httpgw.rb +23 -324
  13. data/lib/aspera/agent/node.rb +19 -20
  14. data/lib/aspera/agent/trsdk.rb +19 -20
  15. data/lib/aspera/api/aoc.rb +17 -14
  16. data/lib/aspera/api/cos_node.rb +4 -4
  17. data/lib/aspera/api/httpgw.rb +339 -0
  18. data/lib/aspera/api/node.rb +34 -21
  19. data/lib/aspera/ascmd.rb +4 -3
  20. data/lib/aspera/ascp/installation.rb +15 -7
  21. data/lib/aspera/ascp/management.rb +2 -2
  22. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  23. data/lib/aspera/cli/extended_value.rb +12 -6
  24. data/lib/aspera/cli/formatter.rb +155 -65
  25. data/lib/aspera/cli/hints.rb +18 -0
  26. data/lib/aspera/cli/main.rb +22 -29
  27. data/lib/aspera/cli/manager.rb +53 -36
  28. data/lib/aspera/cli/plugin.rb +26 -17
  29. data/lib/aspera/cli/plugin_factory.rb +31 -20
  30. data/lib/aspera/cli/plugins/alee.rb +14 -2
  31. data/lib/aspera/cli/plugins/aoc.rb +141 -131
  32. data/lib/aspera/cli/plugins/ats.rb +1 -1
  33. data/lib/aspera/cli/plugins/config.rb +52 -46
  34. data/lib/aspera/cli/plugins/console.rb +8 -5
  35. data/lib/aspera/cli/plugins/faspex.rb +27 -19
  36. data/lib/aspera/cli/plugins/faspex5.rb +222 -149
  37. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  38. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  39. data/lib/aspera/cli/plugins/node.rb +86 -29
  40. data/lib/aspera/cli/plugins/orchestrator.rb +31 -29
  41. data/lib/aspera/cli/plugins/preview.rb +6 -2
  42. data/lib/aspera/cli/plugins/server.rb +5 -5
  43. data/lib/aspera/cli/plugins/shares.rb +16 -14
  44. data/lib/aspera/cli/sync_actions.rb +6 -6
  45. data/lib/aspera/cli/transfer_agent.rb +5 -4
  46. data/lib/aspera/cli/version.rb +1 -1
  47. data/lib/aspera/environment.rb +7 -6
  48. data/lib/aspera/faspex_gw.rb +5 -4
  49. data/lib/aspera/faspex_postproc.rb +2 -2
  50. data/lib/aspera/log.rb +6 -3
  51. data/lib/aspera/node_simulator.rb +2 -2
  52. data/lib/aspera/oauth/base.rb +31 -19
  53. data/lib/aspera/oauth/factory.rb +12 -13
  54. data/lib/aspera/oauth/generic.rb +1 -0
  55. data/lib/aspera/oauth/jwt.rb +18 -15
  56. data/lib/aspera/oauth/url_json.rb +8 -6
  57. data/lib/aspera/open_application.rb +5 -7
  58. data/lib/aspera/persistency_folder.rb +2 -2
  59. data/lib/aspera/preview/generator.rb +3 -3
  60. data/lib/aspera/preview/options.rb +3 -3
  61. data/lib/aspera/preview/terminal.rb +4 -4
  62. data/lib/aspera/preview/utils.rb +3 -3
  63. data/lib/aspera/proxy_auto_config.rb +5 -1
  64. data/lib/aspera/rest.rb +60 -74
  65. data/lib/aspera/rest_call_error.rb +1 -1
  66. data/lib/aspera/rest_error_analyzer.rb +2 -2
  67. data/lib/aspera/rest_errors_aspera.rb +1 -1
  68. data/lib/aspera/resumer.rb +1 -1
  69. data/lib/aspera/secret_hider.rb +2 -4
  70. data/lib/aspera/ssh.rb +1 -1
  71. data/lib/aspera/transfer/parameters.rb +39 -36
  72. data/lib/aspera/transfer/spec.rb +2 -0
  73. data/lib/aspera/transfer/sync.rb +2 -1
  74. data/lib/aspera/transfer/uri.rb +1 -1
  75. data/lib/aspera/uri_reader.rb +5 -4
  76. data/lib/aspera/web_auth.rb +1 -1
  77. data/lib/aspera/web_server_simple.rb +4 -3
  78. data.tar.gz.sig +0 -0
  79. metadata +5 -3
  80. metadata.gz.sig +0 -0
  81. data/lib/aspera/cli/plugins/bss.rb +0 -71
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/log'
4
+ require 'aspera/rest'
5
+ require 'aspera/transfer/faux_file'
6
+ require 'aspera/assert'
7
+ require 'securerandom'
8
+ require 'websocket'
9
+ require 'base64'
10
+ require 'json'
11
+
12
+ module Aspera
13
+ module Api
14
+ # Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
15
+ # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
16
+ # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
17
+ # HTTP GW Upload protocol:
18
+ # # type Contents Ack Counter
19
+ # v1
20
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
21
+ # 1.. JSON.slice_upload File base64 chunks "end upload" sent_general
22
+ # v2
23
+ # 0 JSON.transfer_spec Transfer Spec "end upload" sent_general
24
+ # 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
25
+ # 2.. Binary File binary chunks "end upload" sent_general
26
+ # last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
27
+ class Httpgw < Aspera::Rest
28
+ DEFAULT_BASE_PATH = '/aspera/http-gwy'
29
+ INFO_ENDPOINT = 'info'
30
+ MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
31
+ MSG_SEND_SLICE_UPLOAD = 'slice_upload'
32
+ MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
33
+ MSG_RECV_SLICE_UPLOAD_SIGNAL = 'end_slice_upload'
34
+ # upload API versions
35
+ API_V1 = 'v1'
36
+ API_V2 = 'v2'
37
+ THR_RECV = 'recv'
38
+ LOG_WS_SEND = 'ws: send: '.red
39
+ LOG_WS_RECV = "ws: #{THR_RECV}: ".green
40
+ private_constant :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL
41
+ # send message on http gw web socket
42
+ def ws_snd_json(msg_type, payload)
43
+ if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @upload_version.eql?(API_V2)
44
+ @shared_info[:count][:sent_v2_delimiter] += 1
45
+ else
46
+ @shared_info[:count][:sent_general] += 1
47
+ end
48
+ Log.log.debug do
49
+ log_data = payload.dup
50
+ log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
51
+ "#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
52
+ end
53
+ ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
54
+ end
55
+
56
+ # send data on http gw web socket
57
+ def ws_send(ws_type:, data:)
58
+ Log.log.debug{"#{LOG_WS_SEND}sending: #{ws_type} (#{data&.length || 0} bytes)"}
59
+ @shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
60
+ frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
61
+ @ws_io.write(frame_generator.to_s)
62
+ if @synchronous
63
+ @shared_info[:mutex].synchronize do
64
+ # if read thread exited, there will be no more updates
65
+ # we allow for 1 of difference else it stays blocked
66
+ while @ws_read_thread.alive? &&
67
+ @shared_info[:read_exception].nil? &&
68
+ (((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
69
+ ((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
70
+ if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
71
+ Log.log.debug{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
72
+ end
73
+ end
74
+ end
75
+ end
76
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
77
+ Log.log.debug{"#{LOG_WS_SEND}counts: #{@shared_info[:count]}"}
78
+ end
79
+
80
+ # message processing for read thread
81
+ def process_received_message(message)
82
+ Log.log.debug{"#{LOG_WS_RECV}message: [#{message}] (#{message.class})"}
83
+ if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
84
+ @shared_info[:mutex].synchronize do
85
+ @shared_info[:count][:received_general] += 1
86
+ @shared_info[:cond_var].signal
87
+ end
88
+ elsif message.eql?(MSG_RECV_SLICE_UPLOAD_SIGNAL)
89
+ @shared_info[:mutex].synchronize do
90
+ @shared_info[:count][:received_v2_delimiter] += 1
91
+ @shared_info[:cond_var].signal
92
+ end
93
+ else
94
+ message.chomp!
95
+ error_message =
96
+ if message.start_with?('"') && message.end_with?('"')
97
+ # remove double quotes : 1..-2
98
+ JSON.parse(Base64.strict_decode64(message.chomp[1..-2]))['message']
99
+ elsif message.start_with?('{') && message.end_with?('}')
100
+ JSON.parse(message)['message']
101
+ else
102
+ "unknown message from gateway: [#{message}]"
103
+ end
104
+ raise error_message
105
+ end
106
+ end
107
+
108
+ # main function of read thread
109
+ def process_read_thread
110
+ Log.log.debug{"#{LOG_WS_RECV}read thread started"}
111
+ frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
112
+ until @ws_io.eof?
113
+ begin # rubocop:disable Style/RedundantBegin
114
+ # ready byte by byte until frame is ready
115
+ # blocking read
116
+ byte = @ws_io.read(1)
117
+ Log.log.trace1{"#{LOG_WS_RECV}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}"}
118
+ frame_parser << byte
119
+ frame_ok = frame_parser.next
120
+ next if frame_ok.nil?
121
+ process_received_message(frame_ok.data.to_s)
122
+ Log.log.debug{"#{LOG_WS_RECV}counts: #{@shared_info[:count]}"}
123
+ rescue => e
124
+ Log.log.debug{"#{LOG_WS_RECV}Exception: #{e}"}
125
+ @shared_info[:mutex].synchronize do
126
+ @shared_info[:read_exception] = e
127
+ @shared_info[:cond_var].signal
128
+ end
129
+ break
130
+ end
131
+ end
132
+ Log.log.debug do
133
+ "#{LOG_WS_RECV}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
134
+ end unless @shared_info[:read_exception].nil?
135
+ Log.log.debug{"#{LOG_WS_RECV}read thread stopped (ws eof=#{@ws_io.eof?})"}
136
+ end
137
+
138
+ def upload(transfer_spec)
139
+ # identify this session uniquely
140
+ session_id = SecureRandom.uuid
141
+ @notify_cb&.call(session_id: nil, type: :pre_start, info: 'starting')
142
+ # total size of all files
143
+ total_bytes_to_transfer = 0
144
+ # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
145
+ files_to_read = []
146
+ # get source root or nil
147
+ source_root = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root'] : nil
148
+ # source root is ignored by GW, used only here
149
+ transfer_spec.delete('source_root')
150
+ # compute total size of files to upload (for progress)
151
+ # modify transfer spec to be suitable for GW
152
+ transfer_spec['paths'].each do |item|
153
+ # save actual file location to be able read contents later
154
+ file_to_add = Transfer::FauxFile.open(item['source'])
155
+ if file_to_add
156
+ item['source'] = file_to_add.path
157
+ item['file_size'] = file_to_add.size
158
+ else
159
+ file_to_add = item['source']
160
+ # add source root if needed
161
+ file_to_add = File.join(source_root, file_to_add) unless source_root.nil?
162
+ # GW expects a simple file name in 'source' but if user wants to change the name, we take it
163
+ item['source'] = File.basename(item['destination'].nil? ? item['source'] : item['destination'])
164
+ item['file_size'] = File.size(file_to_add)
165
+ end
166
+ # save so that we can actually read the file later
167
+ files_to_read.push(file_to_add)
168
+ total_bytes_to_transfer += item['file_size']
169
+ end
170
+ # TODO: check that this is available in endpoints: @api_info['endpoints']
171
+ upload_url = File.join(@gw_root_url, @upload_version, 'upload')
172
+ @notify_cb&.call(session_id: nil, type: :pre_start, info: 'connecting wss')
173
+ # open web socket to end point (equivalent to Net::HTTP.start)
174
+ http_session = Rest.start_http_session(upload_url)
175
+ # get the underlying socket i/o
176
+ @ws_io = Rest.io_http_session(http_session)
177
+ @ws_handshake = ::WebSocket::Handshake::Client.new(url: upload_url, headers: {})
178
+ @ws_io.write(@ws_handshake.to_s)
179
+ sleep(0.1)
180
+ @ws_handshake << @ws_io.readuntil("\r\n\r\n")
181
+ Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
182
+ Log.log.debug{"#{LOG_WS_SEND}handshake success"}
183
+ # start read thread after handshake
184
+ @ws_read_thread = Thread.new {process_read_thread}
185
+ @notify_cb&.call(session_id: session_id, type: :session_start)
186
+ @notify_cb&.call(session_id: session_id, type: :session_size, info: total_bytes_to_transfer)
187
+ sleep(1)
188
+ # data shared between main thread and read thread
189
+ @shared_info = {
190
+ read_exception: nil, # error message if any in callback
191
+ count: {
192
+ sent_general: 0,
193
+ received_general: 0,
194
+ sent_v2_delimiter: 0,
195
+ received_v2_delimiter: 0
196
+ },
197
+ mutex: Mutex.new,
198
+ cond_var: ConditionVariable.new
199
+ }
200
+ # notify progress bar
201
+ @notify_cb&.call(type: :session_size, session_id: session_id, info: total_bytes_to_transfer)
202
+ # first step send transfer spec
203
+ Log.log.debug{Log.dump(:ws_spec, transfer_spec)}
204
+ ws_snd_json(MSG_SEND_TRANSFER_SPEC, transfer_spec)
205
+ # current file index
206
+ file_index = 0
207
+ # aggregate size sent
208
+ session_sent_bytes = 0
209
+ # process each file
210
+ transfer_spec['paths'].each do |item|
211
+ slice_info = {
212
+ name: nil,
213
+ # TODO: get mime type?
214
+ type: 'application/octet-stream',
215
+ size: item['file_size'],
216
+ slice: 0, # current slice index
217
+ # index of last slice (i.e number of slices - 1)
218
+ last_slice: (item['file_size'] - 1) / @upload_chunk_size,
219
+ fileIndex: file_index
220
+ }
221
+ file = files_to_read[file_index]
222
+ if file.is_a?(Transfer::FauxFile)
223
+ slice_info[:name] = file.path
224
+ else
225
+ file = File.open(file)
226
+ slice_info[:name] = File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
227
+ end
228
+ begin
229
+ until file.eof?
230
+ slice_bin_data = file.read(@upload_chunk_size)
231
+ # interrupt main thread if read thread failed
232
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
233
+ begin
234
+ if @upload_version.eql?(API_V1)
235
+ slice_info[:data] = Base64.strict_encode64(slice_bin_data)
236
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info)
237
+ else
238
+ # send once, before data, at beginning
239
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
240
+ ws_send(ws_type: :binary, data: slice_bin_data)
241
+ Log.log.debug{"#{LOG_WS_SEND}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{slice_info[:last_slice]}"}
242
+ # send once, after data, at end
243
+ ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(slice_info[:last_slice])
244
+ end
245
+ rescue Errno::EPIPE => e
246
+ raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
247
+ raise e
248
+ rescue Net::ReadTimeout => e
249
+ Log.log.warn{'A timeout condition using HTTPGW may signal a permission problem on destination. Check ascp logs on httpgw.'}
250
+ raise e
251
+ end
252
+ session_sent_bytes += slice_bin_data.length
253
+ @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
254
+ slice_info[:slice] += 1
255
+ end
256
+ ensure
257
+ file.close
258
+ end
259
+ file_index += 1
260
+ end
261
+ # throttling may have skipped last one
262
+ @notify_cb&.call(type: :transfer, session_id: session_id, info: session_sent_bytes)
263
+ @notify_cb&.call(type: :end, session_id: session_id)
264
+ ws_send(ws_type: :close, data: nil)
265
+ Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
266
+ @ws_read_thread.join
267
+ Log.log.debug{'Read thread joined'}
268
+ # session no more used
269
+ @ws_io = nil
270
+ http_session&.finish
271
+ end
272
+
273
+ def download(transfer_spec)
274
+ transfer_spec['zip_required'] ||= false
275
+ transfer_spec['source_root'] ||= '/'
276
+ # is normally provided by application, like package name
277
+ if !transfer_spec.key?('download_name')
278
+ # by default it is the name of first file
279
+ download_name = File.basename(transfer_spec['paths'].first['source'], '.*')
280
+ # ands add indication of number of files if there is more than one
281
+ if transfer_spec['paths'].length > 1
282
+ download_name += " #{transfer_spec['paths'].length} Files"
283
+ end
284
+ transfer_spec['download_name'] = download_name
285
+ end
286
+ creation = create('download', {'transfer_spec' => transfer_spec})[:data]
287
+ transfer_uuid = creation['url'].split('/').last
288
+ file_name =
289
+ if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
290
+ # it is a zip file if zip is required or there is more than 1 file
291
+ transfer_spec['download_name'] + '.zip'
292
+ else
293
+ # it is a plain file if we don't require zip and there is only one file
294
+ File.basename(transfer_spec['paths'].first['source'])
295
+ end
296
+ file_path = File.join(transfer_spec['destination_root'], file_name)
297
+ call(operation: 'GET', subpath: "download/#{transfer_uuid}", save_to_file: file_path)
298
+ end
299
+
300
+ def info
301
+ return @api_info
302
+ end
303
+
304
+ # @param url [String] URL of the HTTP Gateway, without version
305
+ def initialize(
306
+ url:,
307
+ api_version: API_V2,
308
+ upload_chunk_size: 64_000,
309
+ synchronous: false,
310
+ notify_cb: nil,
311
+ **opts
312
+ )
313
+ # add scheme if missing
314
+ url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
315
+ raise 'GW URL shall be with scheme https' unless url.start_with?('https://')
316
+ # remove trailing slash and version if any
317
+ url = url.gsub(%r{/+$}, '').gsub(%r{/#{API_V1}$}o, '')
318
+ url = File.join(url, DEFAULT_BASE_PATH) unless url.end_with?(DEFAULT_BASE_PATH)
319
+ @gw_root_url = url
320
+ super(base_url: "#{@gw_root_url}/#{API_V1}", **opts)
321
+ @upload_version = api_version
322
+ @upload_chunk_size = upload_chunk_size
323
+ @synchronous = synchronous
324
+ @notify_cb = notify_cb
325
+ # get API info
326
+ @api_info = read('info')[:data].freeze
327
+ Log.log.debug{Log.dump(:api_info, @api_info)}
328
+ # web socket endpoint: by default use v2 (newer gateways), without base64 encoding
329
+ # is the latest supported? else revert to old api
330
+ if !@upload_version.eql?(API_V1)
331
+ if !@api_info['endpoints'].any?{|i|i.include?(@upload_version)}
332
+ Log.log.warn{"API version #{@upload_version} not supported, reverting to #{API_V1}"}
333
+ @upload_version = API_V1
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
@@ -14,21 +14,23 @@ module Aspera
14
14
  module Api
15
15
  # Provides additional functions using node API with gen4 extensions (access keys)
16
16
  class Node < Aspera::Rest
17
- # permissions
17
+ # node api permissions
18
18
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
19
+ HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
20
+ SCOPE_SEPARATOR = ':'
21
+ SCOPE_USER = 'user:all'
22
+ SCOPE_ADMIN = 'admin:all'
23
+ SCOPE_NODE_PREFIX = 'node.'
19
24
  # prefix for ruby code for filter (deprecated)
20
25
  MATCH_EXEC_PREFIX = 'exec:'
21
26
  MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
22
- HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
23
27
  PATH_SEPARATOR = '/'
24
- TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
25
- SCOPE_USER = 'user:all'
26
- SCOPE_ADMIN = 'admin:all'
27
- SCOPE_PREFIX = 'node.'
28
- SCOPE_SEPARATOR = ':'
29
28
  SIGNATURE_DELIMITER = '==SIGNATURE=='
30
29
  BEARER_TOKEN_VALIDITY_DEFAULT = 86400
31
30
  BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
31
+ private_constant :MATCH_EXEC_PREFIX, :MATCH_TYPES,
32
+ :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT, :BEARER_TOKEN_SCOPE_DEFAULT,
33
+ :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX
32
34
 
33
35
  # register node special token decoder
34
36
  OAuth::Factory.instance.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
@@ -62,14 +64,14 @@ module Aspera
62
64
 
63
65
  # node API scopes
64
66
  def token_scope(access_key, scope)
65
- return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
67
+ return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
66
68
  end
67
69
 
68
70
  def decode_scope(scope)
69
71
  items = scope.split(SCOPE_SEPARATOR, 2)
70
72
  Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
71
- Aspera.assert(items[0].start_with?(SCOPE_PREFIX)){"invalid scope: #{scope}"}
72
- return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
73
+ Aspera.assert(items[0].start_with?(SCOPE_NODE_PREFIX)){"invalid scope: #{scope}"}
74
+ return {access_key: items[0][SCOPE_NODE_PREFIX.length..-1], scope: items[1]}
73
75
  end
74
76
 
75
77
  # Create an Aspera Node bearer token
@@ -134,6 +136,7 @@ module Aspera
134
136
  @app_info = app_info
135
137
  # this is added to transfer spec, for instance to add tags (COS)
136
138
  @add_tspec = add_tspec
139
+ @std_t_spec_cache = nil
137
140
  if !@app_info.nil?
138
141
  REQUIRED_APP_INFO_FIELDS.each do |field|
139
142
  Aspera.assert(@app_info.key?(field)){"app_info lacks field #{field}"}
@@ -205,7 +208,7 @@ module Aspera
205
208
  end
206
209
  end
207
210
  end
208
- end # process_folder_tree
211
+ end
209
212
 
210
213
  # Navigate the path from given file id
211
214
  # @param top_file_id [String] id initial file id
@@ -271,7 +274,23 @@ module Aspera
271
274
  end
272
275
 
273
276
  def refreshed_transfer_token
274
- return oauth_token(force_refresh: true)
277
+ return oauth.token(refresh: true)
278
+ end
279
+
280
+ # @return part of transfer spec with transport parameters only
281
+ def transport_params
282
+ if @std_t_spec_cache.nil?
283
+ # retrieve values from API (and keep a copy/cache)
284
+ full_spec = create(
285
+ 'files/download_setup',
286
+ {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
287
+ )[:data]['transfer_specs'].first['transfer_spec']
288
+ # set available fields
289
+ @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
290
+ h[i] = full_spec[i] if full_spec.key?(i)
291
+ end
292
+ end
293
+ return @std_t_spec_cache
275
294
  end
276
295
 
277
296
  # Create transfer spec for gen4
@@ -285,9 +304,9 @@ module Aspera
285
304
  ak_token = Rest.basic_token(auth_params[:username], auth_params[:password])
286
305
  when :oauth2
287
306
  ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
288
- # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
307
+ # TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
289
308
  # get bearer token, possibly use cache
290
- ak_token = oauth_token(force_refresh: false)
309
+ ak_token = oauth.token(refresh: false)
291
310
  else Aspera.error_unexpected_value(auth_params[:type])
292
311
  end
293
312
  transfer_spec = {
@@ -327,13 +346,7 @@ module Aspera
327
346
  transfer_spec[i] = settings[i] if settings.key?(i)
328
347
  end if settings.is_a?(Hash)
329
348
  else
330
- # retrieve values from API (and keep a copy/cache)
331
- @std_t_spec_cache ||= create(
332
- 'files/download_setup',
333
- {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
334
- )[:data]['transfer_specs'].first['transfer_spec']
335
- # copy some parts
336
- TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
349
+ transfer_spec.merge!(transport_params)
337
350
  end
338
351
  Log.log.warn{"Expected transfer user: #{Transfer::Spec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
339
352
  unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
data/lib/aspera/ascmd.rb CHANGED
@@ -22,6 +22,7 @@ module Aspera
22
22
  mv: 2,
23
23
  rm: 1
24
24
  }.freeze
25
+ private_constant :OPS_ARGS
25
26
  # list of supported actions
26
27
  OPERATIONS = OPS_ARGS.keys.freeze
27
28
 
@@ -94,7 +95,7 @@ module Aspera
94
95
  raise Error.new(result[:errno], result[:errstr], action_sym, arguments) if
95
96
  result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map{|i|i[:name]}.sort)
96
97
  return result
97
- end # execute_single
98
+ end
98
99
 
99
100
  # This exception is raised when +ascmd+ returns an error.
100
101
  class Error < StandardError
@@ -103,7 +104,7 @@ module Aspera
103
104
 
104
105
  def message; "ascmd: #{@errstr} (#{@errno})"; end
105
106
  def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
106
- end # Error
107
+ end
107
108
 
108
109
  # description of result structures (see ascmdtypes.h). Base types are big endian
109
110
  # key = name of type
@@ -211,7 +212,7 @@ module Aspera
211
212
  end
212
213
  end
213
214
  else Aspera.error_unexpected_value(type_descr[:decode])
214
- end # is_a
215
+ end
215
216
  return result
216
217
  end
217
218
  end
@@ -173,8 +173,7 @@ module Aspera
173
173
  return exe_version
174
174
  end
175
175
 
176
- def ascp_info
177
- data = file_paths
176
+ def ascp_add_pvcl(data)
178
177
  # read PATHs from ascp directly, and pvcl modules as well
179
178
  Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
180
179
  last_line = ''
@@ -202,15 +201,24 @@ module Aspera
202
201
  raise last_line
203
202
  end
204
203
  end
205
- # ascp's openssl directory
204
+ end
205
+
206
+ # extract some stings from ascp binary
207
+ def ascp_add_openssl(data)
206
208
  ascp_file = data['ascp']
207
- File.binread(ascp_file).scan(/[\x20-\x7E]{4,}/) do |match|
208
- if (m = match.match(/OPENSSLDIR.*"(.*)"/))
209
+ File.binread(ascp_file).scan(/[\x20-\x7E]{10,}/) do |bin_string|
210
+ if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
209
211
  data['openssldir'] = m[1]
212
+ elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
213
+ data['openssl_version'] = m[1]
210
214
  end
211
215
  end if File.file?(ascp_file)
212
- # log is "-" no need to display
213
- data.delete('log')
216
+ end
217
+
218
+ def ascp_info
219
+ data = file_paths
220
+ ascp_add_pvcl(data)
221
+ ascp_add_openssl(data)
214
222
  return data
215
223
  end
216
224
 
@@ -209,7 +209,7 @@ module Aspera
209
209
  h[new_name] = value
210
210
  end
211
211
  end
212
- end # class << self
212
+ end
213
213
 
214
214
  def initialize
215
215
  # current event being parsed line by line
@@ -236,7 +236,7 @@ module Aspera
236
236
  return @last_event
237
237
  else
238
238
  raise "mgt port: unexpected line: [#{line}]"
239
- end # case
239
+ end
240
240
  return nil
241
241
  end
242
242
  end
@@ -8,10 +8,7 @@ module Aspera
8
8
  # base class for applications supporting basic authentication
9
9
  class BasicAuthPlugin < Cli::Plugin
10
10
  class << self
11
- #@@basic_options_declared = false # rubocop:disable Style/ClassVars
12
- def declare_options(options) # , force: false
13
- #return if @@basic_options_declared && !force
14
- #@@basic_options_declared = true # rubocop:disable Style/ClassVars
11
+ def declare_options(options)
15
12
  options.declare(:url, 'URL of application, e.g. https://faspex.example.com/aspera/faspex')
16
13
  options.declare(:username, "User's name to log in")
17
14
  options.declare(:password, "User's password")
@@ -21,14 +18,13 @@ module Aspera
21
18
 
22
19
  def initialize(basic_options: true, **env)
23
20
  super(**env)
24
- # , force: env[:all_manuals]
25
21
  BasicAuthPlugin.declare_options(options) if basic_options
26
22
  end
27
23
 
28
24
  # returns a Rest object with basic auth
29
25
  def basic_auth_params(subpath=nil)
30
26
  api_url = options.get_option(:url, mandatory: true)
31
- api_url = api_url + '/' + subpath unless subpath.nil?
27
+ api_url = "#{api_url}/#{subpath}" unless subpath.nil?
32
28
  return {
33
29
  base_url: api_url,
34
30
  auth: {
@@ -41,6 +37,6 @@ module Aspera
41
37
  def basic_auth_api(subpath=nil)
42
38
  return Rest.new(**basic_auth_params(subpath))
43
39
  end
44
- end # BasicAuthPlugin
45
- end # Cli
46
- end # Aspera
40
+ end
41
+ end
42
+ end
@@ -22,6 +22,10 @@ module Aspera
22
22
  ALL = 'ALL'
23
23
  DEF = 'DEF'
24
24
 
25
+ MARKER_START = '@'
26
+ MARKER_END = ':'
27
+ MARKER_IN_END = '@'
28
+
25
29
  class << self
26
30
  # decode comma separated table text
27
31
  def decode_csvt(value)
@@ -61,10 +65,11 @@ module Aspera
61
65
  list: lambda{|v|v[1..-1].split(v[0])},
62
66
  none: lambda{|v|ExtendedValue.assert_no_value(v, :none); nil}, # rubocop:disable Style/Semicolon
63
67
  path: lambda{|v|File.expand_path(v)},
64
- re: lambda{|v|Regexp.new(v)},
68
+ re: lambda{|v|Regexp.new(v, Regexp::MULTILINE)},
65
69
  ruby: lambda{|v|Environment.secure_eval(v, __FILE__, __LINE__)},
66
70
  secret: lambda{|v|prompt = v.empty? ? 'secret' : v; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
67
71
  stdin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
72
+ stdbin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
68
73
  yaml: lambda{|v|YAML.load(v)},
69
74
  zlib: lambda{|v|Zlib::Inflate.inflate(v)},
70
75
  extend: lambda{|v|ExtendedValue.instance.evaluate_all(v)}
@@ -84,14 +89,14 @@ module Aspera
84
89
 
85
90
  # Regex to match an extended value
86
91
  def ext_re
87
- "@(#{modifiers.join('|')}):"
92
+ "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
88
93
  end
89
94
 
90
95
  # parse an option value if it is a String using supported extended value modifiers
91
96
  # other value types are returned as is
92
97
  def evaluate(value)
93
98
  return value unless value.is_a?(String)
94
- regex = Regexp.new("^#{ext_re}(.*)$")
99
+ regex = Regexp.new("^#{ext_re}(.*)$", Regexp::MULTILINE)
95
100
  # first determine decoders, in reversed order
96
101
  handlers_reversed = []
97
102
  while (m = value.match(regex))
@@ -101,18 +106,19 @@ module Aspera
101
106
  # stop processing if handler is extend (it will be processed later)
102
107
  break if handler.eql?(:extend)
103
108
  end
109
+ Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
104
110
  handlers_reversed.each do |handler|
105
111
  value = @handlers[handler].call(value)
106
112
  end
107
113
  return value
108
- end # evaluate
114
+ end
109
115
 
110
116
  # find inner extended values
111
117
  def evaluate_all(value)
112
- regex = Regexp.new("^(.*)#{ext_re}([^@]*)@(.*)$")
118
+ regex = Regexp.new("^(.*)#{ext_re}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
113
119
  while (m = value.match(regex))
114
120
  sub_value = "@#{m[2]}:#{m[3]}"
115
- Log.log.debug("evaluating #{sub_value}")
121
+ Log.log.debug{"evaluating #{sub_value}"}
116
122
  value = m[1] + evaluate(sub_value) + m[4]
117
123
  end
118
124
  return value