aspera-cli 4.19.0 → 4.21.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 (91) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -0
  4. data/CONTRIBUTING.md +18 -4
  5. data/README.md +886 -510
  6. data/bin/asession +27 -20
  7. data/examples/build_exec +65 -76
  8. data/examples/build_exec_rubyc +40 -0
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +18 -24
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +34 -15
  13. data/lib/aspera/agent/direct.rb +44 -54
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +11 -21
  16. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +27 -51
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +139 -105
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +15 -10
  22. data/lib/aspera/api/node.rb +70 -32
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +166 -70
  25. data/lib/aspera/ascp/management.rb +30 -8
  26. data/lib/aspera/assert.rb +10 -5
  27. data/lib/aspera/cli/formatter.rb +166 -162
  28. data/lib/aspera/cli/hints.rb +2 -1
  29. data/lib/aspera/cli/info.rb +12 -10
  30. data/lib/aspera/cli/main.rb +28 -13
  31. data/lib/aspera/cli/manager.rb +7 -2
  32. data/lib/aspera/cli/plugin.rb +17 -31
  33. data/lib/aspera/cli/plugins/alee.rb +3 -3
  34. data/lib/aspera/cli/plugins/aoc.rb +246 -208
  35. data/lib/aspera/cli/plugins/ats.rb +16 -14
  36. data/lib/aspera/cli/plugins/config.rb +154 -94
  37. data/lib/aspera/cli/plugins/console.rb +3 -3
  38. data/lib/aspera/cli/plugins/cos.rb +1 -0
  39. data/lib/aspera/cli/plugins/faspex.rb +15 -23
  40. data/lib/aspera/cli/plugins/faspex5.rb +64 -50
  41. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  42. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  43. data/lib/aspera/cli/plugins/node.rb +174 -109
  44. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  45. data/lib/aspera/cli/plugins/preview.rb +8 -9
  46. data/lib/aspera/cli/plugins/server.rb +5 -9
  47. data/lib/aspera/cli/plugins/shares.rb +2 -2
  48. data/lib/aspera/cli/sync_actions.rb +2 -2
  49. data/lib/aspera/cli/transfer_agent.rb +12 -14
  50. data/lib/aspera/cli/transfer_progress.rb +37 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +4 -5
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +75 -25
  55. data/lib/aspera/faspex_gw.rb +2 -2
  56. data/lib/aspera/json_rpc.rb +1 -1
  57. data/lib/aspera/keychain/macos_security.rb +7 -12
  58. data/lib/aspera/log.rb +3 -4
  59. data/lib/aspera/node_simulator.rb +230 -112
  60. data/lib/aspera/oauth/base.rb +64 -83
  61. data/lib/aspera/oauth/factory.rb +52 -6
  62. data/lib/aspera/oauth/generic.rb +4 -8
  63. data/lib/aspera/oauth/jwt.rb +6 -3
  64. data/lib/aspera/oauth/url_json.rb +1 -2
  65. data/lib/aspera/oauth/web.rb +5 -2
  66. data/lib/aspera/persistency_action_once.rb +16 -8
  67. data/lib/aspera/persistency_folder.rb +20 -2
  68. data/lib/aspera/preview/generator.rb +1 -1
  69. data/lib/aspera/preview/utils.rb +11 -17
  70. data/lib/aspera/products/alpha.rb +30 -0
  71. data/lib/aspera/products/connect.rb +48 -0
  72. data/lib/aspera/products/other.rb +82 -0
  73. data/lib/aspera/products/transferd.rb +54 -0
  74. data/lib/aspera/rest.rb +116 -87
  75. data/lib/aspera/secret_hider.rb +2 -2
  76. data/lib/aspera/ssh.rb +31 -24
  77. data/lib/aspera/transfer/faux_file.rb +4 -4
  78. data/lib/aspera/transfer/parameters.rb +16 -17
  79. data/lib/aspera/transfer/spec.rb +12 -12
  80. data/lib/aspera/transfer/spec.yaml +22 -20
  81. data/lib/aspera/transfer/sync.rb +2 -10
  82. data/lib/aspera/transfer/uri.rb +3 -3
  83. data/lib/aspera/uri_reader.rb +1 -1
  84. data/lib/aspera/web_auth.rb +166 -17
  85. data/lib/aspera/web_server_simple.rb +4 -3
  86. data/lib/transferd_pb.rb +86 -0
  87. data/lib/transferd_services_pb.rb +84 -0
  88. data.tar.gz.sig +0 -0
  89. metadata +58 -22
  90. metadata.gz.sig +0 -0
  91. data/lib/aspera/ascp/products.rb +0 -156
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
3
+ require 'aspera/id_generator'
4
4
  require 'aspera/assert'
5
+ require 'singleton'
5
6
  require 'base64'
6
7
  module Aspera
7
8
  module OAuth
@@ -12,6 +13,7 @@ module Aspera
12
13
  PERSIST_CATEGORY_TOKEN = 'token'
13
14
  # prefix for bearer token when in header
14
15
  BEARER_PREFIX = 'Bearer '
16
+ TOKEN_FIELD = 'access_token'
15
17
 
16
18
  private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
17
19
 
@@ -29,11 +31,17 @@ module Aspera
29
31
  return token[BEARER_PREFIX.length..-1]
30
32
  end
31
33
 
32
- def id(*params)
33
- return [PERSIST_CATEGORY_TOKEN, *params].flatten
34
+ # @return a cache identifier
35
+ def cache_id(url, creator_class, *params)
36
+ return IdGenerator.from_list([
37
+ PERSIST_CATEGORY_TOKEN,
38
+ url,
39
+ Factory.class_to_id(creator_class),
40
+ *params
41
+ ].flatten)
34
42
  end
35
43
 
36
- # snake version of class name is the identifier
44
+ # @return snake version of class name
37
45
  def class_to_id(creator_class)
38
46
  return creator_class.name.split('::').last.capital_to_snake.to_sym
39
47
  end
@@ -80,7 +88,45 @@ module Aspera
80
88
 
81
89
  # delete all existing tokens
82
90
  def flush_tokens
83
- persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
91
+ persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
92
+ end
93
+
94
+ def persisted_tokens
95
+ data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
96
+ data.each.map do |k, v|
97
+ info = {id: k}
98
+ info.merge!(JSON.parse(v)) rescue nil
99
+ d = decode_token(info.delete(TOKEN_FIELD))
100
+ info.merge(d) if d
101
+ info
102
+ end
103
+ end
104
+
105
+ # get token information from cache
106
+ # @param id [String] identifier of token
107
+ # @return [Hash] token internal information , including Date object for `expiration_date`
108
+ def get_token_info(id)
109
+ token_raw_string = persist_mgr.get(id)
110
+ return nil if token_raw_string.nil?
111
+ token_data = JSON.parse(token_raw_string)
112
+ Aspera.assert_type(token_data, Hash)
113
+ decoded_token = decode_token(token_data[TOKEN_FIELD])
114
+ info = { data: token_data }
115
+ Log.log.debug{Log.dump('decoded_token', decoded_token)}
116
+ if decoded_token.is_a?(Hash)
117
+ info[:decoded] = decoded_token
118
+ # TODO: move date decoding to token decoder ?
119
+ expiration_date =
120
+ if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
121
+ elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
122
+ end
123
+ unless expiration_date.nil?
124
+ info[:expiration] = expiration_date
125
+ info[:ttl_sec] = expiration_date - Time.now
126
+ info[:expired] = info[:ttl_sec] < @parameters[:token_expiration_guard_sec]
127
+ end
128
+ end
129
+ return info
84
130
  end
85
131
 
86
132
  # register a bearer token decoder, mainly to inspect expiry date
@@ -118,6 +164,6 @@ module Aspera
118
164
  end
119
165
  end
120
166
  # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
121
- Factory.instance.register_decoder(lambda { |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not aoc token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon, Layout/LineLength
167
+ Factory.instance.register_decoder(lambda { |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not JWS token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon, Layout/LineLength
122
168
  end
123
169
  end
@@ -13,17 +13,13 @@ module Aspera
13
13
  receiver_client_ids: nil,
14
14
  **base_params
15
15
  )
16
- super(**base_params)
16
+ super(**base_params, cache_ids: [grant_type&.split(':')&.last, apikey, response_type])
17
17
  @create_params = {
18
18
  grant_type: grant_type
19
19
  }
20
- @create_params[:response_type] = response_type if response_type
21
- @create_params[:apikey] = apikey if apikey
22
- @create_params[:receiver_client_ids] = receiver_client_ids if receiver_client_ids
23
- @identifiers.push(
24
- @create_params[:grant_type]&.split(':')&.last,
25
- @create_params[:apikey],
26
- @create_params[:response_type])
20
+ @create_params[:response_type] = response_type unless response_type.nil?
21
+ @create_params[:apikey] = apikey unless apikey.nil?
22
+ @create_params[:receiver_client_ids] = receiver_client_ids unless receiver_client_ids.nil?
27
23
  end
28
24
 
29
25
  def create_token
@@ -13,6 +13,7 @@ module Aspera
13
13
  # https://tools.ietf.org/html/rfc7523
14
14
  # https://tools.ietf.org/html/rfc7519
15
15
  class Jwt < Base
16
+ GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
16
17
  # @param private_key_obj private key object
17
18
  # @param payload payload to be included in the JWT
18
19
  # @param headers headers to be included in the JWT
@@ -20,16 +21,18 @@ module Aspera
20
21
  private_key_obj:,
21
22
  payload:,
22
23
  headers: {},
24
+ cache_ids: [],
23
25
  **base_params
24
26
  )
25
27
  Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
26
28
  Aspera.assert_type(payload, Hash){'payload'}
27
29
  Aspera.assert_type(headers, Hash){'headers'}
28
- super(**base_params)
30
+ Aspera.assert_type(cache_ids, Array){'cache ids'}
31
+ new_cache_ids = cache_ids.clone.push(payload[:sub])
32
+ super(**base_params, cache_ids: new_cache_ids)
29
33
  @private_key_obj = private_key_obj
30
34
  @additional_payload = payload
31
35
  @headers = headers
32
- @identifiers.push(@additional_payload[:sub])
33
36
  end
34
37
 
35
38
  def create_token
@@ -46,7 +49,7 @@ module Aspera
46
49
  Log.log.debug{"private=[#{@private_key_obj}]"}
47
50
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
48
51
  Log.log.debug{"assertion=[#{assertion}]"}
49
- return create_token_call(optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
52
+ return create_token_call(optional_scope_client_id.merge(grant_type: GRANT_TYPE, assertion: assertion))
50
53
  end
51
54
  end
52
55
  Factory.instance.register_token_creator(Jwt)
@@ -13,10 +13,9 @@ module Aspera
13
13
  json:,
14
14
  **generic_params
15
15
  )
16
- super(**generic_params)
16
+ super(**generic_params, cache_ids: [json[:url_token]])
17
17
  @body = json
18
18
  @query = url
19
- @identifiers.push(@body[:url_token])
20
19
  end
21
20
 
22
21
  def create_token
@@ -8,6 +8,9 @@ module Aspera
8
8
  module OAuth
9
9
  # Authentication using Web browser
10
10
  class Web < Base
11
+ class << self
12
+ attr_accessor :additionnal_info
13
+ end
11
14
  # @param redirect_uri url to receive the code after auth (to be exchanged for token)
12
15
  # @param path_authorize path to login page on web app
13
16
  def initialize(
@@ -28,12 +31,12 @@ module Aspera
28
31
  # generate secure state to check later
29
32
  random_state = SecureRandom.uuid
30
33
  login_page_url = Rest.build_uri(
31
- "#{@base_url}/#{@path_authorize}",
34
+ "#{@api.base_url}/#{@path_authorize}",
32
35
  optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
33
36
  # here, we need a human to authorize on a web page
34
37
  Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
35
38
  # start a web server to receive request code
36
- web_server = WebAuth.new(@redirect_uri)
39
+ web_server = WebAuth.new(@redirect_uri, self.class.additionnal_info)
37
40
  # start browser on login page
38
41
  Environment.instance.open_uri(login_page_url)
39
42
  # wait for code in request
@@ -7,6 +7,13 @@ require 'aspera/assert'
7
7
  module Aspera
8
8
  # Persist data on file system
9
9
  class PersistencyActionOnce
10
+ DELETE_DEFAULT = lambda(&:empty?)
11
+ PARSE_DEFAULT = lambda {|t| JSON.parse(t)}
12
+ FORMAT_DEFAULT = lambda {|h| JSON.generate(h)}
13
+ MERGE_DEFAULT = lambda {|current, file| current.concat(file).uniq rescue current}
14
+ MANAGER_METHODS = %i[get put delete]
15
+ private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
16
+
10
17
  # @param :manager Mandatory Database
11
18
  # @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
12
19
  # @param :id Mandatory identifiers
@@ -14,21 +21,22 @@ module Aspera
14
21
  # @param :parse Optional parse method (default to JSON)
15
22
  # @param :format Optional dump method (default to JSON)
16
23
  # @param :merge Optional merge data from file to current data
17
- def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
18
- Aspera.assert(!manager.nil?)
24
+ def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
25
+ Aspera.assert(MANAGER_METHODS.all?{|i|manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
19
26
  Aspera.assert(!data.nil?)
20
27
  Aspera.assert_type(id, String)
21
28
  Aspera.assert(!id.empty?)
29
+ Aspera.assert_type(delete, Proc)
30
+ Aspera.assert_type(parse, Proc)
31
+ Aspera.assert_type(format, Proc)
32
+ Aspera.assert_type(merge, Proc)
22
33
  @manager = manager
23
34
  @persisted_object = data
24
35
  @object_id = id
25
- # by default , at save time, file is deleted if data is nil
26
- @delete_condition = delete || lambda{|d|d.empty?}
27
- @persist_format = format || lambda {|h| JSON.generate(h)}
28
- persist_parse = parse || lambda {|t| JSON.parse(t)}
29
- persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
36
+ @delete_condition = delete
37
+ @persist_format = format
30
38
  value = @manager.get(@object_id)
31
- persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
39
+ merge.call(@persisted_object, parse.call(value)) unless value.nil?
32
40
  end
33
41
 
34
42
  def save
@@ -18,7 +18,8 @@ module Aspera
18
18
  Log.log.debug{"persistency folder: #{@folder}"}
19
19
  end
20
20
 
21
- # @return String or nil string on existing persist, else nil
21
+ # Get value of persisted item
22
+ # @return [String,nil] Value of persisted id
22
23
  def get(object_id)
23
24
  Log.log.debug{"persistency get: #{object_id}"}
24
25
  if @cache.key?(object_id)
@@ -34,6 +35,10 @@ module Aspera
34
35
  return @cache[object_id]
35
36
  end
36
37
 
38
+ # Set value of persisted item
39
+ # @param object_id [String] Identifier of persisted item
40
+ # @param value [String] Value of persisted item
41
+ # @return [nil]
37
42
  def put(object_id, value)
38
43
  Aspera.assert_type(value, String)
39
44
  persist_filepath = id_to_filepath(object_id)
@@ -42,8 +47,11 @@ module Aspera
42
47
  File.write(persist_filepath, value)
43
48
  Environment.restrict_file_access(persist_filepath)
44
49
  @cache[object_id] = value
50
+ nil
45
51
  end
46
52
 
53
+ # Delete persisted item
54
+ # @param object_id [String] Identifier of persisted item
47
55
  def delete(object_id)
48
56
  persist_filepath = id_to_filepath(object_id)
49
57
  Log.log.debug{"persistency deleting: #{persist_filepath}"}
@@ -51,8 +59,9 @@ module Aspera
51
59
  @cache.delete(object_id)
52
60
  end
53
61
 
62
+ # Delete persisted items
54
63
  def garbage_collect(persist_category, max_age_seconds=nil)
55
- garbage_files = Dir[File.join(@folder, persist_category + '*' + FILE_SUFFIX)]
64
+ garbage_files = current_files(persist_category)
56
65
  if !max_age_seconds.nil?
57
66
  current_time = Time.now
58
67
  garbage_files.select! { |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
@@ -61,9 +70,18 @@ module Aspera
61
70
  File.delete(filepath)
62
71
  Log.log.debug{"persistency deleted expired: #{filepath}"}
63
72
  end
73
+ @cache.clear
64
74
  return garbage_files
65
75
  end
66
76
 
77
+ def current_files(persist_category)
78
+ Dir[File.join(@folder, persist_category + '*' + FILE_SUFFIX)]
79
+ end
80
+
81
+ def current_items(persist_category)
82
+ current_files(persist_category).each_with_object({}) {|i, h| h[File.basename(i, FILE_SUFFIX)] = File.read(i)}
83
+ end
84
+
67
85
  private
68
86
 
69
87
  # @param object_id String or Array
@@ -66,7 +66,7 @@ module Aspera
66
66
  result_size = File.size(@destination_file_path)
67
67
  Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"} if result_size > @options.max_size
68
68
  rescue StandardError => e
69
- Log.log.error{"Ignoring: #{e.message}"}
69
+ Log.log.error{"Ignoring: #{e.class} #{e.message}"}
70
70
  Log.log.debug(e.backtrace.join("\n").red)
71
71
  FileUtils.cp(File.expand_path(@preview_format_sym.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__)), @destination_file_path)
72
72
  ensure
@@ -32,7 +32,7 @@ module Aspera
32
32
  tools_to_check.delete(:unoconv) if skip_types.include?(:office)
33
33
  # Check for binaries
34
34
  tools_to_check.each do |command_sym|
35
- external_command(command_sym, ['-h'], check_code: false)
35
+ external_command(command_sym, ['-h'])
36
36
  rescue Errno::ENOENT => e
37
37
  raise "missing #{command_sym} binary: #{e}"
38
38
  rescue
@@ -42,20 +42,15 @@ module Aspera
42
42
 
43
43
  # execute external command
44
44
  # one could use "system", but we would need to redirect stdout/err
45
- # @return true if su
46
- def external_command(command_sym, command_args, check_code: true)
45
+ # @return nil
46
+ def external_command(command_sym, command_args)
47
47
  Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
48
- # build command line, and quote special characters
49
- command_line = command_args.clone.unshift(command_sym).map{|i| shell_quote(i.to_s)}.join(' ')
50
- Log.log.debug{"cmd=#{command_line}".blue}
51
- stdout, stderr, status = Open3.capture3(command_line)
52
- if check_code && !status.success?
53
- Log.log.error{"status: #{status}"}
54
- Log.log.error{"stdout: #{stdout}"}
55
- Log.log.error{"stderr: #{stderr}"}
56
- raise "#{command_sym} error #{status}"
57
- end
58
- return {status: status, stdout: stdout}
48
+ Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s))
49
+ end
50
+
51
+ def external_capture(command_sym, command_args)
52
+ Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
53
+ return Environment.secure_capture(exec: command_sym.to_s, args: command_args.map(&:to_s))
59
54
  end
60
55
 
61
56
  def ffmpeg(a)
@@ -73,12 +68,11 @@ module Aspera
73
68
 
74
69
  # @return Float in seconds
75
70
  def video_get_duration(input_file)
76
- result = external_command(:ffprobe, [
71
+ return external_capture(:ffprobe, [
77
72
  '-loglevel', 'error',
78
73
  '-show_entries', 'format=duration',
79
74
  '-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
80
- input_file])
81
- return result[:stdout].to_f
75
+ input_file]).to_f
82
76
  end
83
77
 
84
78
  def ffmpeg_fmt(temp_folder)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+
5
+ module Aspera
6
+ module Products
7
+ # Aspera Desktop Alpha Client
8
+ class Alpha
9
+ APP_NAME = 'IBM Aspera for Desktop'
10
+ APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
11
+ class << self
12
+ # standard folder locations
13
+ def locations
14
+ case Aspera::Environment.os
15
+ when Aspera::Environment::OS_MACOS then [{
16
+ app_root: File.join('', 'Applications', 'IBM Aspera.app'),
17
+ log_root: File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER),
18
+ sub_bin: File.join('Contents', 'Resources', 'transferd', 'bin')
19
+ }]
20
+ else []
21
+ end.map { |i| i.merge({ expected: APP_NAME }) }
22
+ end
23
+
24
+ def log_file
25
+ File.join(Dir.home, 'Library', 'Logs', APP_IDENTIFIER, 'ibm-aspera-desktop.log')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+
5
+ module Aspera
6
+ module Products
7
+ class Connect
8
+ APP_NAME = 'IBM Aspera Connect'
9
+ class << self
10
+ # standard folder locations
11
+ def locations
12
+ case Aspera::Environment.os
13
+ when Aspera::Environment::OS_WINDOWS then [{
14
+ app_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Programs', 'Aspera', 'Aspera Connect'),
15
+ log_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Aspera', 'Aspera Connect', 'var', 'log'),
16
+ run_root: File.join(ENV.fetch('LOCALAPPDATA', nil), 'Aspera', 'Aspera Connect')
17
+ }]
18
+ when Aspera::Environment::OS_MACOS then [{
19
+ app_root: File.join(Dir.home, 'Applications', 'Aspera Connect.app'),
20
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
21
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
22
+ sub_bin: File.join('Contents', 'Resources')
23
+ }, {
24
+ app_root: File.join('', 'Applications', 'Aspera Connect.app'),
25
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
26
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
27
+ sub_bin: File.join('Contents', 'Resources')
28
+ }, {
29
+ app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
30
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
31
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
32
+ sub_bin: File.join('Contents', 'Resources')
33
+ }, {
34
+ app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
35
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
36
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
37
+ sub_bin: File.join('Contents', 'Resources')
38
+ }]
39
+ else [{ # other: Linux and Unix family
40
+ app_root: File.join(Dir.home, '.aspera', 'connect'),
41
+ run_root: File.join(Dir.home, '.aspera', 'connect')
42
+ }]
43
+ end.map { |i| i.merge({ expected: APP_NAME }) }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # cspell:ignore LOCALAPPDATA
4
+ require 'aspera/environment'
5
+
6
+ module Aspera
7
+ # Location of Aspera products, for which an Agent is not proposed
8
+ module Products
9
+ # other Aspera products with ascp
10
+ class Other
11
+ CLI_V3 = 'Aspera CLI (deprecated)'
12
+ DRIVE = 'Aspera Drive (deprecated)'
13
+ HSTS = 'IBM Aspera High-Speed Transfer Server'
14
+
15
+ private_constant :CLI_V3, :DRIVE, :HSTS
16
+ # product information manifest: XML (part of aspera product)
17
+ INFO_META_FILE = 'product-info.mf'
18
+
19
+ # :expected M app name is taken from the manifest if present, else defaults to this value
20
+ # :app_root M main folder for the application
21
+ # :log_root O location of log files (Linux uses syslog)
22
+ # :run_root O only for Connect Client, location of http port file
23
+ # :sub_bin O subfolder with executables, default : bin
24
+ LOCATION_ON_THIS_OS = case Aspera::Environment.os
25
+ when Aspera::Environment::OS_WINDOWS then [{
26
+ expected: CLI_V3,
27
+ app_root: File.join('C:', 'Program Files', 'Aspera', 'cli'),
28
+ log_root: File.join('C:', 'Program Files', 'Aspera', 'cli', 'var', 'log')
29
+ }, {
30
+ expected: HSTS,
31
+ app_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server'),
32
+ log_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server', 'var', 'log')
33
+ }]
34
+ when Aspera::Environment::OS_MACOS then [{
35
+ expected: CLI_V3,
36
+ app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
37
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera')
38
+ }, {
39
+ expected: HSTS,
40
+ app_root: File.join('', 'Library', 'Aspera'),
41
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera')
42
+ }, {
43
+ expected: DRIVE,
44
+ app_root: File.join('', 'Applications', 'Aspera Drive.app'),
45
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Drive'),
46
+ sub_bin: File.join('Contents', 'Resources')
47
+ }]
48
+ else [{ # other: Linux and Unix family
49
+ expected: CLI_V3,
50
+ app_root: File.join(Dir.home, '.aspera', 'cli')
51
+ }, {
52
+ expected: HSTS,
53
+ app_root: File.join('', 'opt', 'aspera')
54
+ }]
55
+ end
56
+ class << self
57
+ def find(scan_locations)
58
+ scan_locations.select do |item|
59
+ # skip if not main folder
60
+ Log.log.trace1{"Checking #{item[:app_root]}"}
61
+ next false unless Dir.exist?(item[:app_root])
62
+ Log.log.debug{"Found #{item[:expected]}"}
63
+ sub_bin = item[:sub_bin] || 'bin'
64
+ item[:ascp_path] = File.join(item[:app_root], sub_bin, Environment.exe_file('ascp'))
65
+ # skip if no ascp
66
+ next false unless File.exist?(item[:ascp_path])
67
+ # read info from product info file if present
68
+ product_info_file = "#{item[:app_root]}/#{INFO_META_FILE}"
69
+ if File.exist?(product_info_file)
70
+ res_s = XmlSimple.xml_in(File.read(product_info_file), {'ForceArray' => false})
71
+ item[:name] = res_s['name']
72
+ item[:version] = res_s['version']
73
+ else
74
+ item[:name] = item[:expected]
75
+ end
76
+ true # select this version
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Products
5
+ class Transferd
6
+ APP_NAME = 'IBM Aspera Transfer Daemon'
7
+ class << self
8
+ # standard folder locations
9
+ def locations
10
+ [{
11
+ app_root: sdk_directory,
12
+ sub_bin: ''
13
+ }].map { |i| i.merge({ expected: APP_NAME }) }
14
+ end
15
+
16
+ # location of SDK files
17
+ def sdk_directory=(v)
18
+ Log.log.debug{"sdk_directory=#{v}"}
19
+ @sdk_dir = v
20
+ sdk_directory
21
+ end
22
+
23
+ # @return the path to folder where SDK is installed
24
+ def sdk_directory
25
+ Aspera.assert(!@sdk_dir.nil?){'SDK path was not initialized'}
26
+ FileUtils.mkdir_p(@sdk_dir)
27
+ @sdk_dir
28
+ end
29
+
30
+ def transferd_path
31
+ return File.join(sdk_directory, Environment.exe_file('asperatransferd')) # cspell:disable-line
32
+ end
33
+
34
+ # Well, the port number is only in log file
35
+ def daemon_port_from_log(log_file)
36
+ result = nil
37
+ # if port is zero, a dynamic port was created, get it
38
+ File.open(log_file, 'r') do |file|
39
+ file.each_line do |line|
40
+ # Well, it's tricky to depend on log
41
+ if (m = line.match(/Info: API Server: Listening on ([^:]+):(\d+) /))
42
+ result = m[2].to_i
43
+ # no "break" , need to read last matching log line
44
+ end
45
+ end
46
+ end
47
+ raise 'Port not found in daemon logs' if result.nil?
48
+ Log.log.debug{"Got port #{result} from log"}
49
+ return result
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end