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