aspera-cli 4.17.0 → 4.18.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -4
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +711 -432
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/examples/build_package.sh +28 -0
  9. data/lib/aspera/agent/alpha.rb +10 -8
  10. data/lib/aspera/agent/base.rb +9 -6
  11. data/lib/aspera/agent/connect.rb +7 -8
  12. data/lib/aspera/agent/direct.rb +56 -37
  13. data/lib/aspera/agent/httpgw.rb +23 -324
  14. data/lib/aspera/agent/node.rb +19 -20
  15. data/lib/aspera/agent/trsdk.rb +19 -20
  16. data/lib/aspera/api/aoc.rb +17 -14
  17. data/lib/aspera/api/cos_node.rb +4 -4
  18. data/lib/aspera/api/httpgw.rb +342 -0
  19. data/lib/aspera/api/node.rb +135 -89
  20. data/lib/aspera/ascmd.rb +4 -3
  21. data/lib/aspera/ascp/installation.rb +15 -7
  22. data/lib/aspera/ascp/management.rb +2 -2
  23. data/lib/aspera/ascp/products.rb +1 -1
  24. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  25. data/lib/aspera/cli/extended_value.rb +35 -16
  26. data/lib/aspera/cli/formatter.rb +161 -70
  27. data/lib/aspera/cli/hints.rb +18 -0
  28. data/lib/aspera/cli/main.rb +32 -39
  29. data/lib/aspera/cli/manager.rb +151 -119
  30. data/lib/aspera/cli/plugin.rb +27 -21
  31. data/lib/aspera/cli/plugin_factory.rb +31 -20
  32. data/lib/aspera/cli/plugins/alee.rb +14 -2
  33. data/lib/aspera/cli/plugins/aoc.rb +152 -141
  34. data/lib/aspera/cli/plugins/ats.rb +1 -1
  35. data/lib/aspera/cli/plugins/config.rb +72 -65
  36. data/lib/aspera/cli/plugins/console.rb +8 -5
  37. data/lib/aspera/cli/plugins/faspex.rb +32 -23
  38. data/lib/aspera/cli/plugins/faspex5.rb +232 -156
  39. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  40. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  41. data/lib/aspera/cli/plugins/node.rb +129 -64
  42. data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
  43. data/lib/aspera/cli/plugins/preview.rb +7 -3
  44. data/lib/aspera/cli/plugins/server.rb +6 -6
  45. data/lib/aspera/cli/plugins/shares.rb +16 -14
  46. data/lib/aspera/cli/special_values.rb +13 -0
  47. data/lib/aspera/cli/sync_actions.rb +10 -10
  48. data/lib/aspera/cli/transfer_agent.rb +7 -6
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/environment.rb +70 -9
  51. data/lib/aspera/faspex_gw.rb +5 -4
  52. data/lib/aspera/faspex_postproc.rb +2 -2
  53. data/lib/aspera/log.rb +6 -3
  54. data/lib/aspera/node_simulator.rb +2 -2
  55. data/lib/aspera/oauth/base.rb +31 -19
  56. data/lib/aspera/oauth/factory.rb +12 -13
  57. data/lib/aspera/oauth/generic.rb +1 -0
  58. data/lib/aspera/oauth/jwt.rb +18 -15
  59. data/lib/aspera/oauth/url_json.rb +8 -6
  60. data/lib/aspera/oauth/web.rb +2 -2
  61. data/lib/aspera/persistency_folder.rb +2 -2
  62. data/lib/aspera/preview/generator.rb +3 -3
  63. data/lib/aspera/preview/options.rb +3 -3
  64. data/lib/aspera/preview/terminal.rb +4 -4
  65. data/lib/aspera/preview/utils.rb +3 -3
  66. data/lib/aspera/proxy_auto_config.rb +5 -1
  67. data/lib/aspera/rest.rb +105 -88
  68. data/lib/aspera/rest_call_error.rb +1 -1
  69. data/lib/aspera/rest_error_analyzer.rb +2 -2
  70. data/lib/aspera/rest_errors_aspera.rb +1 -1
  71. data/lib/aspera/resumer.rb +1 -1
  72. data/lib/aspera/secret_hider.rb +2 -4
  73. data/lib/aspera/ssh.rb +1 -1
  74. data/lib/aspera/transfer/parameters.rb +39 -36
  75. data/lib/aspera/transfer/spec.rb +2 -0
  76. data/lib/aspera/transfer/sync.rb +2 -1
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +5 -4
  79. data/lib/aspera/web_auth.rb +1 -1
  80. data/lib/aspera/web_server_simple.rb +4 -3
  81. data.tar.gz.sig +0 -0
  82. metadata +7 -4
  83. metadata.gz.sig +0 -0
  84. data/lib/aspera/cli/plugins/bss.rb +0 -71
  85. data/lib/aspera/open_application.rb +0 -71
@@ -14,21 +14,28 @@ 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
18
- ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
17
+ SCOPE_SEPARATOR = ':'
18
+ SCOPE_NODE_PREFIX = 'node.'
19
19
  # prefix for ruby code for filter (deprecated)
20
20
  MATCH_EXEC_PREFIX = 'exec:'
21
21
  MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
22
+ SIGNATURE_DELIMITER = '==SIGNATURE=='
23
+ BEARER_TOKEN_VALIDITY_DEFAULT = 86400
24
+ # fields in @app_info
25
+ REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
26
+ # methods of @app_info[:api]
27
+ REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
28
+ private_constant :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_EXEC_PREFIX, :MATCH_TYPES,
29
+ :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
30
+ :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
31
+
32
+ # node api permissions
33
+ ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
22
34
  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
23
- PATH_SEPARATOR = '/'
24
- TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
35
+ HEADER_X_TOTAL_COUNT = 'X-Total-Count'
25
36
  SCOPE_USER = 'user:all'
26
37
  SCOPE_ADMIN = 'admin:all'
27
- SCOPE_PREFIX = 'node.'
28
- SCOPE_SEPARATOR = ':'
29
- SIGNATURE_DELIMITER = '==SIGNATURE=='
30
- BEARER_TOKEN_VALIDITY_DEFAULT = 86400
31
- BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
38
+ PATH_SEPARATOR = '/'
32
39
 
33
40
  # register node special token decoder
34
41
  OAuth::Factory.instance.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
@@ -57,19 +64,19 @@ module Aspera
57
64
  end
58
65
 
59
66
  def file_matcher_from_argument(options)
60
- return file_matcher(options.get_next_argument('filter', type: MATCH_TYPES, mandatory: false))
67
+ return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
61
68
  end
62
69
 
63
70
  # node API scopes
64
71
  def token_scope(access_key, scope)
65
- return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
72
+ return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
66
73
  end
67
74
 
68
75
  def decode_scope(scope)
69
76
  items = scope.split(SCOPE_SEPARATOR, 2)
70
77
  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]}
78
+ Aspera.assert(items[0].start_with?(SCOPE_NODE_PREFIX)){"invalid scope: #{scope}"}
79
+ return {access_key: items[0][SCOPE_NODE_PREFIX.length..-1], scope: items[1]}
73
80
  end
74
81
 
75
82
  # Create an Aspera Node bearer token
@@ -84,7 +91,7 @@ module Aspera
84
91
  # manage convenience parameters
85
92
  expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
86
93
  payload.delete('_validity')
87
- scope = payload['_scope'] || BEARER_TOKEN_SCOPE_DEFAULT
94
+ scope = payload['_scope'] || SCOPE_USER
88
95
  payload.delete('_scope')
89
96
  payload['scope'] ||= token_scope(access_key, scope)
90
97
  payload['auth_type'] ||= 'access_key'
@@ -115,25 +122,20 @@ module Aspera
115
122
  end
116
123
  end
117
124
 
118
- # fields in @app_info
119
- REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
120
- # methods of @app_info[:api]
121
- REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
122
- private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
123
-
124
125
  attr_reader :app_info
125
126
 
127
+ # @param app_info [Hash,NilClass] Special processing for AoC
128
+ # @param add_tspec [Hash,NilClass] Additional transfer spec
126
129
  # @param base_url [String] Rest parameters
127
130
  # @param auth [String,NilClass] Rest parameters
128
131
  # @param headers [String,NilClass] Rest parameters
129
- # @param app_info [Hash,NilClass] Special processing for AoC
130
- # @param add_tspec [Hash,NilClass] Additional transfer spec
131
132
  def initialize(app_info: nil, add_tspec: nil, **rest_args)
132
133
  # init Rest
133
134
  super(**rest_args)
134
135
  @app_info = app_info
135
136
  # this is added to transfer spec, for instance to add tags (COS)
136
137
  @add_tspec = add_tspec
138
+ @std_t_spec_cache = nil
137
139
  if !@app_info.nil?
138
140
  REQUIRED_APP_INFO_FIELDS.each do |field|
139
141
  Aspera.assert(@app_info.key?(field)){"app_info lacks field #{field}"}
@@ -159,25 +161,42 @@ module Aspera
159
161
  workspace_id: @app_info[:workspace_id],
160
162
  workspace_name: @app_info[:workspace_name])
161
163
  end
162
- Log.log.warn{"cannot resolve link with node id #{node_id}"}
164
+ Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
163
165
  return nil
164
166
  end
165
167
 
168
+ # Check if a link entry in folder has target information
169
+ # @param entry [Hash] entry in folder
170
+ # @return [Boolean] true if target information is available
171
+ def entry_has_link_information(entry)
172
+ # if target information is missing in folder, try to get it on entry
173
+ if entry['target_node_id'].nil? || entry['target_id'].nil?
174
+ link_entry = read("files/#{entry['id']}")[:data]
175
+ entry['target_node_id'] = link_entry['target_node_id']
176
+ entry['target_id'] = link_entry['target_id']
177
+ end
178
+ return true unless entry['target_node_id'].nil? || entry['target_id'].nil?
179
+ Log.log.warn{"Missing target information for link: #{entry['name']}"}
180
+ return false
181
+ end
182
+
166
183
  # Recursively browse in a folder (with non-recursive method)
167
184
  # sub folders are processed if the processing method returns true
185
+ # links are processed on the respective node
168
186
  # @param state [Object] state object sent to processing method
169
187
  # @param top_file_id [String] file id to start at (default = access key root file id)
170
188
  # @param top_file_path [String] path of top folder (default = /)
171
189
  # @param block [Proc] processing method, arguments: entry, path, state
172
- def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
190
+ def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
173
191
  Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
174
- Aspera.assert(block){'Missing block'}
192
+ Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
175
193
  # start at top folder
176
194
  folders_to_explore = [{id: top_file_id, path: top_file_path}]
177
195
  Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
178
196
  until folders_to_explore.empty?
197
+ # consume first in job list
179
198
  current_item = folders_to_explore.shift
180
- Log.log.debug{"searching #{current_item[:path]}".bg_green}
199
+ Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
181
200
  # get folder content
182
201
  folder_contents =
183
202
  begin
@@ -189,23 +208,25 @@ module Aspera
189
208
  Log.log.debug{Log.dump(:folder_contents, folder_contents)}
190
209
  folder_contents.each do |entry|
191
210
  relative_path = File.join(current_item[:path], entry['name'])
192
- Log.log.debug{"process_folder_tree checking #{relative_path}"}
193
- # continue only if method returns true
194
- next unless yield(entry, relative_path, state)
211
+ Log.log.debug{"process_folder_tree: checking #{relative_path}"}
212
+ # call block, continue only if method returns true
213
+ next unless send(method_sym, entry, relative_path, state)
195
214
  # entry type is file, folder or link
196
215
  case entry['type']
197
216
  when 'folder'
198
217
  folders_to_explore.push({id: entry['id'], path: relative_path})
199
218
  when 'link'
200
- node_id_to_node(entry['target_node_id'])&.process_folder_tree(
201
- state: state,
202
- top_file_id: entry['target_id'],
203
- top_file_path: relative_path,
204
- &block)
219
+ if entry_has_link_information(entry)
220
+ node_id_to_node(entry['target_node_id'])&.process_folder_tree(
221
+ method_sym: method_sym,
222
+ state: state,
223
+ top_file_id: entry['target_id'],
224
+ top_file_path: relative_path)
225
+ end
205
226
  end
206
227
  end
207
228
  end
208
- end # process_folder_tree
229
+ end
209
230
 
210
231
  # Navigate the path from given file id
211
232
  # @param top_file_id [String] id initial file id
@@ -213,65 +234,44 @@ module Aspera
213
234
  # @return [Hash] {.api,.file_id}
214
235
  def resolve_api_fid(top_file_id, path)
215
236
  Aspera.assert_type(top_file_id, String)
237
+ Aspera.assert_type(path, String)
238
+ # if last element is a link and followed by "/", we list the content of that folder, else we return the link
216
239
  process_last_link = path.end_with?(PATH_SEPARATOR)
240
+ # keep only non-empty elements
217
241
  path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
218
242
  return {api: self, file_id: top_file_id} if path_elements.empty?
219
- resolve_state = {path: path_elements, result: nil}
220
- process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
221
- # this block is called recursively for each entry in folder
222
- # stop digging here if not in right path
223
- next false unless entry['name'].eql?(state[:path].first)
224
- # ok it matches, so we remove the match
225
- state[:path].shift
226
- case entry['type']
227
- when 'file'
228
- # file must be terminal
229
- raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
230
- # it's terminal, we found it
231
- state[:result] = {api: self, file_id: entry['id']}
232
- next false
233
- when 'folder'
234
- if state[:path].empty?
235
- # we found it
236
- state[:result] = {api: self, file_id: entry['id']}
237
- next false
238
- end
239
- when 'link'
240
- if state[:path].empty?
241
- if process_last_link
242
- # we found it
243
- other_node = node_id_to_node(entry['target_node_id'])
244
- raise 'cannot resolve link' if other_node.nil?
245
- state[:result] = {api: other_node, file_id: entry['target_id']}
246
- else
247
- # we found it but we do not process the link
248
- state[:result] = {api: self, file_id: entry['id']}
249
- end
250
- next false
251
- end
252
- else
253
- Log.log.warn{"Unknown element type: #{entry['type']}"}
254
- end
255
- # continue to dig folder
256
- next true
257
- end
243
+ resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
244
+ process_folder_tree(method_sym: :process_api_fid, state: resolve_state, top_file_id: top_file_id)
258
245
  raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
246
+ Log.log.debug{"resolve_api_fid: #{path} -> #{resolve_state[:result][:api].base_url} #{resolve_state[:result][:file_id]}"}
259
247
  return resolve_state[:result]
260
248
  end
261
249
 
262
250
  def find_files(top_file_id, test_block)
263
251
  Log.log.debug{"find_files: file id=#{top_file_id}"}
264
252
  find_state = {found: [], test_block: test_block}
265
- process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
266
- state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
267
- # test all files deeply
268
- true
269
- end
253
+ process_folder_tree(method_sym: :process_find_files, state: find_state, top_file_id: top_file_id)
270
254
  return find_state[:found]
271
255
  end
272
256
 
273
257
  def refreshed_transfer_token
274
- return oauth_token(force_refresh: true)
258
+ return oauth.token(refresh: true)
259
+ end
260
+
261
+ # @return part of transfer spec with transport parameters only
262
+ def transport_params
263
+ if @std_t_spec_cache.nil?
264
+ # retrieve values from API (and keep a copy/cache)
265
+ full_spec = create(
266
+ 'files/download_setup',
267
+ {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
268
+ )[:data]['transfer_specs'].first['transfer_spec']
269
+ # set available fields
270
+ @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
271
+ h[i] = full_spec[i] if full_spec.key?(i)
272
+ end
273
+ end
274
+ return @std_t_spec_cache
275
275
  end
276
276
 
277
277
  # Create transfer spec for gen4
@@ -285,9 +285,9 @@ module Aspera
285
285
  ak_token = Rest.basic_token(auth_params[:username], auth_params[:password])
286
286
  when :oauth2
287
287
  ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
288
- # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
288
+ # TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
289
289
  # get bearer token, possibly use cache
290
- ak_token = oauth_token(force_refresh: false)
290
+ ak_token = oauth.token
291
291
  else Aspera.error_unexpected_value(auth_params[:type])
292
292
  end
293
293
  transfer_spec = {
@@ -327,18 +327,64 @@ module Aspera
327
327
  transfer_spec[i] = settings[i] if settings.key?(i)
328
328
  end if settings.is_a?(Hash)
329
329
  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)}
330
+ transfer_spec.merge!(transport_params)
337
331
  end
338
332
  Log.log.warn{"Expected transfer user: #{Transfer::Spec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
339
333
  unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
340
334
  return transfer_spec
341
335
  end
336
+
337
+ private
338
+
339
+ def process_api_fid(entry, path, state)
340
+ # this block is called recursively for each entry in folder
341
+ # stop digging here if not in right path
342
+ return false unless entry['name'].eql?(state[:path].first)
343
+ # ok it matches, so we remove the match, and continue digging
344
+ state[:path].shift
345
+ path_fully_consumed = state[:path].empty?
346
+ case entry['type']
347
+ when 'file'
348
+ # file must be terminal
349
+ raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless path_fully_consumed
350
+ # it's terminal, we found it
351
+ Log.log.debug{"resolve_api_fid: found #{path} -> #{entry['id']}"}
352
+ state[:result] = {api: self, file_id: entry['id']}
353
+ return false
354
+ when 'folder'
355
+ if path_fully_consumed
356
+ # we found it
357
+ state[:result] = {api: self, file_id: entry['id']}
358
+ return false
359
+ end
360
+ when 'link'
361
+ if path_fully_consumed
362
+ if state[:process_last_link]
363
+ # we found it
364
+ other_node = nil
365
+ if entry_has_link_information(entry)
366
+ other_node = node_id_to_node(entry['target_node_id'])
367
+ end
368
+ raise 'Cannot resolve link' if other_node.nil?
369
+ state[:result] = {api: other_node, file_id: entry['target_id']}
370
+ else
371
+ # we found it but we do not process the link
372
+ state[:result] = {api: self, file_id: entry['id']}
373
+ end
374
+ return false
375
+ end
376
+ else
377
+ Log.log.warn{"Unknown element type: #{entry['type']}"}
378
+ end
379
+ # continue to dig folder
380
+ return true
381
+ end
382
+
383
+ def process_find_files(entry, _path, state)
384
+ state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
385
+ # test all files deeply
386
+ return true
387
+ end
342
388
  end
343
389
  end
344
390
  end
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
@@ -44,7 +44,7 @@ module Aspera
44
44
  app_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server'),
45
45
  log_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server', 'var', 'log')
46
46
  }]
47
- when Aspera::Environment::OS_X then [{
47
+ when Aspera::Environment::OS_MACOS then [{
48
48
  expected: CONNECT,
49
49
  app_root: File.join(Dir.home, 'Applications', 'Aspera Connect.app'),
50
50
  log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # cspell:ignore csvt jsonpp
3
+ # cspell:ignore csvt jsonpp stdbin
4
4
  require 'aspera/uri_reader'
5
5
  require 'aspera/environment'
6
6
  require 'aspera/log'
@@ -17,10 +17,9 @@ module Aspera
17
17
  class ExtendedValue
18
18
  include Singleton
19
19
 
20
- # special values
21
- INIT = 'INIT'
22
- ALL = 'ALL'
23
- DEF = 'DEF'
20
+ MARKER_START = '@'
21
+ MARKER_END = ':'
22
+ MARKER_IN_END = '@'
24
23
 
25
24
  class << self
26
25
  # decode comma separated table text
@@ -61,18 +60,31 @@ module Aspera
61
60
  list: lambda{|v|v[1..-1].split(v[0])},
62
61
  none: lambda{|v|ExtendedValue.assert_no_value(v, :none); nil}, # rubocop:disable Style/Semicolon
63
62
  path: lambda{|v|File.expand_path(v)},
64
- re: lambda{|v|Regexp.new(v)},
63
+ re: lambda{|v|Regexp.new(v, Regexp::MULTILINE)},
65
64
  ruby: lambda{|v|Environment.secure_eval(v, __FILE__, __LINE__)},
66
65
  secret: lambda{|v|prompt = v.empty? ? 'secret' : v; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
67
66
  stdin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
67
+ stdbin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
68
68
  yaml: lambda{|v|YAML.load(v)},
69
69
  zlib: lambda{|v|Zlib::Inflate.inflate(v)},
70
70
  extend: lambda{|v|ExtendedValue.instance.evaluate_all(v)}
71
71
  }
72
+ @default_decoder = nil
73
+ end
74
+
75
+ # Regex to match an extended value
76
+ def handler_regex_string
77
+ "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
72
78
  end
73
79
 
74
80
  public
75
81
 
82
+ def default_decoder=(value)
83
+ Log.log.debug{"setting default decoder to #{value} (#{value.class})"}
84
+ Aspera.assert(value.nil? || @handlers.key?(value))
85
+ @default_decoder = value
86
+ end
87
+
76
88
  def modifiers; @handlers.keys; end
77
89
 
78
90
  # add a new handler
@@ -82,16 +94,13 @@ module Aspera
82
94
  @handlers[name] = method
83
95
  end
84
96
 
85
- # Regex to match an extended value
86
- def ext_re
87
- "@(#{modifiers.join('|')}):"
88
- end
89
-
90
- # parse an option value if it is a String using supported extended value modifiers
97
+ # parse an string value to extended value, if it is a String using supported extended value modifiers
91
98
  # other value types are returned as is
99
+ # @param value [String] the value to parse
100
+ # @param expect [Class,Array] one or a list of expected types
92
101
  def evaluate(value)
93
102
  return value unless value.is_a?(String)
94
- regex = Regexp.new("^#{ext_re}(.*)$")
103
+ regex = Regexp.new("^#{handler_regex_string}(.*)$", Regexp::MULTILINE)
95
104
  # first determine decoders, in reversed order
96
105
  handlers_reversed = []
97
106
  while (m = value.match(regex))
@@ -101,18 +110,28 @@ module Aspera
101
110
  # stop processing if handler is extend (it will be processed later)
102
111
  break if handler.eql?(:extend)
103
112
  end
113
+ Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
104
114
  handlers_reversed.each do |handler|
105
115
  value = @handlers[handler].call(value)
106
116
  end
107
117
  return value
108
- end # evaluate
118
+ end
119
+
120
+ # parse string value as extended value
121
+ # use default decoder if none is specified
122
+ def evaluate_with_default(value)
123
+ if value.is_a?(String) && value.match(/^#{handler_regex_string}.*$/).nil? && !@default_decoder.nil?
124
+ value = [MARKER_START, @default_decoder, MARKER_END, value].join
125
+ end
126
+ return evaluate(value)
127
+ end
109
128
 
110
129
  # find inner extended values
111
130
  def evaluate_all(value)
112
- regex = Regexp.new("^(.*)#{ext_re}([^@]*)@(.*)$")
131
+ regex = Regexp.new("^(.*)#{handler_regex_string}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
113
132
  while (m = value.match(regex))
114
133
  sub_value = "@#{m[2]}:#{m[3]}"
115
- Log.log.debug("evaluating #{sub_value}")
134
+ Log.log.debug{"evaluating #{sub_value}"}
116
135
  value = m[1] + evaluate(sub_value) + m[4]
117
136
  end
118
137
  return value