aspera-cli 4.24.0 → 4.24.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 (87) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +19 -1
  4. data/README.md +1264 -941
  5. data/bin/ascli +20 -1
  6. data/bin/asession +23 -27
  7. data/lib/aspera/agent/base.rb +10 -21
  8. data/lib/aspera/agent/connect.rb +2 -3
  9. data/lib/aspera/agent/desktop.rb +2 -2
  10. data/lib/aspera/agent/direct.rb +49 -32
  11. data/lib/aspera/agent/factory.rb +31 -0
  12. data/lib/aspera/api/aoc.rb +79 -49
  13. data/lib/aspera/api/faspex.rb +212 -0
  14. data/lib/aspera/api/node.rb +99 -84
  15. data/lib/aspera/ascp/installation.rb +22 -21
  16. data/lib/aspera/ascp/management.rb +119 -23
  17. data/lib/aspera/assert.rb +14 -8
  18. data/lib/aspera/cli/extended_value.rb +15 -15
  19. data/lib/aspera/cli/formatter.rb +7 -5
  20. data/lib/aspera/cli/hints.rb +8 -0
  21. data/lib/aspera/cli/info.rb +4 -4
  22. data/lib/aspera/cli/main.rb +56 -71
  23. data/lib/aspera/cli/manager.rb +7 -4
  24. data/lib/aspera/cli/plugins/alee.rb +2 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +110 -186
  26. data/lib/aspera/cli/plugins/ats.rb +4 -4
  27. data/lib/aspera/cli/plugins/base.rb +335 -0
  28. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  29. data/lib/aspera/cli/plugins/config.rb +263 -221
  30. data/lib/aspera/cli/plugins/console.rb +15 -15
  31. data/lib/aspera/cli/plugins/cos.rb +2 -2
  32. data/lib/aspera/cli/plugins/factory.rb +78 -0
  33. data/lib/aspera/cli/plugins/faspex.rb +17 -20
  34. data/lib/aspera/cli/plugins/faspex5.rb +79 -193
  35. data/lib/aspera/cli/plugins/faspio.rb +14 -13
  36. data/lib/aspera/cli/plugins/httpgw.rb +13 -12
  37. data/lib/aspera/cli/plugins/node.rb +34 -32
  38. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  39. data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
  40. data/lib/aspera/cli/plugins/preview.rb +4 -4
  41. data/lib/aspera/cli/plugins/server.rb +15 -13
  42. data/lib/aspera/cli/plugins/shares.rb +18 -15
  43. data/lib/aspera/cli/sync_actions.rb +1 -1
  44. data/lib/aspera/cli/transfer_agent.rb +24 -20
  45. data/lib/aspera/cli/transfer_progress.rb +6 -6
  46. data/lib/aspera/cli/version.rb +3 -3
  47. data/lib/aspera/cli/wizard.rb +74 -65
  48. data/lib/aspera/colors.rb +6 -0
  49. data/lib/aspera/command_line_builder.rb +45 -50
  50. data/lib/aspera/command_line_converter.rb +2 -1
  51. data/lib/aspera/coverage.rb +1 -1
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +13 -9
  54. data/lib/aspera/faspex_gw.rb +6 -4
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/keychain/macos_security.rb +1 -1
  57. data/lib/aspera/log.rb +88 -37
  58. data/lib/aspera/nagios.rb +1 -1
  59. data/lib/aspera/oauth/base.rb +17 -10
  60. data/lib/aspera/oauth/factory.rb +8 -8
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/products/connect.rb +4 -3
  63. data/lib/aspera/products/desktop.rb +1 -4
  64. data/lib/aspera/products/other.rb +9 -1
  65. data/lib/aspera/products/transferd.rb +0 -1
  66. data/lib/aspera/rest.rb +126 -83
  67. data/lib/aspera/ssh.rb +3 -3
  68. data/lib/aspera/sync/args.schema.yaml +46 -3
  69. data/lib/aspera/sync/conf.schema.yaml +130 -94
  70. data/lib/aspera/sync/operations.rb +71 -74
  71. data/lib/aspera/temp_file_manager.rb +17 -5
  72. data/lib/aspera/transfer/error.rb +16 -7
  73. data/lib/aspera/transfer/parameters.rb +34 -20
  74. data/lib/aspera/transfer/resumer.rb +74 -0
  75. data/lib/aspera/transfer/spec.rb +4 -3
  76. data/lib/aspera/transfer/spec.schema.yaml +132 -51
  77. data/lib/aspera/transfer/spec_doc.rb +41 -35
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +6 -6
  80. data.tar.gz.sig +0 -0
  81. metadata +9 -7
  82. metadata.gz.sig +2 -2
  83. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  84. data/lib/aspera/cli/plugin.rb +0 -333
  85. data/lib/aspera/cli/plugin_factory.rb +0 -81
  86. data/lib/aspera/resumer.rb +0 -77
  87. data/lib/aspera/transfer/error_info.rb +0 -91
data/lib/aspera/log.rb CHANGED
@@ -15,14 +15,22 @@ $VERBOSE = nil
15
15
 
16
16
  # Extend Ruby logger with trace levels
17
17
  class Logger
18
- # Two additionnal trace levels
18
+ # Two additional trace levels
19
19
  TRACE_MAX = 2
20
+
20
21
  # Add custom level to logger severity, below debug level
21
22
  module Severity
22
23
  1.upto(TRACE_MAX).each{ |level| const_set(:"TRACE#{level}", - level)}
23
24
  end
24
- # Quick access to label
25
+
26
+ # Hash
27
+ # key [Integer] Log level (e.g. 0 for DEBUG)
28
+ # value [Symbol] Uppercase log level label (e.g. :DEBUG)
25
29
  SEVERITY_LABEL = Severity.constants.each_with_object({}){ |name, hash| hash[Severity.const_get(name)] = name}
30
+
31
+ # Override
32
+ # @param severity [Integer] Log severity as int
33
+ # @return [String] Log severity upper case label
26
34
  def format_severity(severity)
27
35
  SEVERITY_LABEL[severity] || 'ANY'
28
36
  end
@@ -51,34 +59,23 @@ module Aspera
51
59
 
52
60
  # Where logs are sent to
53
61
  LOG_TYPES = %i[stderr stdout syslog].freeze
54
- DEFAULT_FORMATTER = ->(s, _d, _p, m){"#{Log.color_level(s)} #{m}\n"}
55
- FORMATTERS = {
56
- standard: Logger::Formatter.new,
57
- default: DEFAULT_FORMATTER
58
- }.freeze
62
+
63
+ # Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown
64
+ LEVELS = Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}.freeze
65
+
59
66
  # Class methods
60
67
  class << self
61
- def color_level(level)
62
- case level
63
- when :TRACE2 then 'TR2'.dim
64
- when :TRACE1 then 'TR1'.blue
65
- when :DEBUG then 'DBG'.cyan
66
- when :INFO then 'INF'.green
67
- when :WARN then 'WRN'.bg_brown.black
68
- when :ERROR then 'ERR'.bg_red.blink
69
- when :FATAL then 'FTL'.magenta
70
- when :UNKNOWN then 'UKN'.blink
71
- else Aspera.error_unexpected_value(level){'log level'}
72
- end
68
+ # Applies the provided list of string decoration (colors) to the value.
69
+ # @param value [String] Value to enhance.
70
+ # @param colors [Array(Symbol)] List of decorations
71
+ def apply_colors(value, colors)
72
+ colors.inject(value){ |s, c| s.send(c)}
73
73
  end
74
74
 
75
- # levels are :debug,:info,:warn,:error,fatal,:unknown
76
- def levels; Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}; end
77
-
78
- # get the logger object of singleton
75
+ # Get the logger object of singleton
79
76
  def log; instance.logger; end
80
77
 
81
- # Dump object (`Hash`) using specified level
78
+ # Dump object (`Hash`) to log using specified level
82
79
  #
83
80
  # @param name [String, Symbol] Name of object dumped
84
81
  # @param object [Hash, nil] Data to dump
@@ -90,14 +87,16 @@ module Aspera
90
87
  instance.logger.send(level, obj_dump(name, object))
91
88
  end
92
89
 
90
+ # @return [String] Dump of object
93
91
  def obj_dump(name, object)
94
- dump_text = case instance.dump_format
95
- when :json
96
- JSON.pretty_generate(object) rescue PP.pp(object, +'')
97
- when :ruby
98
- PP.pp(object, +'')
99
- else error_unexpected_value(instance.dump_format){'dump format'}
100
- end
92
+ dump_text =
93
+ case instance.dump_format
94
+ when :json
95
+ JSON.pretty_generate(object) rescue PP.pp(object, +'')
96
+ when :ruby
97
+ PP.pp(object, +'')
98
+ else error_unexpected_value(instance.dump_format){'dump format'}
99
+ end
101
100
  "#{name.to_s.green} (#{instance.dump_format})=\n#{dump_text}"
102
101
  end
103
102
 
@@ -110,6 +109,31 @@ module Aspera
110
109
  ensure
111
110
  $stderr = real_stderr
112
111
  end
112
+
113
+ # Returns the last 2 containers (module/class) and method caller
114
+ def caller_method
115
+ stack = caller
116
+ i = stack.rindex{ |line| line.include?('Logger')}
117
+ frame = stack[i + 1] if i && stack[i + 1]
118
+ return '???' unless frame
119
+ # Extract the "Class::Module::Method" or "Class#method" part
120
+ full = frame[/'([^']+)'/, 1]
121
+ return '???' unless full
122
+ # Split into class/module and method parts
123
+ parts = full.split(/(::|#)/)
124
+ # Reconstruct keeping only last two class/module names + separator + method
125
+ if parts.include?('#')
126
+ sep_index = parts.index('#')
127
+ classes = parts[0...sep_index].join
128
+ method = parts[sep_index + 1]
129
+ else
130
+ classes = parts[0..-2].join
131
+ method = parts.last
132
+ end
133
+ class_parts = classes.split('::')
134
+ selected_classes = class_parts.last(2).join('::')
135
+ "#{selected_classes}.#{method}"
136
+ end
113
137
  end
114
138
 
115
139
  attr_reader :logger_type, :logger
@@ -121,7 +145,9 @@ module Aspera
121
145
  end
122
146
 
123
147
  # Set log level of underlying logger given symbol level
148
+ # @param new_level [Symbol] One of LEVELS
124
149
  def level=(new_level)
150
+ Aspera.assert_values(new_level, LEVELS)
125
151
  @logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
126
152
  end
127
153
 
@@ -141,11 +167,10 @@ module Aspera
141
167
  end
142
168
 
143
169
  # Get symbol of debug level of underlying logger
170
+ # @return [Symbol] One of LEVELS
144
171
  def level
145
- Logger::Severity.constants.each do |name|
146
- return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
147
- end
148
- Aspera.error_unexpected_value(@logger.level){'log level'}
172
+ Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level))
173
+ Logger::SEVERITY_LABEL[@logger.level].downcase
149
174
  end
150
175
 
151
176
  # Change underlying logger, but keep log level
@@ -160,8 +185,8 @@ module Aspera
160
185
  @logger = Logger.new($stdout, progname: @program_name, formatter: DEFAULT_FORMATTER)
161
186
  when :syslog
162
187
  require 'syslog/logger'
163
- # the syslog class automatically creates methods from the severity names
164
- # we just need to add the mapping (but syslog lowest is DEBUG)
188
+ # The syslog class automatically creates methods from the severity names.
189
+ # We just need to add the mapping (but syslog lowest is DEBUG)
165
190
  1.upto(Logger::TRACE_MAX).each do |level|
166
191
  Syslog::Logger.const_get(:LEVEL_MAP)[Logger.const_get("TRACE#{level}")] = Syslog::LOG_DEBUG
167
192
  end
@@ -188,5 +213,31 @@ module Aspera
188
213
  # This sets @logger and @logger_type (self needed to call method instead of local var)
189
214
  self.logger_type = @logger_type
190
215
  end
216
+
217
+ # Define decoration of levels
218
+ LVL_DECO = {
219
+ TRACE2: %i{dim},
220
+ TRACE1: %i{blue},
221
+ DEBUG: %i{cyan},
222
+ INFO: %i{green},
223
+ WARN: %i{bg_brown black},
224
+ ERROR: %i{bg_red blink},
225
+ FATAL: %i{magenta},
226
+ UNKNOWN: %i{blink}
227
+ }.freeze
228
+
229
+ # Short levels with color
230
+ LVL_COLOR = LVL_DECO.map{ |k, v| [k, apply_colors("#{k[..2]}#{k[-1]}", v)]}.to_h.freeze
231
+
232
+ DEFAULT_FORMATTER = ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{m}\n"}
233
+
234
+ # pre-defined formatters
235
+ FORMATTERS = {
236
+ standard: Logger::Formatter.new,
237
+ default: DEFAULT_FORMATTER,
238
+ caller: ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{Log.caller_method}\n#{m}\n"}
239
+ }.freeze
240
+
241
+ private_constant :LVL_DECO, :LVL_COLOR, :DEFAULT_FORMATTER, :FORMATTERS
191
242
  end
192
243
  end
data/lib/aspera/nagios.rb CHANGED
@@ -57,7 +57,7 @@ module Aspera
57
57
  # compare remote time with local time
58
58
  def check_time_offset(remote_date, component)
59
59
  # check date if specified : 2015-10-13T07:32:01Z
60
- remote_time = Time.strptime(remote_date)
60
+ remote_time = Time.parse(remote_date)
61
61
  diff_time = (remote_time - Time.now).abs
62
62
  diff_rounded = diff_time.round(-2)
63
63
  Log.log.debug{"DATE: #{remote_date} #{remote_time} diff=#{diff_rounded}"}
@@ -13,6 +13,7 @@ module Aspera
13
13
  # OAuth 2.0 Authorization Framework: https://tools.ietf.org/html/rfc6749
14
14
  # Bearer Token Usage: https://tools.ietf.org/html/rfc6750
15
15
  class Base
16
+ Aspera.require_method!(:create_token)
16
17
  # @param ** Parameters for REST
17
18
  # @param client_id [String, nil]
18
19
  # @param client_secret [String, nil]
@@ -31,8 +32,7 @@ module Aspera
31
32
  cache_ids: nil,
32
33
  **rest_params
33
34
  )
34
- Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', type: InternalError)
35
- # this is the OAuth API
35
+ # This is the OAuth API
36
36
  @api = Rest.new(**rest_params)
37
37
  @scope = nil
38
38
  @token_cache_id = nil
@@ -43,6 +43,7 @@ module Aspera
43
43
  @use_query = use_query
44
44
  @base_cache_ids = cache_ids.nil? ? [] : cache_ids.clone
45
45
  Aspera.assert_type(@base_cache_ids, Array)
46
+ # TODO: this shall be done in class, using cache_ids
46
47
  @base_cache_ids.push(@api.auth_params[:username]) if @api.auth_params.key?(:username)
47
48
  @base_cache_ids.compact!
48
49
  @base_cache_ids.freeze
@@ -56,16 +57,20 @@ module Aspera
56
57
  @token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
57
58
  end
58
59
 
59
- attr_reader :scope, :api, :path_token
60
+ attr_reader :scope, :api, :path_token, :client_id
60
61
 
61
62
  # helper method to create token as per RFC
62
63
  def create_token_call(creation_params)
63
64
  Log.log.debug{'Generating a new token'.bg_green}
64
- payload = {content_type: Rest::MIME_WWW}
65
- if @use_query
66
- payload[:query] = creation_params
65
+ payload = if @use_query
66
+ {
67
+ query: creation_params
68
+ }
67
69
  else
68
- payload[:body] = creation_params
70
+ {
71
+ content_type: Rest::MIME_WWW,
72
+ body: creation_params
73
+ }
69
74
  end
70
75
  return @api.call(
71
76
  operation: 'POST',
@@ -75,12 +80,13 @@ module Aspera
75
80
  )
76
81
  end
77
82
 
78
- # @return Hash with optional general parameters
83
+ # @param add_secret [Boolean] Add secret in default call parameters
84
+ # @return [Hash] Optional general parameters
79
85
  def optional_scope_client_id(add_secret: false)
80
86
  call_params = {}
81
87
  call_params[:scope] = @scope unless @scope.nil?
82
88
  call_params[:client_id] = @client_id unless @client_id.nil?
83
- call_params[:client_secret] = @client_secret if add_secret && !@client_id.nil?
89
+ call_params[:client_secret] = @client_secret unless !add_secret || @client_id.nil? || @client_secret.nil?
84
90
  return call_params
85
91
  end
86
92
 
@@ -106,6 +112,7 @@ module Aspera
106
112
  # `direct` agent is equipped with refresh code
107
113
  # an API was already called, but failed, we need to regenerate or refresh
108
114
  if refresh || token_info[:expired]
115
+ Log.log.trace1{"refresh: #{refresh} expired: #{token_info[:expired]}"}
109
116
  refresh_token = nil
110
117
  if token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
111
118
  # save possible refresh token, before deleting the cache
@@ -118,7 +125,7 @@ module Aspera
118
125
  if !refresh_token.nil?
119
126
  Log.log.info{"refresh=[#{refresh_token}]".bg_green}
120
127
  # NOTE: AoC admin token has no refresh, and lives by default 1800secs
121
- resp = create_token_call(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
128
+ resp = create_token_call(optional_scope_client_id(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
122
129
  if resp[:http].code.start_with?('2')
123
130
  # save only if success
124
131
  json_data = resp[:http].body
@@ -63,10 +63,10 @@ module Aspera
63
63
  @decoders = []
64
64
  # default parameters, others can be added by handlers
65
65
  @parameters = {
66
- # tokens older than 30 minutes will be discarded from cache
67
- token_cache_expiry_sec: 1800,
68
- # tokens valid for less than this duration will be regenerated
69
- token_expiration_guard_sec: 120
66
+ # tokens older than this duration in sec. will be discarded from cache
67
+ token_cache_max_age: 1800,
68
+ # tokens valid for less than this duration in sec. will be regenerated
69
+ token_refresh_threshold: 120
70
70
  }
71
71
  end
72
72
 
@@ -77,7 +77,7 @@ module Aspera
77
77
  def persist_mgr=(manager)
78
78
  @persist = manager
79
79
  # cleanup expired tokens
80
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_expiry_sec])
80
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
81
81
  end
82
82
 
83
83
  def persist_mgr
@@ -108,7 +108,7 @@ module Aspera
108
108
  end
109
109
  end
110
110
 
111
- # get token information from cache
111
+ # Get token information from cache
112
112
  # @param id [String] identifier of token
113
113
  # @return [Hash] token internal information , including Date object for `expiration_date`
114
114
  def get_token_info(id)
@@ -118,7 +118,6 @@ module Aspera
118
118
  Aspera.assert_type(token_data, Hash)
119
119
  decoded_token = decode_token(token_data[TOKEN_FIELD])
120
120
  info = {data: token_data}
121
- Log.dump(:decoded_token, decoded_token)
122
121
  if decoded_token.is_a?(Hash)
123
122
  info[:decoded] = decoded_token
124
123
  # TODO: move date decoding to token decoder ?
@@ -129,9 +128,10 @@ module Aspera
129
128
  unless expiration_date.nil?
130
129
  info[:expiration] = expiration_date
131
130
  info[:ttl_sec] = expiration_date - Time.now
132
- info[:expired] = info[:ttl_sec] < @parameters[:token_expiration_guard_sec]
131
+ info[:expired] = info[:ttl_sec] < @parameters[:token_refresh_threshold]
133
132
  end
134
133
  end
134
+ Log.dump(:token_info, info)
135
135
  return info
136
136
  end
137
137
 
@@ -9,7 +9,7 @@ module Aspera
9
9
  # Authentication using Web browser
10
10
  class Web < Base
11
11
  class << self
12
- attr_accessor :additionnal_info
12
+ attr_accessor :additional_info
13
13
  end
14
14
  # @param redirect_uri url to receive the code after auth (to be exchanged for token)
15
15
  # @param path_authorize path to login page on web app
@@ -37,7 +37,7 @@ module Aspera
37
37
  # here, we need a human to authorize on a web page
38
38
  Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
39
39
  # start a web server to receive request code
40
- web_server = WebAuth.new(@redirect_uri, self.class.additionnal_info)
40
+ web_server = WebAuth.new(@redirect_uri, self.class.additional_info)
41
41
  # start browser on login page
42
42
  Environment.instance.open_uri(login_page_url)
43
43
  # wait for code in request
@@ -48,11 +48,12 @@ module Aspera
48
48
  end
49
49
  end
50
50
 
51
+ # Base URL for CDN of Connect
51
52
  def cdn_api
52
53
  Rest.new(base_url: CDN_BASE_URL)
53
54
  end
54
55
 
55
- # retrieve structure from cloud (CDN) with all versions available
56
+ # Retrieve structure from cloud (CDN) with all versions available
56
57
  def versions
57
58
  if @connect_versions.nil?
58
59
  javascript = cdn_api.call(operation: 'GET', subpath: VERSION_INFO_FILE)
@@ -74,10 +75,10 @@ module Aspera
74
75
  @connect_versions = nil
75
76
  end
76
77
 
77
- VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
78
78
  CDN_BASE_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
79
+ VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
79
80
 
80
- private_constant :VERSION_INFO_FILE, :CDN_BASE_URL
81
+ private_constant :CDN_BASE_URL, :VERSION_INFO_FILE
81
82
  end
82
83
  end
83
84
  end
@@ -8,6 +8,7 @@ module Aspera
8
8
  class Desktop
9
9
  APP_NAME = 'IBM Aspera for Desktop'
10
10
  APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
11
+ LOG_FILENAME = 'ibm-aspera-desktop.log'
11
12
  class << self
12
13
  # standard folder locations
13
14
  def locations
@@ -20,10 +21,6 @@ module Aspera
20
21
  else []
21
22
  end.map{ |i| i.merge({expected: APP_NAME})}
22
23
  end
23
-
24
- def log_file
25
- File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER, 'ibm-aspera-desktop.log')
26
- end
27
24
  end
28
25
  end
29
26
  end
@@ -54,8 +54,14 @@ module Aspera
54
54
  }]
55
55
  end
56
56
  class << self
57
+ # Find installed products and provide paths for it.
58
+ # @param scan_locations [Array] Array of Hash with keys: expected, app_root, sub_bin, ascp_path, name, version
59
+ # @return [Array] of products found, with filled missing fields
60
+ # @raise Exception if no installed product found
57
61
  def find(scan_locations)
58
- scan_locations.select do |item|
62
+ product_names = []
63
+ found = scan_locations.select do |item|
64
+ product_names.push(item[:expected]) unless product_names.include?(item[:expected])
59
65
  # skip if not main folder
60
66
  Log.log.trace1{"Checking #{item[:app_root]}"}
61
67
  next false unless Dir.exist?(item[:app_root])
@@ -75,6 +81,8 @@ module Aspera
75
81
  end
76
82
  true # select this version
77
83
  end
84
+ raise "Product: #{product_names.join(', ')} not found, please install." if found.empty?
85
+ found
78
86
  end
79
87
  end
80
88
  end
@@ -30,7 +30,6 @@ module Aspera
30
30
  # @return the path to folder where SDK is installed
31
31
  def sdk_directory
32
32
  Aspera.assert(!@sdk_dir.nil?){'SDK path was not initialized'}
33
- FileUtils.mkdir_p(@sdk_dir)
34
33
  @sdk_dir
35
34
  end
36
35