aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/log'
4
+ require 'aspera/assert'
3
5
  module Aspera
4
6
  # helper class to build command line from a parameter list (key-value hash)
5
7
  # constructor takes hash: { 'param1':'value1', ...}
@@ -27,20 +29,20 @@ module Aspera
27
29
  # Called by provider of definition before constructor of this class so that params_definition has all mandatory fields
28
30
  def normalize_description(full_description)
29
31
  full_description.each do |name, options|
30
- raise "Expecting Hash, but have #{options.class} in #{name}" unless options.is_a?(Hash)
32
+ assert_type(options, Hash){name}
31
33
  unsupported_keys = options.keys - OPTIONS_KEYS
32
- raise "Unsupported definition keys: #{unsupported_keys}" unless unsupported_keys.empty?
33
- raise "Missing key: cli for #{name}" unless options.key?(:cli)
34
- raise 'Key: cli must be Hash' unless options[:cli].is_a?(Hash)
35
- raise 'Missing key: cli.type' unless options[:cli].key?(:type)
36
- raise "Unsupported processing type for #{name}: #{options[:cli][:type]}" unless CLI_OPTION_TYPES.include?(options[:cli][:type])
34
+ assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
35
+ assert(options.key?(:cli)){"Missing key: cli for #{name}"}
36
+ assert_type(options[:cli], Hash){'Key: cli'}
37
+ assert(options[:cli].key?(:type)){'Missing key: cli.type'}
38
+ assert_values(options[:cli][:type], CLI_OPTION_TYPES){"Unsupported processing type for #{name}"}
37
39
  # by default : optional
38
40
  options[:mandatory] ||= false
39
41
  options[:desc] ||= ''
40
42
  options[:desc] = "DEPRECATED: #{options[:deprecation]}\n#{options[:desc]}" if options.key?(:deprecation)
41
43
  cli = options[:cli]
42
44
  unsupported_cli_keys = cli.keys - CLI_KEYS
43
- raise "Unsupported cli keys: #{unsupported_cli_keys}" unless unsupported_cli_keys.empty?
45
+ assert(unsupported_cli_keys.empty?){"Unsupported cli keys: #{unsupported_cli_keys}"}
44
46
  # by default : string, unless it's without arg
45
47
  options[:accepted_types] ||= options[:cli][:type].eql?(:opt_without_arg) ? :bool : :string
46
48
  # single type is placed in array
@@ -55,31 +57,34 @@ module Aspera
55
57
 
56
58
  attr_reader :params_definition
57
59
 
58
- # @param param_hash
60
+ # @param [Hash] param_hash with parameters
61
+ # @param [Hash] params_definition with definition of parameters
59
62
  def initialize(param_hash, params_definition)
60
63
  @param_hash = param_hash # keep reference so that it can be modified by caller before calling `process_params`
61
64
  @params_definition = params_definition
62
- @result_env = {}
63
- @result_args = []
65
+ @result = {
66
+ env: {},
67
+ args: []
68
+ }
64
69
  @used_param_names = []
65
70
  end
66
71
 
67
- # adds keys :env :args with resulting values after processing
68
- # warns if some parameters were not used
69
- def add_env_args(env, args)
70
- Log.log.debug{"ENV=#{@result_env}, ARGS=#{@result_args}"}
72
+ # add processed parameters to env and args, warns about unused parameters
73
+ # @param [Hash] env_args with :env and :args
74
+ def add_env_args(env_args)
75
+ Log.log.debug{"add_env_args: ENV=#{@result[:env]}, ARGS=#{@result[:args]}"}
71
76
  # warn about non translated arguments
72
77
  @param_hash.each_pair{|key, val|Log.log.warn{"unrecognized parameter: #{key} = \"#{val}\""} if !@used_param_names.include?(key)}
73
78
  # set result
74
- env.merge!(@result_env)
75
- args.push(*@result_args)
79
+ env_args[:env].merge!(@result[:env])
80
+ env_args[:args].push(*@result[:args])
76
81
  return nil
77
82
  end
78
83
 
79
84
  # add options directly to command line
80
85
  def add_command_line_options(options)
81
86
  return if options.nil?
82
- options.each{|o|@result_args.push(o.to_s)}
87
+ options.each{|o|@result[:args].push(o.to_s)}
83
88
  end
84
89
 
85
90
  def process_params
@@ -118,7 +123,7 @@ module Aspera
118
123
  when :hash then Hash
119
124
  when :int then Integer
120
125
  when :bool then [TrueClass, FalseClass]
121
- else raise "INTERNAL: unexpected value: #{type_symbol}"
126
+ else error_unexpected_value(type_symbol)
122
127
  end
123
128
  end.flatten
124
129
  # check that value is of expected type
@@ -147,7 +152,7 @@ module Aspera
147
152
  raise Fasp::Error, "unsupported #{name}: #{parameter_value}" if converted_value.nil?
148
153
  parameter_value = converted_value
149
154
  when NilClass
150
- else raise "not expected type for convert #{options[:cli][:convert].class} for #{name}"
155
+ else error_unexpected_value(options[:cli][:convert].class)
151
156
  end
152
157
 
153
158
  case processing_type
@@ -156,14 +161,14 @@ module Aspera
156
161
  when :ignore, :special # ignore this parameter or process later
157
162
  return
158
163
  when :envvar # set in env var
159
- raise 'error' unless options[:cli].key?(:variable)
160
- @result_env[options[:cli][:variable]] = parameter_value
164
+ assert(options[:cli].key?(:variable)){'missing key: cli.variable'}
165
+ @result[:env][options[:cli][:variable]] = parameter_value
161
166
  when :opt_without_arg # if present and true : just add option without value
162
167
  add_param = false
163
168
  case parameter_value
164
169
  when false then nil # nothing to put on command line, no creation by default
165
170
  when true then add_param = true
166
- else raise Fasp::Error, "unsupported #{name}: #{parameter_value}"
171
+ else error_unexpected_value(parameter_value){name}
167
172
  end
168
173
  add_param = !add_param if options[:add_on_false]
169
174
  add_command_line_options([options[:cli][:switch]]) if add_param
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/log'
4
+ require 'aspera/assert'
4
5
  require 'aspera/rest'
6
+ require 'aspera/oauth'
5
7
  require 'xmlsimple'
6
8
 
7
9
  module Aspera
8
10
  class CosNode < Aspera::Node
9
11
  class << self
10
- def parameters_from_svc_creds(service_credentials, bucket_region)
12
+ def parameters_from_svc_credentials(service_credentials, bucket_region)
11
13
  # check necessary contents
12
- raise 'service_credentials must be a Hash' unless service_credentials.is_a?(Hash)
14
+ assert_type(service_credentials, Hash){'service_credentials'}
13
15
  %w[apikey resource_instance_id endpoints].each do |field|
14
- raise "service_credentials must have a field: #{field}" unless service_credentials.key?(field)
16
+ assert(service_credentials.key?(field)){"service_credentials must have a field: #{field}"}
15
17
  end
16
18
  Aspera::Log.dump('service_credentials', service_credentials)
17
19
  # read endpoints from service provided in service credentials
@@ -85,7 +87,7 @@ module Aspera
85
87
  receiver_client_ids: 'aspera_ats'
86
88
  }})
87
89
  # get delegated token to be placed in rest call header and in transfer tags
88
- @storage_credentials['token'][TOKEN_FIELD] = delegated_oauth.get_authorization.gsub(/^Bearer /, '')
90
+ @storage_credentials['token'][TOKEN_FIELD] = Oauth.bearer_extract(delegated_oauth.get_authorization)
89
91
  @params[:headers] = {'X-Aspera-Storage-Credentials' => JSON.generate(@storage_credentials)}
90
92
  end
91
93
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # coverage for tests
4
+ if ENV.key?('ENABLE_COVERAGE')
5
+ require 'simplecov'
6
+ require 'securerandom'
7
+ # compute gem source root based on this script location, assuming it is in bin/
8
+ # use dirname instead of gsub, in case folder separator is not /
9
+ development_root = 3.times.inject(File.realpath(__FILE__)) { |p, _| File.dirname(p) }
10
+ SimpleCov.root(development_root)
11
+ SimpleCov.enable_for_subprocesses if SimpleCov.respond_to?(:enable_for_subprocesses)
12
+ # keep cache data for 1 day (must be longer that time to run the whole test suite)
13
+ SimpleCov.merge_timeout(86400)
14
+ SimpleCov.command_name(SecureRandom.uuid)
15
+ SimpleCov.at_exit do
16
+ original_file_descriptor = $stdout
17
+ $stdout.reopen(File.join(development_root, 'simplecov.log'))
18
+ SimpleCov.result.format!
19
+ $stdout.reopen(original_file_descriptor)
20
+ end
21
+ SimpleCov.start
22
+ end
@@ -1,15 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/log'
3
+ require 'aspera/assert'
4
4
  require 'singleton'
5
+ require 'openssl'
5
6
 
6
7
  module Aspera
7
8
  # a simple binary data repository
8
9
  class DataRepository
9
10
  include Singleton
11
+ # in same order as elements in folder
12
+ ELEMENTS = %i[dsa rsa uuid aspera.global-cli-client aspera.drive license]
13
+ START_INDEX = 1
14
+ DATA_FOLDER_NAME = 'data'
15
+
16
+ # decode data as expected as string
17
+ # @param name [Symbol] name of the data item
18
+ # @return [String] decoded data
19
+ def item(name)
20
+ index = ELEMENTS.index(name)
21
+ raise ArgumentError, "unknown data item #{name} (#{name.class})" unless index
22
+ raw_data = data(START_INDEX + index)
23
+ case name
24
+ when :dsa, :rsa
25
+ # generate PEM from DER
26
+ return OpenSSL::PKey.const_get(name.to_s.upcase).new(raw_data).to_pem
27
+ when :license
28
+ return Zlib::Inflate.inflate(raw_data)
29
+ when :uuid
30
+ return format('%08x-%04x-%04x-%04x-%04x%08x', *raw_data.unpack('NnnnnN'))
31
+ when :'aspera.global-cli-client', :'aspera.drive'
32
+ return Base64.urlsafe_encode64(raw_data)
33
+ else error_unexpected_value(name)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ private_constant :START_INDEX, :DATA_FOLDER_NAME
40
+
10
41
  # get binary value from data repository
11
42
  def data(id)
12
- File.read(File.join(__dir__, 'data', id.to_s), mode: 'rb')
43
+ File.read(File.join(__dir__, DATA_FOLDER_NAME, id.to_s), mode: 'rb')
13
44
  end
14
45
  end
15
46
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore USERPROFILE HOMEDRIVE HOMEPATH LC_CTYPE msys aarch
3
4
  require 'aspera/log'
5
+ require 'aspera/assert'
4
6
  require 'rbconfig'
5
7
 
6
8
  # cspell:words MEBI mswin bccwin
@@ -14,10 +16,11 @@ module Aspera
14
16
  OS_AIX = :aix
15
17
  OS_LIST = [OS_WINDOWS, OS_X, OS_LINUX, OS_AIX].freeze
16
18
  CPU_X86_64 = :x86_64
19
+ CPU_ARM64 = :arm64
17
20
  CPU_PPC64 = :ppc64
18
21
  CPU_PPC64LE = :ppc64le
19
22
  CPU_S390 = :s390
20
- CPU_LIST = [CPU_X86_64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
23
+ CPU_LIST = [CPU_X86_64, CPU_ARM64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
21
24
 
22
25
  BITS_PER_BYTE = 8
23
26
  MEBI = 1024 * 1024
@@ -52,7 +55,7 @@ module Aspera
52
55
  return CPU_PPC64
53
56
  when /s390/
54
57
  return CPU_S390
55
- when /arm/
58
+ when /arm/, /aarch64/
56
59
  # arm on mac has rosetta 2
57
60
  return CPU_X86_64 if os.eql?(OS_X)
58
61
  end
@@ -71,9 +74,9 @@ module Aspera
71
74
  # on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
72
75
  # so, tell Ruby the right way
73
76
  def fix_home
74
- return unless os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV['USERPROFILE'])
75
- ENV['HOME'] = ENV['USERPROFILE']
76
- Log.log.debug{"Windows: set home to USERPROFILE: #{ENV['HOME']}"}
77
+ return unless os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
78
+ ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
79
+ Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
77
80
  end
78
81
 
79
82
  def empty_binding
@@ -81,13 +84,13 @@ module Aspera
81
84
  end
82
85
 
83
86
  # secure execution of Ruby code
84
- def secure_eval(code)
85
- Kernel.send('lave'.reverse, code, empty_binding, __FILE__, __LINE__)
87
+ def secure_eval(code, file, line)
88
+ Kernel.send('lave'.reverse, code, empty_binding, file, line)
86
89
  end
87
90
 
88
91
  # value is provided in block
89
92
  def write_file_restricted(path, force: false, mode: nil)
90
- raise 'coding error, missing content block' unless block_given?
93
+ assert(block_given?, exception_class: Aspera::InternalError)
91
94
  if force || !File.exist?(path)
92
95
  # Windows may give error
93
96
  File.unlink(path) rescue nil
@@ -113,6 +116,16 @@ module Aspera
113
116
  rescue => e
114
117
  Log.log.warn(e.message)
115
118
  end
119
+
120
+ def terminal?
121
+ $stdout.tty?
122
+ end
123
+
124
+ # @return true if we can display Unicode characters
125
+ def use_unicode?
126
+ @use_unicode = terminal? && ENV.values_at('LC_ALL', 'LC_CTYPE', 'LANG').compact.first.include?('UTF-8') if @use_unicode.nil?
127
+ return @use_unicode
128
+ end
116
129
  end # self
117
130
  end # Environment
118
131
  end # Aspera
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/fasp/agent_base'
4
+ require 'aspera/rest'
5
+ require 'aspera/log'
6
+ require 'aspera/json_rpc'
7
+ require 'aspera/open_application'
8
+ require 'securerandom'
9
+
10
+ module Aspera
11
+ module Fasp
12
+ class AgentAlpha < Aspera::Fasp::AgentBase
13
+ # try twice the main init url in sequence
14
+ START_URIS = ['aspera://']
15
+ # delay between each try to start connect
16
+ SLEEP_SEC_BETWEEN_RETRY = 3
17
+ private_constant :START_URIS, :SLEEP_SEC_BETWEEN_RETRY
18
+ def initialize(options)
19
+ @application_id = SecureRandom.uuid
20
+ super(options)
21
+ raise 'Using client requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
22
+ method_index = 0
23
+ begin
24
+ @client_app_api = Aspera::JsonRpcClient.new(Aspera::Rest.new(base_url: aspera_client_api_url))
25
+ client_info = @client_app_api.get_info
26
+ Log.log.debug{Log.dump(:client_version, client_info)}
27
+ # my_transfer_id = '0513fe85-65cf-465b-ad5f-18fd40d8c69f'
28
+ # @client_app_api.get_all_transfers({app_id: @application_id})
29
+ # @client_app_api.get_transfer(app_id: @application_id, transfer_id: my_transfer_id)
30
+ # @client_app_api.start_transfer(app_id: @application_id,transfer_spec: {})
31
+ # @client_app_api.remove_transfer
32
+ # @client_app_api.stop_transfer
33
+ # @client_app_api.modify_transfer
34
+ # @client_app_api.show_directory({app_id: @application_id, transfer_id: my_transfer_id})
35
+ # @client_app_api.get_files_list({app_id: @application_id, transfer_id: my_transfer_id})
36
+ Log.log.info('Client was reached') if method_index > 0
37
+ rescue StandardError => e # Errno::ECONNREFUSED
38
+ start_url = START_URIS[method_index]
39
+ method_index += 1
40
+ raise StandardError, "Unable to start connect #{method_index} times" if start_url.nil?
41
+ Log.log.warn{"Aspera Connect is not started (#{e}). Trying to start it ##{method_index}..."}
42
+ if !OpenApplication.uri_graphical(start_url)
43
+ OpenApplication.uri_graphical('https://downloads.asperasoft.com/connect2/')
44
+ raise StandardError, 'Connect is not installed'
45
+ end
46
+ sleep(SLEEP_SEC_BETWEEN_RETRY)
47
+ retry
48
+ end
49
+ end
50
+
51
+ def aspera_client_api_url
52
+ log_file = File.join(Dir.home, 'Library', 'Logs', 'IBM Aspera', 'ibm-aspera-desktop.log')
53
+ url = nil
54
+ File.open(log_file, 'r') do |file|
55
+ file.each_line do |line|
56
+ line = line.chomp
57
+ if (m = line.match(/JSON-RPC server listening on (.*)/))
58
+ url = "http://#{m[1]}"
59
+ end
60
+ end
61
+ end
62
+ return url
63
+ end
64
+
65
+ def start_transfer(transfer_spec, token_regenerator: nil)
66
+ @request_id = SecureRandom.uuid
67
+ # if there is a token, we ask connect client to use well known ssh private keys
68
+ # instead of asking password
69
+ transfer_spec['authentication'] = 'token' if transfer_spec.key?('token')
70
+ result = @client_app_api.start_transfer(app_id: @application_id, desktop_spec: {}, transfer_spec: transfer_spec)
71
+ @xfer_id = result['uuid']
72
+ end
73
+
74
+ def wait_for_transfers_completion
75
+ started = false
76
+ pre_calc = false
77
+ begin
78
+ loop do
79
+ transfer = @client_app_api.get_transfer(app_id: @application_id, transfer_id: @xfer_id)
80
+ case transfer['status']
81
+ when 'initiating', 'queued'
82
+ notify_progress(session_id: nil, type: :pre_start, info: transfer['status'])
83
+ when 'running'
84
+ if !started
85
+ notify_progress(session_id: @xfer_id, type: :session_start)
86
+ started = true
87
+ end
88
+ if !pre_calc && (transfer['bytes_expected'] != 0)
89
+ notify_progress(type: :session_size, session_id: @xfer_id, info: transfer['bytes_expected'])
90
+ pre_calc = true
91
+ else
92
+ notify_progress(type: :transfer, session_id: @xfer_id, info: transfer['bytes_written'])
93
+ end
94
+ when 'completed'
95
+ notify_progress(type: :end, session_id: @xfer_id)
96
+ break
97
+ when 'failed'
98
+ notify_progress(type: :end, session_id: @xfer_id)
99
+ raise Fasp::Error, transfer['error_desc']
100
+ when 'cancelled'
101
+ notify_progress(type: :end, session_id: @xfer_id)
102
+ raise Fasp::Error, 'Transfer cancelled by user'
103
+ else
104
+ notify_progress(type: :end, session_id: @xfer_id)
105
+ raise Fasp::Error, "unknown status: #{transfer['status']}: #{transfer['error_desc']}"
106
+ end
107
+ sleep(1)
108
+ end
109
+ rescue StandardError => e
110
+ return [e]
111
+ end
112
+ return [:success]
113
+ end # wait
114
+ end # AgentAlpha
115
+ end
116
+ end
@@ -1,94 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/log'
4
+ require 'aspera/assert'
3
5
  module Aspera
4
6
  module Fasp
5
- # Base class for FASP transfer agents
6
- # sub classes shall implement start_transfer and shutdown
7
+ # Base class for transfer agents
7
8
  class AgentBase
8
- # fields description for JSON generation
9
- # spellchecker: disable
10
- INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
11
- DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
12
- ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
13
- ArgScansCompleted PathScansAttempted FileScansCompleted TransfersAttempted TransfersPassed Delay].freeze
14
- BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
15
- MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
16
- EXPECTED_METHODS = %i[text struct enhanced].freeze
17
- private_constant :INTEGER_FIELDS, :BOOLEAN_FIELDS, :EXPECTED_METHODS
18
- # spellchecker: enable
19
-
20
9
  class << self
21
- # This checks the validity of the value returned by wait_for_transfers_completion
22
- # it must be a list of :success or exception
23
- def validate_status_list(statuses)
24
- raise "internal error: bad statuses type: #{statuses.class}" unless statuses.is_a?(Array)
25
- raise "internal error: bad statuses content: #{statuses}" unless statuses.select{|i|!i.eql?(:success) && !i.is_a?(StandardError)}.empty?
26
- end
27
- end
28
-
29
- private
30
-
31
- # translates legacy event into enhanced (JSON) event
32
- def enhanced_event_format(event)
33
- return event.keys.each_with_object({}) do |e, h|
34
- # capital_to_snake_case
35
- new_name = e
36
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
37
- .gsub(/([a-z\d])(usec)$/, '\1_\2')
38
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
39
- .downcase
40
- value = event[e]
41
- value = value.to_i if INTEGER_FIELDS.include?(e)
42
- value = value.eql?('Yes') if BOOLEAN_FIELDS.include?(e)
43
- h[new_name] = value
44
- end
45
- end
46
-
47
- def initialize
48
- @listeners = []
49
- end
50
-
51
- def notify_listeners(current_event_text, current_event_data)
52
- Log.log.debug('send event to listeners')
53
- enhanced_event = nil
54
- @listeners.each do |listener|
55
- listener.event_text(current_event_text) if listener.respond_to?(:event_text)
56
- listener.event_struct(current_event_data) if listener.respond_to?(:event_struct)
57
- if listener.respond_to?(:event_enhanced)
58
- enhanced_event = enhanced_event_format(current_event_data) if enhanced_event.nil?
59
- listener.event_enhanced(enhanced_event)
10
+ # compute options from user provided and default options
11
+ def options(default:, options:)
12
+ result = options.symbolize_keys
13
+ available = default.map{|k, v|"#{k}(#{v})"}.join(', ')
14
+ result.each do |k, _v|
15
+ assert_values(k, default.keys){"transfer agent parameter: #{k}"}
16
+ # check it is the expected type: too limiting, as we can have an Integer or Float, or symbol and string
17
+ # raise "Invalid value for transfer agent parameter: #{k}, expect #{default[k].class.name}" unless default[k].nil? || v.is_a?(default[k].class)
18
+ end
19
+ default.each do |k, v|
20
+ raise "Missing required agent parameter: #{k}. Parameters: #{available}" if v.eql?(:required) && !result.key?(k)
21
+ result[k] = v unless result.key?(k)
60
22
  end
23
+ return result
61
24
  end
62
- end # notify_listeners
63
25
 
64
- def notify_begin(id, size)
65
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size})
26
+ def agent_list
27
+ Dir.entries(File.dirname(File.expand_path(__FILE__))).select do |file|
28
+ file.start_with?('agent_') && !file.eql?('agent_base.rb')
29
+ end.map{|file|file.sub(/^agent_/, '').sub(/\.rb$/, '').to_sym}
30
+ end
31
+ end
32
+ def wait_for_completion
33
+ # list of: :success or "error message string"
34
+ statuses = wait_for_transfers_completion
35
+ @progress&.reset
36
+ assert_type(statuses, Array)
37
+ assert(statuses.select{|i|!i.eql?(:success) && !i.is_a?(StandardError)}.empty?){"bad statuses content: #{statuses}"}
38
+ return statuses
66
39
  end
67
40
 
68
- def notify_progress(id, size)
69
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'STATS', 'Bytescont' => size})
70
- end
41
+ private
71
42
 
72
- def notify_end(id)
73
- notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'DONE'})
43
+ def initialize(options)
44
+ # method `shutdown` is optional
45
+ assert(respond_to?(:start_transfer))
46
+ assert(respond_to?(:wait_for_transfers_completion))
47
+ assert_type(options, Hash){'transfer agent options'}
48
+ Log.log.debug{Log.dump(:agent_options, options)}
49
+ @progress = options[:progress]
50
+ options.delete(:progress)
74
51
  end
75
52
 
76
- public
77
-
78
- LISTENER_SESSION_ID_B = 'ListenerSessionId'
79
- LISTENER_SESSION_ID_S = 'listener_session_id'
80
-
81
- # listener receives events
82
- def add_listener(listener)
83
- raise "expect one of #{EXPECTED_METHODS}" if EXPECTED_METHODS.inject(0){|m, e|m + (listener.respond_to?("event_#{e}") ? 1 : 0)}.eql?(0)
84
- @listeners.push(listener)
85
- self
53
+ def notify_progress(**parameters)
54
+ @progress&.event(**parameters)
86
55
  end
87
-
88
- # the following methods must be implemented by subclass:
89
- # start_transfer(transfer_spec, token_regenerator: nil) : start transfer
90
- # wait_for_transfers_completion : wait for termination of all transfers, @return list of : :success or error message
91
- # optional: shutdown
92
56
  end
93
57
  end
94
58
  end
@@ -4,7 +4,6 @@ require 'aspera/fasp/agent_base'
4
4
  require 'aspera/rest'
5
5
  require 'aspera/open_application'
6
6
  require 'securerandom'
7
- require 'tty-spinner'
8
7
 
9
8
  module Aspera
10
9
  module Fasp
@@ -14,20 +13,20 @@ module Aspera
14
13
  # delay between each try to start connect
15
14
  SLEEP_SEC_BETWEEN_RETRY = 3
16
15
  private_constant :CONNECT_START_URIS, :SLEEP_SEC_BETWEEN_RETRY
17
- def initialize(_options)
18
- super()
16
+ def initialize(options)
17
+ super(options)
19
18
  @connect_settings = {
20
19
  'app_id' => SecureRandom.uuid
21
20
  }
22
21
  raise 'Using connect requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
23
22
  method_index = 0
24
23
  begin
25
- connect_url = Installation.instance.connect_uri
24
+ connect_url = Products.connect_uri
26
25
  Log.log.debug{"found: #{connect_url}"}
27
26
  @connect_api = Rest.new({base_url: "#{connect_url}/v5/connect", headers: {'Origin' => Rest.user_agent}}) # could use v6 also now
28
27
  connect_info = @connect_api.read('info/version')[:data]
29
28
  Log.log.info('Connect was reached') if method_index > 0
30
- Log.dump(:connect_version, connect_info)
29
+ Log.log.debug{Log.dump(:connect_version, connect_info)}
31
30
  rescue StandardError => e # Errno::ECONNREFUSED
32
31
  start_url = CONNECT_START_URIS[method_index]
33
32
  method_index += 1
@@ -74,10 +73,12 @@ module Aspera
74
73
  def wait_for_transfers_completion
75
74
  connect_activity_args = {'aspera_connect_settings' => @connect_settings}
76
75
  started = false
77
- spinner = nil
76
+ pre_calc = false
77
+ session_id = @xfer_id
78
78
  begin
79
79
  loop do
80
80
  tr_info = @connect_api.create("transfers/info/#{@xfer_id}", connect_activity_args)[:data]
81
+ Log.log.trace1{Log.dump(:tr_info, tr_info)}
81
82
  if tr_info['transfer_info'].is_a?(Hash)
82
83
  transfer = tr_info['transfer_info']
83
84
  if transfer.nil?
@@ -86,32 +87,30 @@ module Aspera
86
87
  end
87
88
  # TODO: get session id
88
89
  case transfer['status']
89
- when 'completed'
90
- notify_end(@connect_settings['app_id'])
91
- break
92
90
  when 'initiating', 'queued'
93
- if spinner.nil?
94
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
95
- spinner.start
96
- end
97
- spinner.update(title: transfer['status'])
98
- spinner.spin
91
+ notify_progress(session_id: nil, type: :pre_start, info: transfer['status'])
99
92
  when 'running'
100
- # puts "running: sessions:#{transfer['sessions'].length}, #{transfer['sessions'].map{|i| i['bytes_transferred']}.join(',')}"
101
- if !started && (transfer['bytes_expected'] != 0)
102
- spinner&.success
103
- notify_begin(@connect_settings['app_id'], transfer['bytes_expected'])
93
+ if !started
94
+ notify_progress(session_id: session_id, type: :session_start)
104
95
  started = true
96
+ end
97
+ if !pre_calc && (transfer['bytes_expected'] != 0)
98
+ notify_progress(type: :session_size, session_id: session_id, info: transfer['bytes_expected'])
99
+ pre_calc = true
105
100
  else
106
- notify_progress(@connect_settings['app_id'], transfer['bytes_written'])
101
+ notify_progress(type: :transfer, session_id: session_id, info: transfer['bytes_written'])
107
102
  end
103
+ when 'completed'
104
+ notify_progress(type: :end, session_id: session_id)
105
+ break
108
106
  when 'failed'
109
- spinner&.error
107
+ notify_progress(type: :end, session_id: session_id)
110
108
  raise Fasp::Error, transfer['error_desc']
111
109
  when 'cancelled'
112
- spinner&.error
110
+ notify_progress(type: :end, session_id: session_id)
113
111
  raise Fasp::Error, 'Transfer cancelled by user'
114
112
  else
113
+ notify_progress(type: :end, session_id: session_id)
115
114
  raise Fasp::Error, "unknown status: #{transfer['status']}: #{transfer['error_desc']}"
116
115
  end
117
116
  end