aspera-cli 4.14.0 → 4.16.0

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 (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