aspera-cli 4.20.0 → 4.21.2

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 (73) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +41 -3
  4. data/CONTRIBUTING.md +69 -142
  5. data/README.md +687 -461
  6. data/bin/ascli +5 -14
  7. data/bin/asession +3 -5
  8. data/examples/get_proto_file.rb +4 -3
  9. data/examples/proxy.pac +20 -20
  10. data/lib/aspera/agent/base.rb +2 -0
  11. data/lib/aspera/agent/connect.rb +20 -2
  12. data/lib/aspera/agent/{alpha.rb → desktop.rb} +12 -18
  13. data/lib/aspera/agent/direct.rb +30 -31
  14. data/lib/aspera/agent/node.rb +1 -11
  15. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +37 -51
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +13 -8
  18. data/lib/aspera/api/cos_node.rb +1 -1
  19. data/lib/aspera/api/node.rb +49 -32
  20. data/lib/aspera/ascp/installation.rb +98 -77
  21. data/lib/aspera/ascp/management.rb +27 -6
  22. data/lib/aspera/cli/extended_value.rb +9 -3
  23. data/lib/aspera/cli/formatter.rb +155 -154
  24. data/lib/aspera/cli/info.rb +2 -1
  25. data/lib/aspera/cli/main.rb +12 -0
  26. data/lib/aspera/cli/manager.rb +4 -4
  27. data/lib/aspera/cli/plugin.rb +2 -2
  28. data/lib/aspera/cli/plugins/aoc.rb +134 -73
  29. data/lib/aspera/cli/plugins/config.rb +114 -83
  30. data/lib/aspera/cli/plugins/cos.rb +1 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +4 -2
  32. data/lib/aspera/cli/plugins/faspex5.rb +29 -14
  33. data/lib/aspera/cli/plugins/node.rb +51 -41
  34. data/lib/aspera/cli/transfer_progress.rb +2 -0
  35. data/lib/aspera/cli/version.rb +1 -1
  36. data/lib/aspera/command_line_builder.rb +1 -1
  37. data/lib/aspera/coverage.rb +5 -3
  38. data/lib/aspera/environment.rb +59 -16
  39. data/lib/aspera/faspex_postproc.rb +3 -5
  40. data/lib/aspera/hash_ext.rb +2 -12
  41. data/lib/aspera/node_simulator.rb +230 -112
  42. data/lib/aspera/oauth/base.rb +40 -48
  43. data/lib/aspera/oauth/factory.rb +41 -2
  44. data/lib/aspera/oauth/jwt.rb +4 -1
  45. data/lib/aspera/persistency_action_once.rb +1 -1
  46. data/lib/aspera/persistency_folder.rb +20 -2
  47. data/lib/aspera/preview/generator.rb +13 -10
  48. data/lib/aspera/preview/options.rb +2 -2
  49. data/lib/aspera/preview/terminal.rb +1 -1
  50. data/lib/aspera/preview/utils.rb +11 -6
  51. data/lib/aspera/products/connect.rb +82 -0
  52. data/lib/aspera/products/desktop.rb +30 -0
  53. data/lib/aspera/products/other.rb +82 -0
  54. data/lib/aspera/products/transferd.rb +61 -0
  55. data/lib/aspera/rest.rb +22 -17
  56. data/lib/aspera/secret_hider.rb +9 -2
  57. data/lib/aspera/ssh.rb +31 -24
  58. data/lib/aspera/temp_file_manager.rb +5 -4
  59. data/lib/aspera/transfer/parameters.rb +2 -1
  60. data/lib/aspera/transfer/spec.yaml +22 -20
  61. data/lib/aspera/transfer/sync.rb +1 -5
  62. data/lib/aspera/transfer/uri.rb +2 -2
  63. data/lib/aspera/uri_reader.rb +18 -1
  64. data/lib/transferd_pb.rb +86 -0
  65. data/lib/transferd_services_pb.rb +84 -0
  66. data.tar.gz.sig +0 -0
  67. metadata +13 -166
  68. metadata.gz.sig +0 -0
  69. data/examples/build_exec +0 -74
  70. data/examples/build_exec_rubyc +0 -40
  71. data/lib/aspera/ascp/products.rb +0 -168
  72. data/lib/transfer_pb.rb +0 -84
  73. data/lib/transfer_services_pb.rb +0 -82
@@ -63,13 +63,14 @@ module Aspera
63
63
  return client_key, DataRepository.instance.item(client_key)
64
64
  end
65
65
 
66
- # base API url depends on domain, which could be "qa.xxx"
67
- def api_base_url(organization: 'api', api_domain: SAAS_DOMAIN_PROD)
68
- return "https://#{organization}.#{api_domain}"
66
+ # base API url depends on domain, which could be "qa.xxx" or self-managed domain
67
+ def api_base_url(api_domain: SAAS_DOMAIN_PROD)
68
+ return "https://api.#{api_domain}"
69
69
  end
70
70
 
71
- # split host of http://myorg.asperafiles.com in org and domain
71
+ # split host of URL into organization and domain
72
72
  def split_org_domain(uri)
73
+ Aspera.assert_type(uri, URI)
73
74
  raise "No host found in URL.Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}" if uri.host.nil?
74
75
  parts = uri.host.split('.', 2)
75
76
  Aspera.assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
@@ -77,6 +78,10 @@ module Aspera
77
78
  return %i{organization domain}.zip(parts).to_h
78
79
  end
79
80
 
81
+ def saas_url?(url)
82
+ url.include?(SAAS_DOMAIN_PROD)
83
+ end
84
+
80
85
  # @param url [String] URL of AoC public link
81
86
  # @return [Hash] information about public link, or nil if not a public link
82
87
  def link_info(url)
@@ -187,6 +192,7 @@ module Aspera
187
192
  }
188
193
  # add jwt payload for global client id
189
194
  auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(auth_params[:client_id])
195
+ auth_params[:cache_ids] = [url_info[:organization]]
190
196
  when :url_json
191
197
  auth_params[:url] = {grant_type: 'url_token'} # URL arguments
192
198
  auth_params[:json] = {url_token: url_info[:token]} # JSON body
@@ -442,13 +448,12 @@ module Aspera
442
448
  case pkg_data['metadata']
443
449
  when Array, NilClass # no action
444
450
  when Hash
445
- api_meta = []
446
- pkg_data['metadata'].each do |k, v|
447
- api_meta.push({
451
+ api_meta = pkg_data['metadata'].map do |k, v|
452
+ {
448
453
  # 'input_type' => 'single-dropdown',
449
454
  'name' => k,
450
455
  'values' => v.is_a?(Array) ? v : [v]
451
- })
456
+ }
452
457
  end
453
458
  pkg_data['metadata'] = api_meta
454
459
  else Aspera.error_unexpected_value(pkg_meta.class)
@@ -87,7 +87,7 @@ module Aspera
87
87
  receiver_client_ids: 'aspera_ats'
88
88
  )
89
89
  # get delegated token to be placed in rest call header and in transfer tags
90
- @storage_credentials['token'][TOKEN_FIELD] = OAuth::Factory.bearer_extract(delegated_oauth.token)
90
+ @storage_credentials['token'][TOKEN_FIELD] = delegated_oauth.token
91
91
  @headers['X-Aspera-Storage-Credentials'] = JSON.generate(@storage_credentials)
92
92
  end
93
93
  end
@@ -16,8 +16,6 @@ module Aspera
16
16
  class Node < Aspera::Rest
17
17
  SCOPE_SEPARATOR = ':'
18
18
  SCOPE_NODE_PREFIX = 'node.'
19
- # prefix for ruby code for filter (deprecated)
20
- MATCH_EXEC_PREFIX = 'exec:'
21
19
  MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
22
20
  SIGNATURE_DELIMITER = '==SIGNATURE=='
23
21
  BEARER_TOKEN_VALIDITY_DEFAULT = 86400
@@ -25,7 +23,7 @@ module Aspera
25
23
  REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
26
24
  # methods of @app_info[:api]
27
25
  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,
26
+ private_constant :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_TYPES,
29
27
  :SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
30
28
  :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
31
29
 
@@ -47,27 +45,20 @@ module Aspera
47
45
  @use_node_cache = true
48
46
 
49
47
  class << self
48
+ # set to false to read transfer parameters from download_setup
50
49
  attr_accessor :use_standard_ports
50
+ # set to false to bypass cache in redis
51
51
  attr_accessor :use_node_cache
52
52
 
53
- def cache_control_headers
54
- h = {'Accept' => 'application/json'}
55
- h[HEADER_X_CACHE_CONTROL] = 'no-cache' unless use_node_cache
56
- h
57
- end
58
-
59
53
  # For access keys: provide expression to match entry in folder
54
+ # @param match_expression one of supported types
55
+ # @return lambda function
60
56
  def file_matcher(match_expression)
61
57
  case match_expression
62
58
  when Proc then return match_expression
63
59
  when Regexp then return ->(f){f['name'].match?(match_expression)}
64
60
  when String
65
- if match_expression.start_with?(MATCH_EXEC_PREFIX)
66
- code = "->(f){#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
67
- Log.log.warn{"Use of prefix #{MATCH_EXEC_PREFIX} is deprecated (4.15), instead use: @ruby:'#{code}'"}
68
- return Environment.secure_eval(code, __FILE__, __LINE__)
69
- end
70
- return lambda{|f|File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
61
+ return ->(f){File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
71
62
  when NilClass then return ->(_){true}
72
63
  else Aspera.error_unexpected_value(match_expression.class.name, exception_class: Cli::BadArgument)
73
64
  end
@@ -158,7 +149,13 @@ module Aspera
158
149
 
159
150
  # Call node API, possibly adding cache control header, as globally specified
160
151
  def read_with_cache(subpath, query=nil)
161
- return call(operation: 'GET', subpath: subpath, headers: self.class.cache_control_headers, query: query)[:data]
152
+ headers = {'Accept' => 'application/json'}
153
+ headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless self.class.use_node_cache
154
+ return call(
155
+ operation: 'GET',
156
+ subpath: subpath,
157
+ headers: headers,
158
+ query: query)[:data]
162
159
  end
163
160
 
164
161
  # update transfer spec with special additional tags
@@ -198,11 +195,11 @@ module Aspera
198
195
  # Recursively browse in a folder (with non-recursive method)
199
196
  # sub folders are processed if the processing method returns true
200
197
  # links are processed on the respective node
198
+ # @param method_sym [Symbol] processing method, arguments: entry, path, state
201
199
  # @param state [Object] state object sent to processing method
202
200
  # @param top_file_id [String] file id to start at (default = access key root file id)
203
201
  # @param top_file_path [String] path of top folder (default = /)
204
- # @param block [Proc] processing method, arguments: entry, path, state
205
- def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
202
+ def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/', query: nil)
206
203
  Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
207
204
  Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
208
205
  # start at top folder
@@ -215,7 +212,8 @@ module Aspera
215
212
  # get folder content
216
213
  folder_contents =
217
214
  begin
218
- read("files/#{current_item[:id]}/files")
215
+ # TODO: use header
216
+ read_with_cache("files/#{current_item[:id]}/files")
219
217
  rescue StandardError => e
220
218
  Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
221
219
  []
@@ -228,21 +226,21 @@ module Aspera
228
226
  end
229
227
  next
230
228
  end
231
- relative_path = File.join(current_item[:path], entry['name'])
232
- Log.log.debug{"process_folder_tree: checking #{relative_path}"}
229
+ current_path = File.join(current_item[:path], entry['name'])
230
+ Log.log.debug{"process_folder_tree: checking #{current_path}"}
233
231
  # call block, continue only if method returns true
234
- next unless send(method_sym, entry, relative_path, state)
232
+ next unless send(method_sym, entry, current_path, state)
235
233
  # entry type is file, folder or link
236
234
  case entry['type']
237
235
  when 'folder'
238
- folders_to_explore.push({id: entry['id'], path: relative_path})
236
+ folders_to_explore.push({id: entry['id'], path: current_path})
239
237
  when 'link'
240
238
  if entry_has_link_information(entry)
241
239
  node_id_to_node(entry['target_node_id'])&.process_folder_tree(
242
240
  method_sym: method_sym,
243
241
  state: state,
244
242
  top_file_id: entry['target_id'],
245
- top_file_path: relative_path)
243
+ top_file_path: current_path)
246
244
  end
247
245
  end
248
246
  end
@@ -268,15 +266,21 @@ module Aspera
268
266
  return resolve_state[:result]
269
267
  end
270
268
 
271
- def find_files(top_file_id, test_block)
269
+ def find_files(top_file_id, test_lambda)
272
270
  Log.log.debug{"find_files: file id=#{top_file_id}"}
273
- find_state = {found: [], test_block: test_block}
271
+ find_state = {found: [], test_lambda: test_lambda}
274
272
  process_folder_tree(method_sym: :process_find_files, state: find_state, top_file_id: top_file_id)
275
273
  return find_state[:found]
276
274
  end
277
275
 
276
+ def list_files(top_file_id)
277
+ find_state = {found: []}
278
+ process_folder_tree(method_sym: :process_list_files, state: find_state, top_file_id: top_file_id)
279
+ return find_state[:found]
280
+ end
281
+
278
282
  def refreshed_transfer_token
279
- return oauth.token(refresh: true)
283
+ return oauth.authorization(refresh: true)
280
284
  end
281
285
 
282
286
  # @return part of transfer spec with transport parameters only
@@ -296,6 +300,9 @@ module Aspera
296
300
  end
297
301
 
298
302
  # Create transfer spec for gen4
303
+ # @param file_id destination or source folder (id)
304
+ # @param direction one of Transfer::Spec::DIRECTION_SEND, Transfer::Spec::DIRECTION_RECEIVE
305
+ # @param ts_merge additional transfer spec to merge
299
306
  def transfer_spec_gen4(file_id, direction, ts_merge=nil)
300
307
  ak_name = nil
301
308
  ak_token = nil
@@ -303,12 +310,15 @@ module Aspera
303
310
  when :basic
304
311
  ak_name = auth_params[:username]
305
312
  Aspera.assert(auth_params[:password]){'no secret in node object'}
306
- ak_token = Rest.basic_token(auth_params[:username], auth_params[:password])
313
+ ak_token = Rest.basic_authorization(auth_params[:username], auth_params[:password])
307
314
  when :oauth2
308
315
  ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
309
- # TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
316
+ # TODO: token_generation_lambda = lambda{|do_refresh|oauth.authorization(refresh: do_refresh)}
310
317
  # get bearer token, possibly use cache
311
- ak_token = oauth.token
318
+ ak_token = oauth.authorization
319
+ when :none
320
+ ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
321
+ ak_token = params[:headers]['Authorization']
312
322
  else Aspera.error_unexpected_value(auth_params[:type])
313
323
  end
314
324
  transfer_spec = {
@@ -357,8 +367,8 @@ module Aspera
357
367
 
358
368
  private
359
369
 
370
+ # method called in loop for each entry for `resolve_api_fid`
360
371
  def process_api_fid(entry, path, state)
361
- # this block is called recursively for each entry in folder
362
372
  # stop digging here if not in right path
363
373
  return false unless entry['name'].eql?(state[:path].first)
364
374
  # ok it matches, so we remove the match, and continue digging
@@ -401,11 +411,18 @@ module Aspera
401
411
  return true
402
412
  end
403
413
 
414
+ # method called in loop for each entry for `find_files`
404
415
  def process_find_files(entry, path, state)
405
- state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
416
+ state[:found].push(entry.merge({'path' => path})) if state[:test_lambda].call(entry)
406
417
  # test all files deeply
407
418
  return true
408
419
  end
420
+
421
+ # method called in loop for each entry for `list_files`
422
+ def process_list_files(entry, path, state)
423
+ state[:found].push(entry.merge({'path' => path}))
424
+ return false
425
+ end
409
426
  end
410
427
  end
411
428
  end
@@ -3,11 +3,17 @@
3
3
  # cspell:ignore protobuf ckpt
4
4
  require 'aspera/environment'
5
5
  require 'aspera/data_repository'
6
- require 'aspera/ascp/products'
7
6
  require 'aspera/log'
8
7
  require 'aspera/rest'
8
+ require 'aspera/uri_reader'
9
9
  require 'aspera/assert'
10
10
  require 'aspera/web_server_simple'
11
+ require 'aspera/cli/info'
12
+ require 'aspera/cli/version'
13
+ require 'aspera/products/desktop'
14
+ require 'aspera/products/connect'
15
+ require 'aspera/products/transferd'
16
+ require 'aspera/products/other'
11
17
  require 'English'
12
18
  require 'singleton'
13
19
  require 'xmlsimple'
@@ -20,15 +26,12 @@ module Aspera
20
26
  # Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
21
27
  # It is used by object : AgentDirect to find necessary resources
22
28
  # By default it takes the first Aspera product found
23
- # but the user can specify ascp location by calling:
29
+ # The user can specify ascp location by calling:
24
30
  # Installation.instance.use_ascp_from_product(product_name)
25
31
  # or
26
32
  # Installation.instance.ascp_path=""
27
33
  class Installation
28
34
  include Singleton
29
- # protobuf generated files from sdk
30
- EXT_RUBY_PROTOBUF = '_pb.rb'
31
- RB_SDK_SUBFOLDER = 'lib'
32
35
  DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
33
36
  <?xml version='1.0' encoding='UTF-8'?>
34
37
  <CONF version="2">
@@ -44,14 +47,29 @@ module Aspera
44
47
  EXE_FILES = %i[ascp ascp4 async].freeze
45
48
  FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
46
49
  TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
47
- FILE_SCHEME_PREFIX = 'file:///'
48
- SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
49
- private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
50
+ # filename for ascp with optional extension (Windows)
51
+ private_constant :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL
50
52
  # options for SSH client private key
51
53
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
52
54
 
55
+ # Loads YAML from cloud with locations of SDK archives for all platforms
56
+ # @return location structure
57
+ def sdk_locations
58
+ location_url = @transferd_urls
59
+ transferd_locations = UriReader.read(location_url)
60
+ Log.log.debug{"Retrieving SDK locations from #{location_url}"}
61
+ begin
62
+ return YAML.load(transferd_locations)
63
+ rescue Psych::SyntaxError
64
+ raise "Error when parsing yaml data from: #{location_url}"
65
+ end
66
+ end
67
+
53
68
  # set ascp executable path
54
69
  def ascp_path=(v)
70
+ Aspera.assert_type(v, String)
71
+ Aspera.assert(!v.empty?) {'ascp path cannot be empty: check your config file'}
72
+ Aspera.assert(File.exist?(v)) {"No such file: [#{v}]"}
55
73
  @path_to_ascp = v
56
74
  end
57
75
 
@@ -59,31 +77,19 @@ module Aspera
59
77
  path(:ascp)
60
78
  end
61
79
 
62
- # location of SDK files
80
+ # Compatibility
63
81
  def sdk_folder=(v)
64
- Log.log.debug{"sdk_folder=#{v}"}
65
- @sdk_dir = v
66
- sdk_folder
67
- end
68
-
69
- # backward compatibility in sample program
70
- alias_method :folder=, :sdk_folder=
71
-
72
- # @return the path to folder where SDK is installed
73
- def sdk_folder
74
- raise 'SDK path was ot initialized' if @sdk_dir.nil?
75
- FileUtils.mkdir_p(@sdk_dir)
76
- @sdk_dir
82
+ Products::Transferd.sdk_directory = v
77
83
  end
78
84
 
79
85
  # find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
80
- # or select one from Products.installed_products()
86
+ # or select one from installed_products()
81
87
  def use_ascp_from_product(product_name)
82
88
  if product_name.eql?(FIRST_FOUND)
83
- pl = Products.installed_products.first
84
- raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
89
+ pl = installed_products.first
90
+ raise "no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
85
91
  else
86
- pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
92
+ pl = installed_products.find{|i|i[:name].eql?(product_name)}
87
93
  raise "no such product installed: #{product_name}" if pl.nil?
88
94
  end
89
95
  self.ascp_path = pl[:ascp_path]
@@ -96,6 +102,8 @@ module Aspera
96
102
  m[v.to_s] =
97
103
  begin
98
104
  path(v)
105
+ rescue Errno::ENOENT => e
106
+ e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
99
107
  rescue => e
100
108
  e.message
101
109
  end
@@ -103,7 +111,7 @@ module Aspera
103
111
  end
104
112
 
105
113
  def check_or_create_sdk_file(filename, force: false, &block)
106
- return Environment.write_file_restricted(File.join(sdk_folder, filename), force: force, mode: 0o644, &block)
114
+ return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
107
115
  end
108
116
 
109
117
  # get path of one resource file of currently activated product
@@ -118,7 +126,7 @@ module Aspera
118
126
  file = @path_to_ascp.gsub('ascp', k.to_s)
119
127
  when :transferd
120
128
  file_is_optional = true
121
- file = transferd_filepath
129
+ file = Products::Transferd.transferd_path
122
130
  when :ssh_private_dsa, :ssh_private_rsa
123
131
  # assume last 3 letters are type
124
132
  type = k.to_s[-3..-1].to_sym
@@ -128,8 +136,8 @@ module Aspera
128
136
  when :aspera_conf
129
137
  file = check_or_create_sdk_file('aspera.conf') {DEFAULT_ASPERA_CONF}
130
138
  when :fallback_certificate, :fallback_private_key
131
- file_key = File.join(sdk_folder, 'aspera_fallback_cert_private_key.pem')
132
- file_cert = File.join(sdk_folder, 'aspera_fallback_cert.pem')
139
+ file_key = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert_private_key.pem')
140
+ file_cert = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert.pem')
133
141
  if !File.exist?(file_key) || !File.exist?(file_cert)
134
142
  require 'openssl'
135
143
  # create new self signed certificate for http fallback
@@ -143,7 +151,7 @@ module Aspera
143
151
  else Aspera.error_unexpected_value(k)
144
152
  end
145
153
  return nil if file_is_optional && !File.exist?(file)
146
- Aspera.assert(File.exist?(file)){"no such file: #{file}"}
154
+ Aspera.assert(File.exist?(file), exception_class: Errno::ENOENT){"#{k} not found (#{file})"}
147
155
  return file
148
156
  end
149
157
 
@@ -231,18 +239,10 @@ module Aspera
231
239
  def ascp_info
232
240
  ascp_data = file_paths
233
241
  ascp_data.merge!(ascp_pvcl_info)
234
- ascp_data['sdk_locations'] = TRANSFER_SDK_LOCATION_URL
235
242
  ascp_data.merge!(ascp_ssl_info)
236
243
  return ascp_data
237
244
  end
238
245
 
239
- # Loads YAML from cloud with locations of SDK archives for all platforms
240
- # @return location structure
241
- def sdk_locations
242
- yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
243
- YAML.load(yaml_text)
244
- end
245
-
246
246
  # @return the url for download of SDK archive for the given platform and version
247
247
  def sdk_url_for_platform(platform: nil, version: nil)
248
248
  locations = sdk_locations
@@ -255,6 +255,7 @@ module Aspera
255
255
  return info.first['url']
256
256
  end
257
257
 
258
+ # @param &block called with entry information
258
259
  def extract_archive_files(sdk_archive_path)
259
260
  raise 'missing block' unless block_given?
260
261
  case sdk_archive_path
@@ -265,7 +266,7 @@ module Aspera
265
266
  Zip::File.open(sdk_archive_path) do |zip_file|
266
267
  zip_file.each do |entry|
267
268
  next if entry.name.end_with?('/')
268
- yield(entry.name, entry.get_input_stream)
269
+ yield(entry.name, entry.get_input_stream, nil)
269
270
  end
270
271
  end
271
272
  # Other Unixes use tar.gz
@@ -276,7 +277,7 @@ module Aspera
276
277
  Gem::Package::TarReader.new(gzip) do |tar|
277
278
  tar.each do |entry|
278
279
  next if entry.directory?
279
- yield(entry.full_name, entry)
280
+ yield(entry.full_name, entry, entry.symlink? ? entry.header.linkname : nil)
280
281
  end
281
282
  end
282
283
  end
@@ -285,54 +286,52 @@ module Aspera
285
286
  end
286
287
  end
287
288
 
288
- # download aspera SDK or use local file
289
- # extracts ascp binary for current system architecture
290
- # @param url [String] URL to SDK archive, or SpecialValues::DEF
289
+ # Retrieves ascp binary for current system architecture from URL or file
290
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
291
+ # @param folder [String] Destination folder path
292
+ # @param backup [Bool] If destination folder exists, then rename
293
+ # @param with_exe [Bool] If false, only retrieves files, but do not generate or restrict access
294
+ # @param &block [Proc] A lambda that receives a file path from archive and tells detination sub folder(end with /) or file, or nil to not extract
291
295
  # @return ascp version (from execution)
292
- def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
293
- url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
294
- folder = sdk_folder if folder.nil?
296
+ def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
297
+ url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
298
+ folder = Products::Transferd.sdk_directory if folder.nil?
295
299
  subfolder_lambda = block
296
300
  if subfolder_lambda.nil?
301
+ # default files to extract directly to main folder if in selected source folders
297
302
  subfolder_lambda = ->(name) do
298
- if SDK_ARCHIVE_FOLDERS.any?{|i|name.include?(i)}
299
- '/'
300
- elsif name.end_with?(EXT_RUBY_PROTOBUF)
301
- RB_SDK_SUBFOLDER
302
- end
303
+ Products::Transferd::RUNTIME_FOLDERS.any?{|i|name.match?(%r{^[^/]*/#{i}/})} ? '/' : nil
303
304
  end
304
305
  end
305
- if url.start_with?('file:')
306
- # require specific file scheme: the path part is "relative", or absolute if there are 4 slash
307
- raise 'use format: file:///<path>' unless url.start_with?(FILE_SCHEME_PREFIX)
308
- sdk_archive_path = url[FILE_SCHEME_PREFIX.length..-1]
309
- delete_archive = false
310
- else
311
- sdk_archive_path = File.join(Dir.tmpdir, File.basename(url))
312
- Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
313
- delete_archive = true
314
- end
315
306
  # rename old install
316
307
  if backup && !Dir.empty?(folder)
317
308
  Log.log.warn('Previous install exists, renaming folder.')
318
309
  File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
319
310
  # TODO: delete old archives ?
320
311
  end
321
- extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
322
- subfolder = subfolder_lambda.call(entry_name)
323
- next if subfolder.nil?
324
- dest_folder = File.join(folder, subfolder)
312
+ sdk_archive_path = UriReader.read_as_file(url)
313
+ extract_archive_files(sdk_archive_path) do |entry_name, entry_stream, link_target|
314
+ dest_folder = subfolder_lambda.call(entry_name)
315
+ next if dest_folder.nil?
316
+ dest_folder = File.join(folder, dest_folder)
317
+ if dest_folder.end_with?('/')
318
+ dest_file = File.join(dest_folder, File.basename(entry_name))
319
+ else
320
+ dest_file = dest_folder
321
+ dest_folder = File.dirname(dest_file)
322
+ end
325
323
  FileUtils.mkdir_p(dest_folder)
326
- File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
327
- IO.copy_stream(entry_stream, output_stream)
324
+ if link_target.nil?
325
+ File.open(dest_file, 'wb') { |output_stream|IO.copy_stream(entry_stream, output_stream)}
326
+ else
327
+ File.symlink(link_target, dest_file)
328
328
  end
329
329
  end
330
- File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
331
330
  return unless with_exe
332
331
  # ensure license file are generated so that ascp invocation for version works
333
332
  path(:aspera_license)
334
333
  path(:aspera_conf)
335
- sdk_ascp_file = Products.ascp_filename
334
+ sdk_ascp_file = Environment.exe_file('ascp')
336
335
  sdk_ascp_path = File.join(folder, sdk_ascp_file)
337
336
  raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
338
337
  EXE_FILES.each do |exe_sym|
@@ -340,16 +339,18 @@ module Aspera
340
339
  Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
341
340
  end
342
341
  sdk_ascp_version = get_ascp_version(sdk_ascp_path)
343
- sdk_daemon_path = transferd_filepath
344
- Log.log.warn{"No #{sdk_daemon_path} in SDK archive"} unless File.exist?(sdk_daemon_path)
345
- Environment.restrict_file_access(sdk_daemon_path, mode: 0o755) if File.exist?(sdk_daemon_path)
346
- transferd_version = get_exe_version(sdk_daemon_path, 'version')
342
+ transferd_exe_path = Products::Transferd.transferd_path
343
+ Log.log.warn{"No #{transferd_exe_path} in SDK archive"} unless File.exist?(transferd_exe_path)
344
+ Environment.restrict_file_access(transferd_exe_path, mode: 0o755) if File.exist?(transferd_exe_path)
345
+ transferd_version = get_exe_version(transferd_exe_path, 'version')
347
346
  sdk_name = 'IBM Aspera Transfer SDK'
348
347
  sdk_version = transferd_version || sdk_ascp_version
349
- File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
348
+ File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
350
349
  return sdk_name, sdk_version
351
350
  end
352
351
 
352
+ attr_accessor :transferd_urls
353
+
353
354
  private
354
355
 
355
356
  # policy for product selection
@@ -358,10 +359,30 @@ module Aspera
358
359
  def initialize
359
360
  @path_to_ascp = nil
360
361
  @sdk_dir = nil
362
+ @found_products = nil
363
+ @transferd_urls = TRANSFER_SDK_LOCATION_URL
361
364
  end
362
365
 
363
- def transferd_filepath
364
- return File.join(sdk_folder, 'asperatransferd' + Environment.exe_extension) # cspell:disable-line
366
+ public
367
+
368
+ # @return the list of installed products in format of product_locations_on_current_os
369
+ def installed_products
370
+ if @found_products.nil?
371
+ # :expected M app name is taken from the manifest if present, else defaults to this value
372
+ # :app_root M main folder for the application
373
+ # :log_root O location of log files (Linux uses syslog)
374
+ # :run_root O only for Connect Client, location of http port file
375
+ # :sub_bin O subfolder with executables, default : bin
376
+ scan_locations = Products::Transferd.locations.concat(
377
+ Products::Desktop.locations,
378
+ Products::Connect.locations,
379
+ Products::Other::LOCATION_ON_THIS_OS
380
+ )
381
+ # .each {|item| item.deep_do {|h, _k, _v, _m|h.freeze}}.freeze
382
+ # search installed products: with ascp
383
+ @found_products = Products::Other.find(scan_locations)
384
+ end
385
+ return @found_products
365
386
  end
366
387
  end
367
388
  end
@@ -198,18 +198,39 @@ module Aspera
198
198
  BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
199
199
  MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
200
200
  BOOLEAN_TRUE = 'Yes'
201
+
202
+ private_constant :OPERATIONS, :PARAMETERS, :MGT_HEADER, :MGT_FRAME_SEPARATOR, :INTEGER_FIELDS, :BOOLEAN_FIELDS, :BOOLEAN_TRUE
201
203
  # cspell: enable
202
204
 
203
205
  class << self
204
206
  # translates mgt port event into (enhanced) typed event
205
207
  def enhanced_event_format(event)
206
208
  return event.keys.each_with_object({}) do |e, h|
207
- new_name = e.capital_to_snake.gsub(/(usec)$/, '_\1').downcase
208
- value = event[e]
209
- value = value.to_i if INTEGER_FIELDS.include?(e)
210
- value = value.eql?(BOOLEAN_TRUE) if BOOLEAN_FIELDS.include?(e)
211
- h[new_name] = value
212
- end
209
+ new_name =
210
+ case e
211
+ when 'Elapsedusec' then 'elapsed_usec'
212
+ when 'Bytescont' then 'bytes_cont'
213
+ else e.capital_to_snake
214
+ end
215
+ h[new_name] =
216
+ if INTEGER_FIELDS.include?(e) then event[e].to_i
217
+ elsif BOOLEAN_FIELDS.include?(e) then event[e].eql?(BOOLEAN_TRUE)
218
+ else
219
+ event[e]
220
+ end
221
+ end
222
+ end
223
+
224
+ # build command to send on management port
225
+ # @param data [Hash] {'type'=>'START','source'=>_path_,'destination'=>_path_}
226
+ def command_to_stream(data)
227
+ # TODO: translate enhanced to capitalized ?
228
+ data
229
+ .keys
230
+ .map{|k|"#{k.capitalize}: #{data[k]}"}
231
+ .unshift(MGT_HEADER)
232
+ .push('', '')
233
+ .join("\n")
213
234
  end
214
235
  end
215
236