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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +300 -185
- data/CONTRIBUTING.md +74 -23
- data/README.md +2346 -1619
- data/bin/ascli +16 -25
- data/bin/asession +15 -15
- data/examples/dascli +2 -2
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +216 -150
- data/lib/aspera/ascmd.rb +25 -18
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
- data/lib/aspera/cli/error.rb +17 -0
- data/lib/aspera/cli/extended_value.rb +51 -16
- data/lib/aspera/cli/formatter.rb +276 -174
- data/lib/aspera/cli/hints.rb +81 -0
- data/lib/aspera/cli/main.rb +114 -147
- data/lib/aspera/cli/manager.rb +181 -136
- data/lib/aspera/cli/plugin.rb +82 -64
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +327 -331
- data/lib/aspera/cli/plugins/ats.rb +12 -8
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +575 -439
- data/lib/aspera/cli/plugins/console.rb +40 -0
- data/lib/aspera/cli/plugins/cos.rb +4 -5
- data/lib/aspera/cli/plugins/faspex.rb +111 -92
- data/lib/aspera/cli/plugins/faspex5.rb +245 -182
- data/lib/aspera/cli/plugins/node.rb +239 -160
- data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
- data/lib/aspera/cli/plugins/preview.rb +54 -38
- data/lib/aspera/cli/plugins/server.rb +63 -20
- data/lib/aspera/cli/plugins/shares.rb +64 -38
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -67
- data/lib/aspera/cli/transfer_progress.rb +73 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +27 -22
- data/lib/aspera/cos_node.rb +6 -4
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +21 -8
- data/lib/aspera/fasp/agent_alpha.rb +116 -0
- data/lib/aspera/fasp/agent_base.rb +40 -76
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +169 -179
- data/lib/aspera/fasp/agent_httpgw.rb +200 -195
- data/lib/aspera/fasp/agent_node.rb +43 -35
- data/lib/aspera/fasp/agent_trsdk.rb +124 -41
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +89 -191
- data/lib/aspera/fasp/management.rb +249 -0
- data/lib/aspera/fasp/parameters.rb +86 -47
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +6 -6
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +8 -7
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +51 -0
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +61 -19
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +105 -21
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +57 -36
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +5 -4
- data/lib/aspera/preview/file_types.rb +56 -268
- data/lib/aspera/preview/generator.rb +28 -39
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +36 -16
- data/lib/aspera/preview/utils.rb +23 -29
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +127 -80
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +16 -14
- data/lib/aspera/rest_errors_aspera.rb +39 -34
- data/lib/aspera/secret_hider.rb +18 -17
- data/lib/aspera/ssh.rb +10 -5
- data/lib/aspera/temp_file_manager.rb +11 -4
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +11 -5
- data.tar.gz.sig +0 -0
- metadata +108 -39
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/listener/line_dump.rb +0 -19
- data/lib/aspera/cli/listener/logger.rb +0 -22
- data/lib/aspera/cli/listener/progress.rb +0 -50
- data/lib/aspera/cli/listener/progress_multi.rb +0 -84
- data/lib/aspera/cli/plugins/sync.rb +0 -44
- data/lib/aspera/fasp/listener.rb +0 -13
- 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
|
-
|
32
|
+
assert_type(options, Hash){name}
|
31
33
|
unsupported_keys = options.keys - OPTIONS_KEYS
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
@
|
63
|
-
|
65
|
+
@result = {
|
66
|
+
env: {},
|
67
|
+
args: []
|
68
|
+
}
|
64
69
|
@used_param_names = []
|
65
70
|
end
|
66
71
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
def add_env_args(
|
70
|
-
Log.log.debug{"ENV=#{@
|
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!(@
|
75
|
-
args.push(*@
|
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|@
|
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
|
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
|
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
|
-
|
160
|
-
@
|
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
|
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
|
data/lib/aspera/cos_node.rb
CHANGED
@@ -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
|
12
|
+
def parameters_from_svc_credentials(service_credentials, bucket_region)
|
11
13
|
# check necessary contents
|
12
|
-
|
14
|
+
assert_type(service_credentials, Hash){'service_credentials'}
|
13
15
|
%w[apikey resource_instance_id endpoints].each do |field|
|
14
|
-
|
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
|
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/
|
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__,
|
43
|
+
File.read(File.join(__dir__, DATA_FOLDER_NAME, id.to_s), mode: 'rb')
|
13
44
|
end
|
14
45
|
end
|
15
46
|
end
|
data/lib/aspera/environment.rb
CHANGED
@@ -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
|
75
|
-
ENV['HOME'] = ENV
|
76
|
-
Log.log.debug{"Windows: set
|
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,
|
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
|
-
|
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
|
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
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
69
|
-
notify_listeners('emulated', {LISTENER_SESSION_ID_B => id, 'Type' => 'STATS', 'Bytescont' => size})
|
70
|
-
end
|
41
|
+
private
|
71
42
|
|
72
|
-
def
|
73
|
-
|
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
|
-
|
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(
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
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(
|
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
|
-
|
107
|
+
notify_progress(type: :end, session_id: session_id)
|
110
108
|
raise Fasp::Error, transfer['error_desc']
|
111
109
|
when 'cancelled'
|
112
|
-
|
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
|