aspera-cli 4.20.0 → 4.21.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 (73) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +41 -3
  4. data/CONTRIBUTING.md +69 -142
  5. data/README.md +687 -461
  6. data/bin/ascli +5 -14
  7. data/bin/asession +3 -5
  8. data/examples/get_proto_file.rb +4 -3
  9. data/examples/proxy.pac +20 -20
  10. data/lib/aspera/agent/base.rb +2 -0
  11. data/lib/aspera/agent/connect.rb +20 -2
  12. data/lib/aspera/agent/{alpha.rb → desktop.rb} +12 -18
  13. data/lib/aspera/agent/direct.rb +30 -31
  14. data/lib/aspera/agent/node.rb +1 -11
  15. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +37 -51
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +13 -8
  18. data/lib/aspera/api/cos_node.rb +1 -1
  19. data/lib/aspera/api/node.rb +49 -32
  20. data/lib/aspera/ascp/installation.rb +98 -77
  21. data/lib/aspera/ascp/management.rb +27 -6
  22. data/lib/aspera/cli/extended_value.rb +9 -3
  23. data/lib/aspera/cli/formatter.rb +155 -154
  24. data/lib/aspera/cli/info.rb +2 -1
  25. data/lib/aspera/cli/main.rb +12 -0
  26. data/lib/aspera/cli/manager.rb +4 -4
  27. data/lib/aspera/cli/plugin.rb +2 -2
  28. data/lib/aspera/cli/plugins/aoc.rb +134 -73
  29. data/lib/aspera/cli/plugins/config.rb +114 -83
  30. data/lib/aspera/cli/plugins/cos.rb +1 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +4 -2
  32. data/lib/aspera/cli/plugins/faspex5.rb +29 -14
  33. data/lib/aspera/cli/plugins/node.rb +51 -41
  34. data/lib/aspera/cli/transfer_progress.rb +2 -0
  35. data/lib/aspera/cli/version.rb +1 -1
  36. data/lib/aspera/command_line_builder.rb +1 -1
  37. data/lib/aspera/coverage.rb +5 -3
  38. data/lib/aspera/environment.rb +59 -16
  39. data/lib/aspera/faspex_postproc.rb +3 -5
  40. data/lib/aspera/hash_ext.rb +2 -12
  41. data/lib/aspera/node_simulator.rb +230 -112
  42. data/lib/aspera/oauth/base.rb +40 -48
  43. data/lib/aspera/oauth/factory.rb +41 -2
  44. data/lib/aspera/oauth/jwt.rb +4 -1
  45. data/lib/aspera/persistency_action_once.rb +1 -1
  46. data/lib/aspera/persistency_folder.rb +20 -2
  47. data/lib/aspera/preview/generator.rb +13 -10
  48. data/lib/aspera/preview/options.rb +2 -2
  49. data/lib/aspera/preview/terminal.rb +1 -1
  50. data/lib/aspera/preview/utils.rb +11 -6
  51. data/lib/aspera/products/connect.rb +82 -0
  52. data/lib/aspera/products/desktop.rb +30 -0
  53. data/lib/aspera/products/other.rb +82 -0
  54. data/lib/aspera/products/transferd.rb +61 -0
  55. data/lib/aspera/rest.rb +22 -17
  56. data/lib/aspera/secret_hider.rb +9 -2
  57. data/lib/aspera/ssh.rb +31 -24
  58. data/lib/aspera/temp_file_manager.rb +5 -4
  59. data/lib/aspera/transfer/parameters.rb +2 -1
  60. data/lib/aspera/transfer/spec.yaml +22 -20
  61. data/lib/aspera/transfer/sync.rb +1 -5
  62. data/lib/aspera/transfer/uri.rb +2 -2
  63. data/lib/aspera/uri_reader.rb +18 -1
  64. data/lib/transferd_pb.rb +86 -0
  65. data/lib/transferd_services_pb.rb +84 -0
  66. data.tar.gz.sig +0 -0
  67. metadata +13 -166
  68. metadata.gz.sig +0 -0
  69. data/examples/build_exec +0 -74
  70. data/examples/build_exec_rubyc +0 -40
  71. data/lib/aspera/ascp/products.rb +0 -168
  72. data/lib/transfer_pb.rb +0 -84
  73. data/lib/transfer_services_pb.rb +0 -82
@@ -127,10 +127,13 @@ module Aspera
127
127
  # commands for execute_command_gen4
128
128
  COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
129
129
 
130
+ # commands supported in ATS for COS
130
131
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
131
132
  COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
132
133
  COMMANDS_FASPEX = COMMON_ACTIONS
133
134
 
135
+ GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
136
+
134
137
  def initialize(api: nil, **env)
135
138
  super(**env, basic_options: api.nil?)
136
139
  Node.declare_options(options) if api.nil?
@@ -474,7 +477,7 @@ module Aspera
474
477
  result[:password] = apifid[:api].auth_params[:password]
475
478
  when :oauth2
476
479
  result[:username] = apifid[:api].params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
477
- result[:password] = apifid[:api].oauth.token
480
+ result[:password] = apifid[:api].oauth.authorization
478
481
  else Aspera.error_unreachable_line
479
482
  end
480
483
  return {type: :single_object, data: result} if command_repo.eql?(:node_info)
@@ -484,22 +487,15 @@ module Aspera
484
487
  when :browse
485
488
  apifid = apifid_from_next_arg(top_file_id)
486
489
  file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
487
- if file_info['type'].eql?('folder')
488
- result = apifid[:api].call(
489
- operation: 'GET',
490
- subpath: "files/#{apifid[:file_id]}/files",
491
- headers: Api::Node.cache_control_headers,
492
- query: query_read_delete)
493
- items = result[:data]
494
- formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
495
- else
496
- items = [file_info]
490
+ unless file_info['type'].eql?('folder')
491
+ # a single file
492
+ return {type: :object_list, data: [file_info], fields: GEN4_LS_FIELDS}
497
493
  end
498
- return {type: :object_list, data: items, fields: %w[name type recursive_size size modified_time access_level]}
494
+ return {type: :object_list, data: apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS}
499
495
  when :find
500
496
  apifid = apifid_from_next_arg(top_file_id)
501
- test_block = Api::Node.file_matcher_from_argument(options)
502
- return {type: :object_list, data: @api_node.find_files(apifid[:file_id], test_block), fields: ['path']}
497
+ find_lambda = Api::Node.file_matcher_from_argument(options)
498
+ return {type: :object_list, data: @api_node.find_files(apifid[:file_id], find_lambda), fields: ['path']}
503
499
  when :mkdir
504
500
  containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
505
501
  new_folder = containing_folder_path.pop
@@ -605,16 +601,19 @@ module Aspera
605
601
  return Main.result_image(result[:http].body, formatter: formatter)
606
602
  when :permission
607
603
  apifid = apifid_from_next_arg(top_file_id)
608
- command_perm = options.get_next_command(%i[list create delete])
604
+ command_perm = options.get_next_command(%i[list show create delete])
609
605
  case command_perm
610
606
  when :list
611
- # generic options : TODO: as arg ? query_read_delete
612
- list_options ||= {'include' => Rest.array_params(%w[access_level permission_count])}
613
- # add which one to get
614
- list_options['file_id'] = apifid[:file_id]
615
- list_options['inherited'] ||= false
616
- items = apifid[:api].read('permissions', list_options)
607
+ list_query = query_read_delete(default: {'include' => Rest.array_params(%w[access_level permission_count])})
608
+ # specify file to get permissions for unless not specified
609
+ list_query['file_id'] = apifid[:file_id] unless apifid[:file_id].to_s.empty?
610
+ list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
611
+ # NOTE: supports per_page and page and header X-Total-Count
612
+ items = apifid[:api].read('permissions', list_query)
617
613
  return {type: :object_list, data: items}
614
+ when :show
615
+ perm_id = instance_identifier
616
+ return Main.result_single_object(apifid[:api].read("permissions/#{perm_id}"))
618
617
  when :delete
619
618
  return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
620
619
  apifid[:api].delete("permissions/#{one_id}")
@@ -802,42 +801,53 @@ module Aspera
802
801
  end
803
802
  case command
804
803
  when :list
804
+ transfer_filter = query_read_delete(default: {})
805
+ last_iteration_token = nil
805
806
  iteration_persistency = nil
806
- iteration_data = []
807
807
  if options.get_option(:once_only, mandatory: true)
808
808
  iteration_persistency = PersistencyActionOnce.new(
809
809
  manager: persistency,
810
- data: iteration_data,
810
+ data: [],
811
811
  id: IdGenerator.from_list([
812
812
  'node_transfers',
813
813
  options.get_option(:url, mandatory: true),
814
814
  options.get_option(:username, mandatory: true)
815
815
  ]))
816
+ if transfer_filter.delete('reset')
817
+ iteration_persistency.data.clear
818
+ iteration_persistency.save
819
+ return Main.result_status('Persistency reset')
820
+ end
821
+ last_iteration_token = iteration_persistency.data.first
816
822
  end
817
- transfer_filter = query_read_delete(default: {})
818
- if transfer_filter.delete('reset')
819
- iteration_data.clear
820
- iteration_persistency&.save
821
- return Main.result_status('Persistency reset')
822
- end
823
+ raise 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
823
824
  max_items = transfer_filter.delete(MAX_ITEMS)
824
- transfer_filter['iteration_token'] = iteration_persistency.data[0] unless iteration_data.empty?
825
825
  transfers_data = []
826
826
  loop do
827
+ transfer_filter['iteration_token'] = last_iteration_token unless last_iteration_token.nil?
827
828
  result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
828
- data = result[:data]
829
- transfers_data.concat(data)
830
- if !max_items.nil? && (transfers_data.length >= max_items)
829
+ # no data
830
+ break if result[:data].empty?
831
+ # get next iteration token from link
832
+ next_iteration_token = nil
833
+ link_info = result[:http]['Link']
834
+ unless link_info.nil?
835
+ m = link_info.match(/<([^>]+)>/)
836
+ raise "Cannot parse iteration in Link: #{link_info}" if m.nil?
837
+ next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
838
+ end
839
+ # same as last iteration: stop
840
+ break if next_iteration_token&.eql?(last_iteration_token)
841
+ last_iteration_token = next_iteration_token
842
+ transfers_data.concat(result[:data])
843
+ if max_items&.<=(transfers_data.length)
844
+ # if !max_items.nil? && (transfers_data.length >= max_items)
831
845
  transfers_data = transfers_data.slice(0, max_items)
832
846
  break
833
847
  end
834
- link_info = result[:http]['Link']
835
- break if iteration_persistency.nil? || data.empty? || link_info.nil?
836
- m = link_info.match(/<([^>]+)>/)
837
- raise "Problem with iteration: #{link_info}" if m.nil?
838
- iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
839
- iteration_data[0] = transfer_filter['iteration_token'] = iteration_token
848
+ break if last_iteration_token.nil?
840
849
  end
850
+ iteration_persistency&.data&.[]=(0, last_iteration_token)
841
851
  iteration_persistency&.save
842
852
  return {
843
853
  type: :object_list,
@@ -1006,7 +1016,7 @@ module Aspera
1006
1016
  Environment.instance.open_uri("#{options.get_option(:asperabrowserurl)}?goto=#{encoded_params}")
1007
1017
  return Main.result_status('done')
1008
1018
  when :basic_token
1009
- return Main.result_status(Rest.basic_token(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
1019
+ return Main.result_status(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
1010
1020
  when :bearer_token
1011
1021
  private_key = OpenSSL::PKey::RSA.new(options.get_next_argument('private RSA key PEM value', validation: String))
1012
1022
  token_info = options.get_next_argument('user and group identification', validation: Hash)
@@ -1019,7 +1029,7 @@ module Aspera
1019
1029
  raise 'Missing key: url' unless parameters.key?(:url)
1020
1030
  uri = URI.parse(parameters[:url])
1021
1031
  server = WebServerSimple.new(uri, certificate: parameters[:certificate])
1022
- server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], transfer)
1032
+ server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], NodeSimulator.new)
1023
1033
  server.start
1024
1034
  return Main.result_status('Simulator terminated')
1025
1035
  end
@@ -79,6 +79,8 @@ module Aspera
79
79
  new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.length}] #{@title}"
80
80
  @progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
81
81
  @progress_bar.increment if !progress_provided && !@completed
82
+ rescue ProgressBar::InvalidProgressError => e
83
+ Log.log.error{"Progress error: #{e}"}
82
84
  end
83
85
 
84
86
  private
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # for beta add extension : .beta1
6
6
  # for dev version add extension : .pre
7
- VERSION = '4.20.0'
7
+ VERSION = '4.21.2'
8
8
  end
9
9
  end
@@ -77,7 +77,7 @@ module Aspera
77
77
  @param_hash.each_pair{|key, val|Log.log.warn{"unrecognized parameter: #{key} = \"#{val}\""} if !@used_param_names.include?(key)}
78
78
  # set result
79
79
  env_args[:env].merge!(@result[:env])
80
- env_args[:args].push(*@result[:args])
80
+ env_args[:args].concat(@result[:args])
81
81
  return nil
82
82
  end
83
83
 
@@ -20,14 +20,16 @@ if ENV.key?('ENABLE_COVERAGE')
20
20
  # lines with those words are ignored from coverage
21
21
  no_cov_functions = %w[error_unreachable_line error_unexpected_value Log.log.trace].freeze
22
22
  SimpleCov.start do
23
- add_filter 'lib/aspera/cli/plugins/faspex.rb'
24
- add_filter 'lib/aspera/node_simulator.rb'
25
- add_filter 'lib/aspera/keychain/macos_security.rb'
23
+ # assert usually do not trigger
26
24
  add_filter do |source_file|
27
25
  source_file.lines.each do |line|
28
26
  line.skipped! if no_cov_functions.any?{|i|line.src.include?(i)}
29
27
  end
30
28
  false
31
29
  end
30
+ # no coverage test in those
31
+ add_filter 'lib/aspera/cli/plugins/faspex.rb'
32
+ add_filter 'lib/aspera/node_simulator.rb'
33
+ add_filter 'lib/aspera/keychain/macos_security.rb'
32
34
  end
33
35
  end
@@ -46,8 +46,7 @@ module Aspera
46
46
  return OS_LINUX
47
47
  when /aix/
48
48
  return OS_AIX
49
- else
50
- raise "Unknown OS: #{RbConfig::CONFIG['host_os']}"
49
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
51
50
  end
52
51
  end
53
52
 
@@ -62,8 +61,8 @@ module Aspera
62
61
  return CPU_S390
63
62
  when /arm/, /aarch64/
64
63
  return CPU_ARM64
64
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
65
65
  end
66
- raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
67
66
  end
68
67
 
69
68
  # normalized architecture name
@@ -73,9 +72,9 @@ module Aspera
73
72
  end
74
73
 
75
74
  # executable file extension for current OS
76
- def exe_extension
77
- return '.exe' if os.eql?(OS_WINDOWS)
78
- return ''
75
+ def exe_file(name='')
76
+ return "#{name}.exe" if os.eql?(OS_WINDOWS)
77
+ return name
79
78
  end
80
79
 
81
80
  # on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
@@ -96,34 +95,78 @@ module Aspera
96
95
  Kernel.send('lave'.reverse, code, empty_binding, file, line)
97
96
  end
98
97
 
99
- def log_spawn(env:, exec:, args:)
98
+ # Generate log line for external program with arguments
99
+ # @param env [Hash, nil] environment variables
100
+ # @param exec [String] path to executable
101
+ # @param args [Array, nil] arguments
102
+ # @return [String] log line with environment, program and arguments
103
+ def log_spawn(exec:, args: nil, env: nil)
100
104
  [
101
105
  'execute:'.red,
102
- env.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
106
+ env&.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
103
107
  Shellwords.shellescape(exec),
104
- args.map{|a|Shellwords.shellescape(a)}
105
- ].flatten.join(' ')
108
+ args&.map{|a|Shellwords.shellescape(a)}
109
+ ].compact.flatten.join(' ')
106
110
  end
107
111
 
108
- # start process in background, or raise exception
112
+ # Start process in background
109
113
  # caller can call Process.wait on returned value
110
- def secure_spawn(exec:, args: [], env: [])
111
- Log.log.debug {log_spawn(env: env, exec: exec, args: args)}
114
+ # @param exec [String] path to executable
115
+ # @param args [Array, nil] arguments for executable
116
+ # @param env [Hash, nil] environment variables
117
+ # @param options [Hash, nil] spawn options
118
+ # @return [String] PID of process
119
+ # @raise [Exception] if problem
120
+ def secure_spawn(exec:, args: nil, env: nil, **options)
121
+ Aspera.assert_type(exec, String)
122
+ Aspera.assert_type(args, Array) unless args.nil?
123
+ Aspera.assert_type(env, Hash) unless env.nil?
124
+ Aspera.assert_type(options, Hash) unless options.nil?
125
+ Log.log.debug {log_spawn(exec: exec, args: args, env: env)}
112
126
  # start ascp in separate process
113
- ascp_pid = Process.spawn(env, [exec, exec], *args, close_others: true)
127
+ spawn_args = []
128
+ spawn_args.push(env) unless env.nil?
129
+ spawn_args.push([exec, exec])
130
+ spawn_args.concat(args) unless args.nil?
131
+ opts = {close_others: true}
132
+ opts.merge!(options) unless options.nil?
133
+ ascp_pid = Process.spawn(*spawn_args, **opts)
114
134
  Log.log.debug{"pid: #{ascp_pid}"}
115
135
  return ascp_pid
116
136
  end
117
137
 
138
+ # start process and wait for completion
139
+ # @param env [Hash, nil] environment variables
140
+ # @param exec [String] path to executable
141
+ # @param args [Array, nil] arguments
142
+ # @return [String] PID of process
143
+ def secure_execute(exec:, args: nil, env: nil, **system_args)
144
+ Aspera.assert_type(exec, String)
145
+ Aspera.assert_type(args, Array) unless args.nil?
146
+ Aspera.assert_type(env, Hash) unless env.nil?
147
+ Log.log.debug {log_spawn(exec: exec, args: args, env: env)}
148
+ # start in separate process
149
+ spawn_args = []
150
+ spawn_args.push(env) unless env.nil?
151
+ # ensure no shell expansion
152
+ spawn_args.push([exec, exec])
153
+ spawn_args.concat(args) unless args.nil?
154
+ kwargs = {exception: true}
155
+ kwargs.merge!(system_args)
156
+ Kernel.system(*spawn_args, **kwargs)
157
+ nil
158
+ end
159
+
160
+ # Execute process and capture stdout
118
161
  # @param exec [String] path to executable
119
162
  # @param args [Array] arguments to executable
120
163
  # @param opts [Hash] options to capture3
121
- # @return stdout of executable or raise expcetion
164
+ # @return stdout of executable or raise exception
122
165
  def secure_capture(exec:, args: [], **opts)
123
166
  Aspera.assert_type(exec, String)
124
167
  Aspera.assert_type(args, Array)
125
168
  Aspera.assert_type(opts, Hash)
126
- Log.log.debug {log_spawn(env: {}, exec: exec, args: args)}
169
+ Log.log.debug {log_spawn(exec: exec, args: args)}
127
170
  stdout, stderr, status = Open3.capture3(exec, *args, **opts)
128
171
  Log.log.debug{"status=#{status}, stderr=#{stderr}"}
129
172
  Log.log.trace1{"stdout=#{stdout}"}
@@ -39,7 +39,7 @@ module Aspera
39
39
  response.body = {status: 'error', message: 'Empty request'}.to_json
40
40
  return
41
41
  end
42
- # build script path by removing domain, and adding script folder
42
+ # build script path by removing domain and adding script folder
43
43
  script_file = request.path[@parameters[:root].size..]
44
44
  Log.log.debug{"script file=#{script_file}"}
45
45
  script_path = File.join(@parameters[:script_folder], script_file)
@@ -48,11 +48,9 @@ module Aspera
48
48
  Log.log.debug{Log.dump(:webhook_parameters, webhook_parameters)}
49
49
  # env expects only strings
50
50
  environment = webhook_parameters.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
51
- post_proc_pid = Process.spawn(environment, [script_path, script_path])
52
- Log.log.debug{"pid=#{post_proc_pid}"}
53
- raise 'no pid' if post_proc_pid.nil?
54
- # "wait" for process to avoid zombie
51
+ post_proc_pid = Environment.secure_spawn(env: environment, exec: script_path)
55
52
  Timeout.timeout(@parameters[:timeout_seconds]) do
53
+ # "wait" for process to avoid zombie
56
54
  Process.wait(post_proc_pid)
57
55
  post_proc_pid = nil
58
56
  end
@@ -20,17 +20,7 @@ class ::Hash
20
20
  end
21
21
  end
22
22
 
23
- # in 2.5
24
- unless Hash.method_defined?(:transform_keys)
25
- class Hash
26
- def transform_keys
27
- raise 'missing block' unless block_given?
28
- return each_with_object({}){|(k, v), memo|memo[yield(k)] = v}
29
- end
30
- end
31
- end
32
-
33
- # rails
23
+ # Exists in Rails
34
24
  unless Hash.method_defined?(:symbolize_keys)
35
25
  class Hash
36
26
  def symbolize_keys
@@ -39,7 +29,7 @@ unless Hash.method_defined?(:symbolize_keys)
39
29
  end
40
30
  end
41
31
 
42
- # rails
32
+ # Exists in Rails
43
33
  unless Hash.method_defined?(:stringify_keys)
44
34
  class Hash
45
35
  def stringify_keys