aspera-cli 4.25.1 → 4.25.2
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/CHANGELOG.md +46 -1
- data/CONTRIBUTING.md +6 -11
- data/README.md +31 -9742
- data/bin/asession +111 -88
- data/lib/aspera/agent/connect.rb +1 -1
- data/lib/aspera/agent/desktop.rb +1 -1
- data/lib/aspera/agent/direct.rb +19 -18
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/aoc.rb +25 -8
- data/lib/aspera/api/node.rb +16 -14
- data/lib/aspera/ascp/installation.rb +32 -51
- data/lib/aspera/assert.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +1 -0
- data/lib/aspera/cli/formatter.rb +0 -4
- data/lib/aspera/cli/hints.rb +11 -4
- data/lib/aspera/cli/main.rb +3 -6
- data/lib/aspera/cli/manager.rb +8 -4
- data/lib/aspera/cli/plugins/aoc.rb +11 -14
- data/lib/aspera/cli/plugins/base.rb +1 -1
- data/lib/aspera/cli/plugins/config.rb +50 -48
- data/lib/aspera/cli/plugins/factory.rb +2 -2
- data/lib/aspera/cli/plugins/faspex5.rb +4 -3
- data/lib/aspera/cli/plugins/preview.rb +6 -11
- data/lib/aspera/cli/transfer_agent.rb +2 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +30 -16
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/hash_ext.rb +8 -0
- data/lib/aspera/log.rb +3 -4
- data/lib/aspera/markdown.rb +17 -0
- data/lib/aspera/oauth/base.rb +1 -1
- data/lib/aspera/oauth/web.rb +1 -1
- data/lib/aspera/preview/generator.rb +9 -9
- data/lib/aspera/rest_call_error.rb +16 -8
- data/lib/aspera/rest_error_analyzer.rb +1 -1
- data/lib/aspera/transfer/resumer.rb +2 -2
- data/lib/aspera/yaml.rb +49 -0
- data.tar.gz.sig +0 -0
- metadata +16 -2
- metadata.gz.sig +0 -0
- data/release_notes.md +0 -8
data/bin/asession
CHANGED
|
@@ -6,95 +6,118 @@ require 'aspera/agent/direct'
|
|
|
6
6
|
require 'aspera/cli/extended_value'
|
|
7
7
|
require 'aspera/products/transferd'
|
|
8
8
|
require 'aspera/log'
|
|
9
|
+
require 'aspera/assert'
|
|
9
10
|
require 'json'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
11
|
+
|
|
12
|
+
class UsageError < StandardError; end
|
|
13
|
+
|
|
14
|
+
class SimpleTransferManager
|
|
15
|
+
# First level parameters in session spec
|
|
16
|
+
# - Extended transfer spec parameter (only used in asession)
|
|
17
|
+
PARAM_SPEC = 'spec'
|
|
18
|
+
# - Log level
|
|
19
|
+
PARAM_LOG_LEVEL = 'loglevel'
|
|
20
|
+
# - Transfer agent options
|
|
21
|
+
PARAM_AGENT = 'agent'
|
|
22
|
+
# - By default go to /tmp/username.filelist
|
|
23
|
+
PARAM_TMP_FILE_LIST_FOLDER = 'file_list_folder'
|
|
24
|
+
# - SDK location
|
|
25
|
+
PARAM_SDK = 'sdk'
|
|
26
|
+
# Default SDK location
|
|
27
|
+
SDK_DEFAULT_DIR = File.join(Dir.home, '.aspera', 'sdk')
|
|
28
|
+
# Place transfer spec in that
|
|
29
|
+
SAMPLE_DEMO = '"remote_host":"demo.asperasoft.com","remote_user":"asperaweb","ssh_port":33001,"remote_password":"demoaspera"'
|
|
30
|
+
SAMPLE_DEMO2 = '"direction":"receive","destination_root":"./test.dir"'
|
|
31
|
+
STDIN_INPUT = '@json:@stdin:'
|
|
32
|
+
class << self
|
|
33
|
+
# Assert usage condition, otherwise display usage message and exit
|
|
34
|
+
# @param assertion [Boolean] Assertion to verify
|
|
35
|
+
# @param error_message [String] Error message to display
|
|
36
|
+
def usage
|
|
37
|
+
# rubocop:disable Style/StderrPuts
|
|
38
|
+
$stderr.puts('USAGE')
|
|
39
|
+
$stderr.puts(' asession')
|
|
40
|
+
$stderr.puts(' asession -h|--help')
|
|
41
|
+
$stderr.puts(' asession [<session spec extended value>]')
|
|
42
|
+
$stderr.puts(' ')
|
|
43
|
+
$stderr.puts(' If no argument is provided, default will be used: @json:@stdin')
|
|
44
|
+
$stderr.puts(' -h, --help display this message')
|
|
45
|
+
$stderr.puts(' <session spec extended value> a dictionary (Hash)')
|
|
46
|
+
$stderr.puts(' The value can be either:')
|
|
47
|
+
$stderr.puts(" the JSON description itself, e.g. @json:'{\"xx\":\"yy\",...}'")
|
|
48
|
+
$stderr.puts(' @json:@stdin, if the JSON is provided from stdin')
|
|
49
|
+
$stderr.puts(' @json:@file:<path>, if the JSON is provided from a file')
|
|
50
|
+
$stderr.puts(' The following keys are recognized in session spec:')
|
|
51
|
+
$stderr.puts(" #{PARAM_SPEC} : mandatory, contains the transfer spec")
|
|
52
|
+
$stderr.puts(" #{PARAM_LOG_LEVEL} : modify log level (to stderr)")
|
|
53
|
+
$stderr.puts(" #{PARAM_AGENT} : modify transfer agent parameters, e.g. ascp_args")
|
|
54
|
+
$stderr.puts(" #{PARAM_TMP_FILE_LIST_FOLDER} : location of temporary files")
|
|
55
|
+
$stderr.puts(" #{PARAM_SDK} : location of SDK (ascp)")
|
|
56
|
+
$stderr.puts(' Asynchronous commands can be provided on STDIN, examples:')
|
|
57
|
+
$stderr.puts(' {"type":"START","source":"/aspera-test-dir-tiny/200KB.2"}')
|
|
58
|
+
$stderr.puts(' {"type":"START","source":"xx","destination":"yy"}')
|
|
59
|
+
$stderr.puts(' {"type":"DONE"}')
|
|
60
|
+
$stderr.puts('EXAMPLES')
|
|
61
|
+
$stderr.puts(%Q( asession @json:'{"#{PARAM_SPEC}":{#{SAMPLE_DEMO},#{SAMPLE_DEMO2},"paths":[{"source":"/aspera-test-dir-tiny/200KB.1"}]}}'))
|
|
62
|
+
$stderr.puts(%Q( echo '{"#{PARAM_SPEC}":{"remote_host":...}}'|asession #{STDIN_INPUT}))
|
|
63
|
+
# rubocop:enable Style/StderrPuts
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
def initialize(args)
|
|
67
|
+
parameter_source_err_msg = ' (argument), did you specify: "@json:" ?'
|
|
68
|
+
# By default assume JSON input on stdin if no argument
|
|
69
|
+
if args.empty?
|
|
70
|
+
args.push(STDIN_INPUT)
|
|
71
|
+
parameter_source_err_msg = ' (JSON on stdin)'
|
|
72
|
+
end
|
|
73
|
+
# Anyway expect only one argument: session information
|
|
74
|
+
Aspera.assert(args.length.eql?(1), 'exactly one argument is expected', type: UsageError)
|
|
75
|
+
if ['-h', '--help'].include?(args.first)
|
|
76
|
+
SimpleTransferManager.usage
|
|
77
|
+
exit(0)
|
|
78
|
+
end
|
|
79
|
+
# Parse transfer spec
|
|
80
|
+
@session_spec = Aspera::Cli::ExtendedValue.instance.evaluate(args.pop, context: 'asession parameter')
|
|
81
|
+
# Ensure right type for parameter
|
|
82
|
+
Aspera.assert(@session_spec.is_a?(Hash), "The value must be a Hash#{parameter_source_err_msg}", type: UsageError)
|
|
83
|
+
Aspera.assert(@session_spec[PARAM_SPEC].is_a?(Hash), "The value must contain key #{PARAM_SPEC} with Hash value", type: UsageError)
|
|
84
|
+
# Additional debug capability
|
|
85
|
+
Aspera::Log.instance.level = @session_spec[PARAM_LOG_LEVEL].to_sym if @session_spec.key?(PARAM_LOG_LEVEL)
|
|
86
|
+
# Possibly override temp folder
|
|
87
|
+
Aspera::Transfer::Parameters.file_list_folder = @session_spec[PARAM_TMP_FILE_LIST_FOLDER] if @session_spec.key?(PARAM_TMP_FILE_LIST_FOLDER)
|
|
88
|
+
Aspera::Products::Transferd.sdk_directory = @session_spec[PARAM_SDK] || SDK_DEFAULT_DIR
|
|
89
|
+
agent_params = @session_spec[PARAM_AGENT] || {}
|
|
90
|
+
agent_params['quiet'] = true
|
|
91
|
+
agent_params['management_cb'] = ->(event) do
|
|
92
|
+
puts JSON.generate(Aspera::Ascp::Management.event_native_to_snake(event))
|
|
93
|
+
end
|
|
94
|
+
# Get local agent (ascp), disable ascp output on stdout to not mix with JSON events
|
|
95
|
+
@client = Aspera::Agent::Direct.new(**agent_params.symbolize_keys)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def start
|
|
99
|
+
# Start transfer (asynchronous)
|
|
100
|
+
@client.start_transfer(@session_spec[PARAM_SPEC])
|
|
101
|
+
# commands to ascp on mgt port
|
|
102
|
+
Thread.new do
|
|
103
|
+
loop do
|
|
104
|
+
data = JSON.parse($stdin.gets)
|
|
105
|
+
@client.send_command(data)
|
|
106
|
+
end
|
|
107
|
+
rescue => e
|
|
108
|
+
Aspera::Log.log.error("Error reading commands from stdin: #{e.message}")
|
|
109
|
+
Process.exit(1)
|
|
110
|
+
end
|
|
111
|
+
# No exit code: status is success (0)
|
|
112
|
+
@client.wait_for_transfers_completion
|
|
113
|
+
@client.shutdown
|
|
114
|
+
end
|
|
59
115
|
end
|
|
60
|
-
|
|
61
|
-
assert_usage(ARGV.length.eql?(1), 'exactly one argument is expected')
|
|
62
|
-
assert_usage(!['-h', '--help'].include?(ARGV.first), nil)
|
|
63
|
-
# Parse transfer spec
|
|
116
|
+
|
|
64
117
|
begin
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# Ensure right type for parameter
|
|
71
|
-
assert_usage(session_spec.is_a?(Hash), "The value must be a Hash#{parameter_source_err_msg}")
|
|
72
|
-
assert_usage(session_spec[PARAM_SPEC].is_a?(Hash), "The value must contain key #{PARAM_SPEC} with Hash value")
|
|
73
|
-
# Additional debug capability
|
|
74
|
-
Aspera::Log.instance.level = session_spec[PARAM_LOG_LEVEL].to_sym if session_spec.key?(PARAM_LOG_LEVEL)
|
|
75
|
-
# Possibly override temp folder
|
|
76
|
-
Aspera::Transfer::Parameters.file_list_folder = session_spec[PARAM_TMP_FILE_LIST_FOLDER] if session_spec.key?(PARAM_TMP_FILE_LIST_FOLDER)
|
|
77
|
-
session_spec[PARAM_SDK] = File.join(Dir.home, '.aspera', 'sdk') unless session_spec.key?(PARAM_SDK)
|
|
78
|
-
Aspera::Products::Transferd.sdk_directory = session_spec[PARAM_SDK]
|
|
79
|
-
session_spec[PARAM_AGENT] = {} unless session_spec.key?(PARAM_AGENT)
|
|
80
|
-
agent_params = session_spec[PARAM_AGENT]
|
|
81
|
-
agent_params['quiet'] = true
|
|
82
|
-
agent_params['management_cb'] = ->(event) do
|
|
83
|
-
puts JSON.generate(Aspera::Ascp::Management.event_native_to_snake(event))
|
|
84
|
-
end
|
|
85
|
-
# Get local agent (ascp), disable ascp output on stdout to not mix with JSON events
|
|
86
|
-
client = Aspera::Agent::Direct.new(**agent_params.symbolize_keys)
|
|
87
|
-
# Start transfer (asynchronous)
|
|
88
|
-
client.start_transfer(session_spec[PARAM_SPEC])
|
|
89
|
-
# commands to ascp on mgt port
|
|
90
|
-
Thread.new do
|
|
91
|
-
loop do
|
|
92
|
-
data = JSON.parse($stdin.gets)
|
|
93
|
-
client.send_command(data)
|
|
94
|
-
end
|
|
95
|
-
rescue
|
|
96
|
-
Process.exit(1)
|
|
118
|
+
SimpleTransferManager.new(ARGV).start
|
|
119
|
+
rescue UsageError => e
|
|
120
|
+
Aspera::Log.log.error(e.message)
|
|
121
|
+
SimpleTransferManager.usage
|
|
122
|
+
exit(1)
|
|
97
123
|
end
|
|
98
|
-
# No exit code: status is success (0)
|
|
99
|
-
client.wait_for_transfers_completion
|
|
100
|
-
client.shutdown
|
data/lib/aspera/agent/connect.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Aspera
|
|
|
32
32
|
headers: {'Origin' => RestParameters.instance.user_agent}
|
|
33
33
|
)
|
|
34
34
|
connect_info = @connect_api.read('info/version')
|
|
35
|
-
Log.log.
|
|
35
|
+
Log.log.debug('Connect was reached') if method_index > 0
|
|
36
36
|
Log.dump(:connect_version, connect_info)
|
|
37
37
|
rescue StandardError => e # Errno::ECONNREFUSED
|
|
38
38
|
Log.log.debug{"Exception: #{e}"}
|
data/lib/aspera/agent/desktop.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Aspera
|
|
|
30
30
|
@client_app_api = Aspera::JsonRpcClient.new(Aspera::Rest.new(base_url: aspera_client_api_url))
|
|
31
31
|
client_info = @client_app_api.get_info
|
|
32
32
|
Log.dump(:client_version, client_info)
|
|
33
|
-
Log.log.
|
|
33
|
+
Log.log.debug('Client was reached') if method_index > 0
|
|
34
34
|
rescue Errno::ECONNREFUSED => e
|
|
35
35
|
start_url = START_URIS[method_index]
|
|
36
36
|
method_index += 1
|
data/lib/aspera/agent/direct.rb
CHANGED
|
@@ -16,9 +16,9 @@ require 'English'
|
|
|
16
16
|
|
|
17
17
|
module Aspera
|
|
18
18
|
module Agent
|
|
19
|
-
#
|
|
19
|
+
# Execute a local `ascp` and use its management port to monitor progress
|
|
20
20
|
class Direct < Base
|
|
21
|
-
# ascp started locally, so listen local
|
|
21
|
+
# `ascp` started locally, so listen local
|
|
22
22
|
LISTEN_LOCAL_ADDRESS = '127.0.0.1'
|
|
23
23
|
# 0 means: use any available port
|
|
24
24
|
SELECT_AVAILABLE_PORT = 0
|
|
@@ -48,13 +48,13 @@ module Aspera
|
|
|
48
48
|
spawn_timeout_sec: 2,
|
|
49
49
|
spawn_delay_sec: 2,
|
|
50
50
|
multi_incr_udp: nil,
|
|
51
|
-
resume:
|
|
51
|
+
resume: {},
|
|
52
52
|
monitor: true,
|
|
53
53
|
management_cb: nil,
|
|
54
54
|
**base_options
|
|
55
55
|
)
|
|
56
56
|
super(**base_options)
|
|
57
|
-
#
|
|
57
|
+
# Options that have impact on `ascp` command line generated
|
|
58
58
|
@tr_opts = {
|
|
59
59
|
ascp_args: ascp_args,
|
|
60
60
|
wss: wss,
|
|
@@ -69,7 +69,6 @@ module Aspera
|
|
|
69
69
|
@multi_incr_udp = multi_incr_udp.nil? ? Environment.instance.os.eql?(Environment::OS_WINDOWS) : multi_incr_udp
|
|
70
70
|
@monitor = monitor
|
|
71
71
|
@management_cb = management_cb
|
|
72
|
-
resume = {} if resume.nil?
|
|
73
72
|
Aspera.assert_type(resume, Hash){'resume'}
|
|
74
73
|
@resume_policy = Transfer::Resumer.new(**resume.symbolize_keys)
|
|
75
74
|
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
|
|
@@ -78,6 +77,7 @@ module Aspera
|
|
|
78
77
|
@mutex = Mutex.new
|
|
79
78
|
@pre_calc_sent = false
|
|
80
79
|
@pre_calc_last_size = nil
|
|
80
|
+
# Check on all management messages if that file exists, and if so, read commands from it
|
|
81
81
|
@command_file = File.join(config_dir || '.', "send_#{$PROCESS_ID}")
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -115,7 +115,7 @@ module Aspera
|
|
|
115
115
|
Log.log.debug('multi_session count is zero: no multi session')
|
|
116
116
|
multi_session_info = nil
|
|
117
117
|
elsif @multi_incr_udp # multi_session_info[:count] > 0
|
|
118
|
-
# if option not true
|
|
118
|
+
# if option not `true`: keep default UDP port for all sessions
|
|
119
119
|
multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] : Transfer::Spec::UDP_PORT
|
|
120
120
|
# delete from original transfer spec, as we will increment values
|
|
121
121
|
transfer_spec.delete('fasp_port')
|
|
@@ -225,9 +225,9 @@ module Aspera
|
|
|
225
225
|
start_and_monitor_process(session: session, **session[:env_args])
|
|
226
226
|
end
|
|
227
227
|
Log.log.debug('transfer ok'.bg_green)
|
|
228
|
-
rescue
|
|
228
|
+
rescue => e
|
|
229
229
|
session[:error] = e
|
|
230
|
-
Log.log.
|
|
230
|
+
raise if Log.log.debug? || !e.is_a?(Transfer::Error)
|
|
231
231
|
end
|
|
232
232
|
Log.log.debug{"EXIT (#{Thread.current[:name]})"}
|
|
233
233
|
end
|
|
@@ -242,7 +242,7 @@ module Aspera
|
|
|
242
242
|
# @param env [Hash] Environment variables (comes from ascp_args)
|
|
243
243
|
# @param args [Array] Command line arguments (comes from ascp_args)
|
|
244
244
|
# @return [nil] when process has exited
|
|
245
|
-
# @
|
|
245
|
+
# @raise FaspError on error
|
|
246
246
|
def start_and_monitor_process(
|
|
247
247
|
session:,
|
|
248
248
|
name:,
|
|
@@ -251,15 +251,15 @@ module Aspera
|
|
|
251
251
|
)
|
|
252
252
|
Aspera.assert_type(session, Hash)
|
|
253
253
|
notify_progress(:sessions_init, info: 'starting')
|
|
254
|
+
# Do not use `capture_stderr`
|
|
255
|
+
capture_stderr = false
|
|
256
|
+
stderr_r, stderr_w = nil
|
|
257
|
+
spawn_args = {}
|
|
258
|
+
command_pid = nil
|
|
259
|
+
# Get location of command executable (ascp, async)
|
|
260
|
+
command_path = Ascp::Installation.instance.path(name)
|
|
261
|
+
command_arguments = [command_path]
|
|
254
262
|
begin
|
|
255
|
-
# do not use
|
|
256
|
-
capture_stderr = false
|
|
257
|
-
stderr_r, stderr_w = nil
|
|
258
|
-
spawn_args = {}
|
|
259
|
-
command_pid = nil
|
|
260
|
-
# get location of command executable (ascp, async)
|
|
261
|
-
command_path = Ascp::Installation.instance.path(name)
|
|
262
|
-
command_arguments = [command_path]
|
|
263
263
|
if @monitor
|
|
264
264
|
# we use Socket directly, instead of TCPServer, as it gives access to lower level options
|
|
265
265
|
socket_class = defined?(JRUBY_VERSION) ? ServerSocket : Socket
|
|
@@ -353,7 +353,7 @@ module Aspera
|
|
|
353
353
|
Process.kill(:INT, command_pid) if @monitor && !Environment.instance.os.eql?(Environment::OS_WINDOWS)
|
|
354
354
|
# collect process exit status or wait for termination
|
|
355
355
|
_, status = Process.wait2(command_pid)
|
|
356
|
-
if
|
|
356
|
+
if capture_stderr
|
|
357
357
|
# process stderr of ascp
|
|
358
358
|
stderr_flag = false
|
|
359
359
|
stderr_r.each_line do |line|
|
|
@@ -379,6 +379,7 @@ module Aspera
|
|
|
379
379
|
|
|
380
380
|
private
|
|
381
381
|
|
|
382
|
+
# [Array<Hash>] List of sessions, one per `ascp` process
|
|
382
383
|
attr_reader :sessions
|
|
383
384
|
|
|
384
385
|
# Notify progress to callback
|
data/lib/aspera/agent/node.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Aspera
|
|
|
77
77
|
# status is empty sometimes with status 200...
|
|
78
78
|
transfer_data = node_api_.read("ops/transfers/#{@transfer_id}") || {'status' => 'unknown'} rescue {'status' => 'waiting(api error)'}
|
|
79
79
|
case transfer_data['status']
|
|
80
|
-
when 'waiting', 'partially_completed', 'unknown', 'waiting(read error)'
|
|
80
|
+
when 'waiting', 'partially_completed', 'unknown', 'waiting(read error)', 'waiting(api error)'
|
|
81
81
|
notify_progress(:sessions_init, info: transfer_data['status'])
|
|
82
82
|
when 'running'
|
|
83
83
|
if !session_started
|
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Aspera
|
|
|
17
17
|
DEFAULT_WORKSPACE = ''
|
|
18
18
|
# Production domain of AoC
|
|
19
19
|
SAAS_DOMAIN_PROD = 'ibmaspera.com' # cspell:disable-line
|
|
20
|
-
#
|
|
20
|
+
# To avoid infinite loop in pub link redirection
|
|
21
21
|
MAX_AOC_URL_REDIRECT = 10
|
|
22
22
|
CLIENT_ID_PREFIX = 'aspera.'
|
|
23
23
|
# Well-known AoC global client apps
|
|
@@ -47,7 +47,7 @@ module Aspera
|
|
|
47
47
|
:PERMISSIONS_CREATED,
|
|
48
48
|
:ID_AK_ADMIN
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Various API scopes supported
|
|
51
51
|
module Scope
|
|
52
52
|
SELF = 'self'
|
|
53
53
|
USER = 'user:all'
|
|
@@ -195,6 +195,23 @@ module Aspera
|
|
|
195
195
|
def workspace_access?(permission)
|
|
196
196
|
permission['access_id'].start_with?("#{ID_AK_ADMIN}_WS_")
|
|
197
197
|
end
|
|
198
|
+
|
|
199
|
+
# Expand access levels to full list of levels.
|
|
200
|
+
# @param levels [nil, String, Array] Access levels
|
|
201
|
+
# @return [Array] Expanded access levels
|
|
202
|
+
def expand_access_levels(levels)
|
|
203
|
+
case levels
|
|
204
|
+
when nil, 'edit' then Node::ACCESS_LEVELS
|
|
205
|
+
when 'preview' then %w[list preview]
|
|
206
|
+
when 'download' then %w[list preview read]
|
|
207
|
+
when 'upload' then %w[mkdir write]
|
|
208
|
+
when Array
|
|
209
|
+
Aspera.assert_array_all(levels, String){'access_levels'}
|
|
210
|
+
levels.each{ |level| Aspera.assert_value(level, Node::ACCESS_LEVELS){'access_level'}}
|
|
211
|
+
levels
|
|
212
|
+
else Aspera.error_unexpected_value(levels){"access_levels must be a list of #{Node::ACCESS_LEVELS.join(', ')} or one of edit, preview, download, upload"}
|
|
213
|
+
end
|
|
214
|
+
end
|
|
198
215
|
end
|
|
199
216
|
|
|
200
217
|
attr_reader :private_link
|
|
@@ -316,9 +333,9 @@ module Aspera
|
|
|
316
333
|
@cache_user_info =
|
|
317
334
|
begin
|
|
318
335
|
read('self')
|
|
319
|
-
rescue
|
|
320
|
-
raise
|
|
321
|
-
Log.log.debug{"
|
|
336
|
+
rescue Aspera::RestCallError => e
|
|
337
|
+
raise if exception || e.message.include?('invalid_grant')
|
|
338
|
+
Log.log.debug{"Ignoring error: (#{e.class}) #{e}"}
|
|
322
339
|
{}
|
|
323
340
|
end
|
|
324
341
|
USER_INFO_FIELDS_MIN.each{ |f| @cache_user_info[f] = nil if @cache_user_info[f].nil?}
|
|
@@ -394,10 +411,10 @@ module Aspera
|
|
|
394
411
|
# @param node_id [String] identifier of node in AoC
|
|
395
412
|
# @param workspace_id [String,nil] workspace identifier
|
|
396
413
|
# @param workspace_name [String,nil] workspace name
|
|
397
|
-
# @param scope [String,nil] e.g. Node::
|
|
414
|
+
# @param scope [String,nil] e.g. Node::Scope::USER, or Node::Scope::ADMIN, or nil (requires secret)
|
|
398
415
|
# @param package_info [Hash,nil] created package information
|
|
399
416
|
# @returns [Node] a node API for access key
|
|
400
|
-
def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::
|
|
417
|
+
def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::Scope::USER, package_info: nil)
|
|
401
418
|
Aspera.assert_type(node_id, String)
|
|
402
419
|
node_info = read("nodes/#{node_id}")
|
|
403
420
|
workspace_name = read("workspaces/#{workspace_id}")['name'] if workspace_name.nil? && !workspace_id.nil?
|
|
@@ -428,7 +445,7 @@ module Aspera
|
|
|
428
445
|
node_params[:auth] = auth_params.clone
|
|
429
446
|
node_params[:auth][:params] ||= {}
|
|
430
447
|
node_params[:auth][:params][:scope] = Node.token_scope(node_info['access_key'], scope)
|
|
431
|
-
node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::
|
|
448
|
+
node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::Scope::ADMIN)
|
|
432
449
|
# Special header required for bearer token only
|
|
433
450
|
node_params[:headers] = {Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
|
|
434
451
|
end
|
data/lib/aspera/api/node.rb
CHANGED
|
@@ -16,9 +16,15 @@ module Aspera
|
|
|
16
16
|
module Api
|
|
17
17
|
# Provides additional functions using node API with gen4 extensions (access keys)
|
|
18
18
|
class Node < Aspera::Rest
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# Format of node scope : node.<access key>:<scope>
|
|
20
|
+
module Scope
|
|
21
|
+
# Node sub-scopes
|
|
22
|
+
USER = 'user:all'
|
|
23
|
+
ADMIN = 'admin:all'
|
|
24
|
+
# Separator between node.AK and user:all
|
|
25
|
+
SEPARATOR = ':'
|
|
26
|
+
NODE_PREFIX = 'node.'
|
|
27
|
+
end
|
|
22
28
|
# Accepted types in `file_matcher`
|
|
23
29
|
MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
|
|
24
30
|
# Delimiter in decoded node token
|
|
@@ -29,20 +35,16 @@ module Aspera
|
|
|
29
35
|
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
30
36
|
# Methods of @app_info[:api]
|
|
31
37
|
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
32
|
-
private_constant :
|
|
38
|
+
private_constant :MATCH_TYPES,
|
|
33
39
|
:SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
|
|
34
40
|
:REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
35
|
-
|
|
36
|
-
# Node API permissions
|
|
41
|
+
# Node API permissions: delete list mkdir preview read rename write
|
|
37
42
|
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
38
43
|
# Special HTTP Headers
|
|
39
44
|
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
40
45
|
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
41
46
|
HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
|
|
42
47
|
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
|
43
|
-
# Node sub-scopes
|
|
44
|
-
SCOPE_USER = 'user:all'
|
|
45
|
-
SCOPE_ADMIN = 'admin:all'
|
|
46
48
|
# / in cloud
|
|
47
49
|
PATH_SEPARATOR = '/'
|
|
48
50
|
|
|
@@ -126,16 +128,16 @@ module Aspera
|
|
|
126
128
|
# Node API scopes
|
|
127
129
|
# @return [String] node scope
|
|
128
130
|
def token_scope(access_key, scope)
|
|
129
|
-
return [
|
|
131
|
+
return [Scope::NODE_PREFIX, access_key, Scope::SEPARATOR, scope].join('')
|
|
130
132
|
end
|
|
131
133
|
|
|
132
134
|
# Decode node scope into access key and scope
|
|
133
135
|
# @return [Hash]
|
|
134
136
|
def decode_scope(scope)
|
|
135
|
-
items = scope.split(
|
|
137
|
+
items = scope.split(Scope::SEPARATOR, 2)
|
|
136
138
|
Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
|
|
137
|
-
Aspera.assert(items[0].start_with?(
|
|
138
|
-
return {access_key: items[0][
|
|
139
|
+
Aspera.assert(items[0].start_with?(Scope::NODE_PREFIX)){"invalid scope: #{scope}"}
|
|
140
|
+
return {access_key: items[0][Scope::NODE_PREFIX.length..-1], scope: items[1]}
|
|
139
141
|
end
|
|
140
142
|
|
|
141
143
|
# Create an Aspera Node bearer token
|
|
@@ -151,7 +153,7 @@ module Aspera
|
|
|
151
153
|
# Manage convenience parameters
|
|
152
154
|
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
153
155
|
payload.delete('_validity')
|
|
154
|
-
scope = payload['_scope'] ||
|
|
156
|
+
scope = payload['_scope'] || Scope::USER
|
|
155
157
|
payload.delete('_scope')
|
|
156
158
|
payload['scope'] ||= token_scope(access_key, scope)
|
|
157
159
|
payload['auth_type'] ||= 'access_key'
|
|
@@ -26,10 +26,7 @@ module Aspera
|
|
|
26
26
|
# Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
|
|
27
27
|
# It is used by object : AgentDirect to find necessary resources
|
|
28
28
|
# By default it takes the first Aspera product found
|
|
29
|
-
# The user can specify ascp location by calling:
|
|
30
|
-
# Installation.instance.use_ascp_from_product(product_name)
|
|
31
|
-
# or
|
|
32
|
-
# Installation.instance.ascp_path=""
|
|
29
|
+
# The user can specify `ascp` location by calling: `sdk_folder=` method
|
|
33
30
|
class Installation
|
|
34
31
|
include Singleton
|
|
35
32
|
|
|
@@ -53,37 +50,37 @@ module Aspera
|
|
|
53
50
|
end
|
|
54
51
|
end
|
|
55
52
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
# Set `ascp` executable "location"
|
|
54
|
+
# It can be:
|
|
55
|
+
# - Full path to folder where `ascp` executable is located
|
|
56
|
+
# - "product:PRODUCT_NAME" to use ascp from named product
|
|
57
|
+
# - "product:FIRST" to use ascp from first found product
|
|
58
|
+
def sdk_folder=(ascp_location)
|
|
59
|
+
Aspera.assert_type(ascp_location, String){'ascp_location'}
|
|
60
|
+
Aspera.assert(!ascp_location.empty?){'ascp location cannot be empty: check your config file'}
|
|
61
|
+
folder =
|
|
62
|
+
if ascp_location.start_with?(USE_PRODUCT_PREFIX)
|
|
63
|
+
product_name = ascp_location[USE_PRODUCT_PREFIX.length..-1]
|
|
64
|
+
if product_name.eql?(FIRST_FOUND)
|
|
65
|
+
pl = installed_products.first
|
|
66
|
+
raise "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" if pl.nil?
|
|
67
|
+
else
|
|
68
|
+
pl = installed_products.find{ |i| i[:name].eql?(product_name)}
|
|
69
|
+
raise "No such product installed: #{product_name}" if pl.nil?
|
|
70
|
+
end
|
|
71
|
+
File.dirname(pl[:ascp_path])
|
|
72
|
+
else
|
|
73
|
+
ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
|
|
74
|
+
end
|
|
75
|
+
Log.log.debug{"ascp_folder=#{folder}"}
|
|
76
|
+
Products::Transferd.sdk_directory = folder
|
|
62
77
|
return
|
|
63
78
|
end
|
|
64
79
|
|
|
65
|
-
def
|
|
80
|
+
def sdk_folder
|
|
66
81
|
path(:ascp)
|
|
67
82
|
end
|
|
68
83
|
|
|
69
|
-
# Compatibility
|
|
70
|
-
def sdk_folder=(v)
|
|
71
|
-
Products::Transferd.sdk_directory = v
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
|
|
75
|
-
# or select one from installed_products()
|
|
76
|
-
def use_ascp_from_product(product_name)
|
|
77
|
-
if product_name.eql?(FIRST_FOUND)
|
|
78
|
-
pl = installed_products.first
|
|
79
|
-
raise "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" if pl.nil?
|
|
80
|
-
else
|
|
81
|
-
pl = installed_products.find{ |i| i[:name].eql?(product_name)}
|
|
82
|
-
raise "No such product installed: #{product_name}" if pl.nil?
|
|
83
|
-
end
|
|
84
|
-
@ascp_path = pl[:ascp_path]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
84
|
# @return [Hash] with key = file name (String), and value = path to file
|
|
88
85
|
def file_paths
|
|
89
86
|
return SDK_FILES.each_with_object({}) do |v, m|
|
|
@@ -113,22 +110,8 @@ module Aspera
|
|
|
113
110
|
case k
|
|
114
111
|
when *EXE_FILES
|
|
115
112
|
file_is_required = k.eql?(:ascp)
|
|
116
|
-
file =
|
|
117
|
-
|
|
118
|
-
else
|
|
119
|
-
# find ascp when needed
|
|
120
|
-
if @ascp_path.nil?
|
|
121
|
-
if @ascp_location.start_with?(USE_PRODUCT_PREFIX)
|
|
122
|
-
use_ascp_from_product(@ascp_location[USE_PRODUCT_PREFIX.length..-1])
|
|
123
|
-
else
|
|
124
|
-
@ascp_path = @ascp_location
|
|
125
|
-
end
|
|
126
|
-
Aspera.assert(File.exist?(@ascp_path)){"No such file: [#{@ascp_path}]"}
|
|
127
|
-
Log.log.debug{"ascp_path=#{@ascp_path}"}
|
|
128
|
-
end
|
|
129
|
-
# NOTE: that there might be a .exe at the end
|
|
130
|
-
@ascp_path.gsub('ascp', k.to_s)
|
|
131
|
-
end
|
|
113
|
+
file = Products::Transferd.transferd_path
|
|
114
|
+
file = File.join(File.dirname(file), Environment.instance.exe_file(k.to_s)) unless k.eql?(:transferd)
|
|
132
115
|
when :ssh_private_dsa, :ssh_private_rsa
|
|
133
116
|
# assume last 3 letters are type
|
|
134
117
|
type = k.to_s[-3..-1].to_sym
|
|
@@ -197,7 +180,7 @@ module Aspera
|
|
|
197
180
|
def ascp_info_from_log
|
|
198
181
|
data = {}
|
|
199
182
|
# read PATHs from ascp directly, and pvcl modules as well
|
|
200
|
-
Open3.popen3(
|
|
183
|
+
Open3.popen3(path(:ascp), '-DDL-') do |_stdin, _stdout, stderr, thread|
|
|
201
184
|
last_line = ''
|
|
202
185
|
while (line = stderr.gets)
|
|
203
186
|
line.chomp!
|
|
@@ -230,13 +213,13 @@ module Aspera
|
|
|
230
213
|
# Openssl information
|
|
231
214
|
def ascp_info_from_file
|
|
232
215
|
data = {}
|
|
233
|
-
File.binread(
|
|
216
|
+
File.binread(path(:ascp)).scan(/[\x20-\x7E]{10,}/) do |bin_string|
|
|
234
217
|
if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
|
|
235
218
|
data['ascp_openssl_dir'] = m[1]
|
|
236
219
|
elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
|
|
237
220
|
data['ascp_openssl_version'] = m[1]
|
|
238
221
|
end
|
|
239
|
-
end if File.file?(
|
|
222
|
+
end if File.file?(path(:ascp))
|
|
240
223
|
return data
|
|
241
224
|
end
|
|
242
225
|
|
|
@@ -374,9 +357,7 @@ module Aspera
|
|
|
374
357
|
private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
|
|
375
358
|
|
|
376
359
|
def initialize
|
|
377
|
-
|
|
378
|
-
@ascp_location = nil
|
|
379
|
-
@sdk_dir = nil
|
|
360
|
+
# cache for installed products found
|
|
380
361
|
@found_products = nil
|
|
381
362
|
@transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
|
|
382
363
|
end
|
data/lib/aspera/assert.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Aspera
|
|
|
55
55
|
assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have (#{value.class})#{value.inspect}"}
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# Assert that all value of array are of the same specified type
|
|
58
|
+
# Assert that all value of array are of the same specified type.
|
|
59
59
|
# @param array [Array] The array to check
|
|
60
60
|
# @param klass [Class] The expected type of elements
|
|
61
61
|
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|