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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +6 -11
  5. data/README.md +31 -9742
  6. data/bin/asession +111 -88
  7. data/lib/aspera/agent/connect.rb +1 -1
  8. data/lib/aspera/agent/desktop.rb +1 -1
  9. data/lib/aspera/agent/direct.rb +19 -18
  10. data/lib/aspera/agent/node.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +25 -8
  12. data/lib/aspera/api/node.rb +16 -14
  13. data/lib/aspera/ascp/installation.rb +32 -51
  14. data/lib/aspera/assert.rb +1 -1
  15. data/lib/aspera/cli/extended_value.rb +1 -0
  16. data/lib/aspera/cli/formatter.rb +0 -4
  17. data/lib/aspera/cli/hints.rb +11 -4
  18. data/lib/aspera/cli/main.rb +3 -6
  19. data/lib/aspera/cli/manager.rb +8 -4
  20. data/lib/aspera/cli/plugins/aoc.rb +11 -14
  21. data/lib/aspera/cli/plugins/base.rb +1 -1
  22. data/lib/aspera/cli/plugins/config.rb +50 -48
  23. data/lib/aspera/cli/plugins/factory.rb +2 -2
  24. data/lib/aspera/cli/plugins/faspex5.rb +4 -3
  25. data/lib/aspera/cli/plugins/preview.rb +6 -11
  26. data/lib/aspera/cli/transfer_agent.rb +2 -2
  27. data/lib/aspera/cli/version.rb +1 -1
  28. data/lib/aspera/environment.rb +30 -16
  29. data/lib/aspera/faspex_gw.rb +1 -1
  30. data/lib/aspera/faspex_postproc.rb +16 -10
  31. data/lib/aspera/hash_ext.rb +8 -0
  32. data/lib/aspera/log.rb +3 -4
  33. data/lib/aspera/markdown.rb +17 -0
  34. data/lib/aspera/oauth/base.rb +1 -1
  35. data/lib/aspera/oauth/web.rb +1 -1
  36. data/lib/aspera/preview/generator.rb +9 -9
  37. data/lib/aspera/rest_call_error.rb +16 -8
  38. data/lib/aspera/rest_error_analyzer.rb +1 -1
  39. data/lib/aspera/transfer/resumer.rb +2 -2
  40. data/lib/aspera/yaml.rb +49 -0
  41. data.tar.gz.sig +0 -0
  42. metadata +16 -2
  43. metadata.gz.sig +0 -0
  44. 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
- # Extended transfer spec parameter (only used in asession)
11
- PARAM_SPEC = 'spec'
12
- # Log level
13
- PARAM_LOG_LEVEL = 'loglevel'
14
- # Transfer agent options
15
- PARAM_AGENT = 'agent'
16
- # By default go to /tmp/username.filelist
17
- PARAM_TMP_FILE_LIST_FOLDER = 'file_list_folder'
18
- PARAM_SDK = 'sdk'
19
- # Place transfer spec in that
20
- SAMPLE_DEMO = '"remote_host":"demo.asperasoft.com","remote_user":"asperaweb","ssh_port":33001,"remote_password":"demoaspera"'
21
- SAMPLE_DEMO2 = '"direction":"receive","destination_root":"./test.dir"'
22
- def assert_usage(assertion, error_message)
23
- return if assertion
24
- # rubocop:disable Style/StderrPuts
25
- $stderr.puts('ERROR: '.red.blink + error_message) if error_message
26
- $stderr.puts('USAGE')
27
- $stderr.puts(' asession')
28
- $stderr.puts(' asession -h|--help')
29
- $stderr.puts(' asession [<session spec extended value>]')
30
- $stderr.puts(' ')
31
- $stderr.puts(' If no argument is provided, default will be used: @json:@stdin')
32
- $stderr.puts(' -h, --help display this message')
33
- $stderr.puts(' <session spec extended value> a dictionary (Hash)')
34
- $stderr.puts(' The value can be either:')
35
- $stderr.puts(" the JSON description itself, e.g. @json:'{\"xx\":\"yy\",...}'")
36
- $stderr.puts(' @json:@stdin, if the JSON is provided from stdin')
37
- $stderr.puts(' @json:@file:<path>, if the JSON is provided from a file')
38
- $stderr.puts(' The following keys are recognized in session spec:')
39
- $stderr.puts(" #{PARAM_SPEC} : mandatory, contains the transfer spec")
40
- $stderr.puts(" #{PARAM_LOG_LEVEL} : modify log level (to stderr)")
41
- $stderr.puts(" #{PARAM_AGENT} : modify transfer agent parameters, e.g. ascp_args")
42
- $stderr.puts(" #{PARAM_TMP_FILE_LIST_FOLDER} : location of temporary files")
43
- $stderr.puts(" #{PARAM_SDK} : location of SDK (ascp)")
44
- $stderr.puts(' Asynchronous commands can be provided on STDIN, examples:')
45
- $stderr.puts(' {"type":"START","source":"/aspera-test-dir-tiny/200KB.2"}')
46
- $stderr.puts(' {"type":"START","source":"xx","destination":"yy"}')
47
- $stderr.puts(' {"type":"DONE"}')
48
- $stderr.puts('EXAMPLES')
49
- $stderr.puts(%Q( asession @json:'{"#{PARAM_SPEC}":{#{SAMPLE_DEMO},#{SAMPLE_DEMO2},"paths":[{"source":"/aspera-test-dir-tiny/200KB.1"}]}}'))
50
- $stderr.puts(%Q( echo '{"#{PARAM_SPEC}":{"remote_host":...}}'|asession @json:@stdin))
51
- # rubocop:enable Style/StderrPuts
52
- Process.exit(0)
53
- end
54
- parameter_source_err_msg = ' (argument), did you specify: "@json:" ?'
55
- # By default assume JSON input on stdin if no argument
56
- if ARGV.empty?
57
- ARGV.push('@json:@stdin')
58
- parameter_source_err_msg = ' (JSON on stdin)'
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
- # Anyway expect only one argument: session information
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
- session_argument = ARGV.pop
66
- session_spec = Aspera::Cli::ExtendedValue.instance.evaluate(session_argument)
67
- rescue
68
- assert_usage(false, "Cannot parse argument: #{session_argument}")
69
- end
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
@@ -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.info('Connect was reached') if method_index > 0
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}"}
@@ -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.info('Client was reached') if method_index > 0
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
@@ -16,9 +16,9 @@ require 'English'
16
16
 
17
17
  module Aspera
18
18
  module Agent
19
- # executes a local "ascp", create mgt port
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: nil,
51
+ resume: {},
52
52
  monitor: true,
53
53
  management_cb: nil,
54
54
  **base_options
55
55
  )
56
56
  super(**base_options)
57
- # Special transfer parameters provided
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: keep default udp port for all sessions
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 StandardError => e
228
+ rescue => e
229
229
  session[:error] = e
230
- Log.log.error{"Transfer thread error: #{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red} if Log.instance.level.eql?(:debug)
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
- # @throw FaspError on error
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 stderr_r
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
@@ -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
@@ -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
- # to avoid infinite loop in pub link redirection
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
- # various API scopes supported
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 StandardError => e
320
- raise e if exception
321
- Log.log.debug{"ignoring error: #{e}"}
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::SCOPE_USER, or Node::SCOPE_ADMIN, or nil (requires secret)
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::SCOPE_USER, package_info: nil)
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::SCOPE_ADMIN)
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
@@ -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
- # Separator between node.AK and user:all
20
- SCOPE_SEPARATOR = ':'
21
- SCOPE_NODE_PREFIX = 'node.'
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 :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_TYPES,
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 [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
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(SCOPE_SEPARATOR, 2)
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?(SCOPE_NODE_PREFIX)){"invalid scope: #{scope}"}
138
- return {access_key: items[0][SCOPE_NODE_PREFIX.length..-1], scope: items[1]}
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'] || SCOPE_USER
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
- # set ascp executable "location"
57
- def ascp_path=(v)
58
- Aspera.assert_type(v, String)
59
- Aspera.assert(!v.empty?){'ascp location cannot be empty: check your config file'}
60
- @ascp_location = v
61
- @ascp_path = nil
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 ascp_path
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 = if k.eql?(:transferd)
117
- Products::Transferd.transferd_path
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(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
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(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
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?(ascp_path)
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
- @ascp_path = nil
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
@@ -5,6 +5,7 @@ require 'aspera/uri_reader'
5
5
  require 'aspera/environment'
6
6
  require 'aspera/log'
7
7
  require 'aspera/assert'
8
+ require 'aspera/cli/error'
8
9
  require 'json'
9
10
  require 'base64'
10
11
  require 'zlib'