aspera-cli 4.18.1 → 4.20.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -5,16 +5,16 @@ require 'aspera/environment'
5
5
  require 'aspera/data_repository'
6
6
  require 'aspera/ascp/products'
7
7
  require 'aspera/log'
8
+ require 'aspera/rest'
8
9
  require 'aspera/assert'
9
10
  require 'aspera/web_server_simple'
10
11
  require 'English'
11
12
  require 'singleton'
12
13
  require 'xmlsimple'
13
- require 'zlib'
14
14
  require 'base64'
15
15
  require 'fileutils'
16
16
  require 'openssl'
17
-
17
+ require 'yaml'
18
18
  module Aspera
19
19
  module Ascp
20
20
  # Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
@@ -28,7 +28,7 @@ module Aspera
28
28
  include Singleton
29
29
  # protobuf generated files from sdk
30
30
  EXT_RUBY_PROTOBUF = '_pb.rb'
31
- RB_SDK_FOLDER = 'lib'
31
+ RB_SDK_SUBFOLDER = 'lib'
32
32
  DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
33
33
  <?xml version='1.0' encoding='UTF-8'?>
34
34
  <CONF version="2">
@@ -41,8 +41,15 @@ module Aspera
41
41
  </CONF>
42
42
  END_OF_CONFIG_FILE
43
43
  # all ascp files (in SDK)
44
- FILES = %i[ascp ascp4 transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].freeze
45
- private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_FOLDER, :DEFAULT_ASPERA_CONF, :FILES
44
+ EXE_FILES = %i[ascp ascp4 async].freeze
45
+ FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
46
+ TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
47
+ FILE_SCHEME_PREFIX = 'file:///'
48
+ SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
49
+ private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
50
+ # options for SSH client private key
51
+ CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
52
+
46
53
  # set ascp executable path
47
54
  def ascp_path=(v)
48
55
  @path_to_ascp = v
@@ -52,12 +59,6 @@ module Aspera
52
59
  path(:ascp)
53
60
  end
54
61
 
55
- def sdk_ruby_folder
56
- ruby_pb_folder = File.join(sdk_folder, RB_SDK_FOLDER)
57
- FileUtils.mkdir_p(ruby_pb_folder)
58
- return ruby_pb_folder
59
- end
60
-
61
62
  # location of SDK files
62
63
  def sdk_folder=(v)
63
64
  Log.log.debug{"sdk_folder=#{v}"}
@@ -80,7 +81,7 @@ module Aspera
80
81
  def use_ascp_from_product(product_name)
81
82
  if product_name.eql?(FIRST_FOUND)
82
83
  pl = Products.installed_products.first
83
- raise "no FASP installation found\nPlease check manual on how to install FASP." if pl.nil?
84
+ raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
84
85
  else
85
86
  pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
86
87
  raise "no such product installed: #{product_name}" if pl.nil?
@@ -110,14 +111,14 @@ module Aspera
110
111
  def path(k)
111
112
  file_is_optional = false
112
113
  case k
113
- when :ascp, :ascp4
114
+ when *EXE_FILES
115
+ file_is_optional = k.eql?(:async)
114
116
  use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
115
- file = @path_to_ascp
116
117
  # NOTE: that there might be a .exe at the end
117
- file = file.gsub('ascp', 'ascp4') if k.eql?(:ascp4)
118
+ file = @path_to_ascp.gsub('ascp', k.to_s)
118
119
  when :transferd
119
- file = transferd_filepath
120
120
  file_is_optional = true
121
+ file = transferd_filepath
121
122
  when :ssh_private_dsa, :ssh_private_rsa
122
123
  # assume last 3 letters are type
123
124
  type = k.to_s[-3..-1].to_sym
@@ -151,8 +152,16 @@ module Aspera
151
152
  return DataRepository.instance.item(:uuid)
152
153
  end
153
154
 
154
- def aspera_token_ssh_key_paths
155
- return %i[ssh_private_dsa ssh_private_rsa].map{|i|Installation.instance.path(i)}
155
+ # get paths of SSH keys to use for ascp client
156
+ # @param types [Symbol] types to use
157
+ def aspera_token_ssh_key_paths(types)
158
+ Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
159
+ return case types
160
+ when :dsa_rsa, :rsa
161
+ types.to_s.split('_').map{|i|Installation.instance.path("ssh_private_#{i}".to_sym)}
162
+ when :per_client
163
+ raise 'Not yet implemented'
164
+ end
156
165
  end
157
166
 
158
167
  # use in plugin `config`
@@ -166,16 +175,17 @@ module Aspera
166
175
  return nil unless File.exist?(exe_path)
167
176
  exe_version = nil
168
177
  cmd_out = %x("#{exe_path}" #{vers_arg})
169
- raise "An error occurred when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
178
+ raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
170
179
  # get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
171
180
  m = cmd_out.match(/ version ([0-9.]+)/)
172
- exe_version = m[1] unless m.nil?
181
+ exe_version = m[1].gsub(/\.$/, '') unless m.nil?
173
182
  return exe_version
174
183
  end
175
184
 
176
- def ascp_add_pvcl(data)
185
+ def ascp_pvcl_info
186
+ data = {}
177
187
  # read PATHs from ascp directly, and pvcl modules as well
178
- Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
188
+ Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
179
189
  last_line = ''
180
190
  while (line = stderr.gets)
181
191
  line.chomp!
@@ -194,88 +204,150 @@ module Aspera
194
204
  data['product_name'] = Regexp.last_match(1)
195
205
  data['product_version'] = Regexp.last_match(2)
196
206
  when /^LOG Initializing FASP version ([^,]+),/
197
- data['ascp_version'] = Regexp.last_match(1)
207
+ data['sdk_ascp_version'] = Regexp.last_match(1)
198
208
  end
199
209
  end
200
210
  if !thread.value.exitstatus.eql?(1) && !data.key?('root')
201
211
  raise last_line
202
212
  end
203
213
  end
214
+ return data
204
215
  end
205
216
 
206
217
  # extract some stings from ascp binary
207
- def ascp_add_openssl(data)
208
- ascp_file = data['ascp']
209
- File.binread(ascp_file).scan(/[\x20-\x7E]{10,}/) do |bin_string|
218
+ def ascp_ssl_info
219
+ data = {}
220
+ File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
210
221
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
211
222
  data['openssldir'] = m[1]
212
223
  elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
213
224
  data['openssl_version'] = m[1]
214
225
  end
215
- end if File.file?(ascp_file)
226
+ end if File.file?(ascp_path)
227
+ return data
216
228
  end
217
229
 
230
+ # information for `ascp info`
218
231
  def ascp_info
219
- data = file_paths
220
- ascp_add_pvcl(data)
221
- ascp_add_openssl(data)
222
- return data
232
+ ascp_data = file_paths
233
+ ascp_data.merge!(ascp_pvcl_info)
234
+ ascp_data['sdk_locations'] = TRANSFER_SDK_LOCATION_URL
235
+ ascp_data.merge!(ascp_ssl_info)
236
+ return ascp_data
237
+ end
238
+
239
+ # Loads YAML from cloud with locations of SDK archives for all platforms
240
+ # @return location structure
241
+ def sdk_locations
242
+ yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
243
+ YAML.load(yaml_text)
244
+ end
245
+
246
+ # @return the url for download of SDK archive for the given platform and version
247
+ def sdk_url_for_platform(platform: nil, version: nil)
248
+ locations = sdk_locations
249
+ platform = Environment.architecture if platform.nil?
250
+ locations = locations.select{|l|l['platform'].eql?(platform)}
251
+ raise "No SDK for platform: #{platform}" if locations.empty?
252
+ version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
253
+ info = locations.select{|entry| entry['version'].eql?(version)}
254
+ raise "No such version: #{version} for #{platform}" if info.empty?
255
+ return info.first['url']
256
+ end
257
+
258
+ def extract_archive_files(sdk_archive_path)
259
+ raise 'missing block' unless block_given?
260
+ case sdk_archive_path
261
+ # Windows and Mac use zip
262
+ when /\.zip$/
263
+ require 'zip'
264
+ # extract files from archive
265
+ Zip::File.open(sdk_archive_path) do |zip_file|
266
+ zip_file.each do |entry|
267
+ next if entry.name.end_with?('/')
268
+ yield(entry.name, entry.get_input_stream)
269
+ end
270
+ end
271
+ # Other Unixes use tar.gz
272
+ when /\.tar\.gz/
273
+ require 'zlib'
274
+ require 'rubygems/package'
275
+ Zlib::GzipReader.open(sdk_archive_path) do |gzip|
276
+ Gem::Package::TarReader.new(gzip) do |tar|
277
+ tar.each do |entry|
278
+ next if entry.directory?
279
+ yield(entry.full_name, entry)
280
+ end
281
+ end
282
+ end
283
+ else
284
+ raise "unknown archive extension: #{sdk_archive_path}"
285
+ end
223
286
  end
224
287
 
225
288
  # download aspera SDK or use local file
226
289
  # extracts ascp binary for current system architecture
290
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
227
291
  # @return ascp version (from execution)
228
- def install_sdk(sdk_url)
229
- # SDK is organized by architecture, check this first, in case architecture is not supported
230
- arch_filter = "#{Environment.architecture}/"
231
- require 'zip'
232
- sdk_zip_path = File.join(Dir.tmpdir, 'sdk.zip')
233
- if sdk_url.start_with?('file:')
292
+ def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
293
+ url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
294
+ folder = sdk_folder if folder.nil?
295
+ subfolder_lambda = block
296
+ if subfolder_lambda.nil?
297
+ subfolder_lambda = ->(name) do
298
+ if SDK_ARCHIVE_FOLDERS.any?{|i|name.include?(i)}
299
+ '/'
300
+ elsif name.end_with?(EXT_RUBY_PROTOBUF)
301
+ RB_SDK_SUBFOLDER
302
+ end
303
+ end
304
+ end
305
+ if url.start_with?('file:')
234
306
  # require specific file scheme: the path part is "relative", or absolute if there are 4 slash
235
- raise 'use format: file:///<path>' unless sdk_url.start_with?('file:///')
236
- sdk_zip_path = sdk_url.gsub(%r{^file:///}, '')
307
+ raise 'use format: file:///<path>' unless url.start_with?(FILE_SCHEME_PREFIX)
308
+ sdk_archive_path = url[FILE_SCHEME_PREFIX.length..-1]
309
+ delete_archive = false
237
310
  else
238
- Aspera::Rest.new(base_url: sdk_url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_zip_path)
311
+ sdk_archive_path = File.join(Dir.tmpdir, File.basename(url))
312
+ Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
313
+ delete_archive = true
239
314
  end
240
315
  # rename old install
241
- if !Dir.empty?(sdk_folder)
316
+ if backup && !Dir.empty?(folder)
242
317
  Log.log.warn('Previous install exists, renaming folder.')
243
- File.rename(sdk_folder, "#{sdk_folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
318
+ File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
244
319
  # TODO: delete old archives ?
245
320
  end
246
- # extract files from archive
247
- Zip::File.open(sdk_zip_path) do |zip_file|
248
- zip_file.each do |entry|
249
- # skip folder entries
250
- next if entry.name.end_with?('/')
251
- dest_folder = nil
252
- # binaries
253
- dest_folder = sdk_folder if entry.name.include?(arch_filter)
254
- # ruby adapters
255
- dest_folder = sdk_ruby_folder if entry.name.end_with?(EXT_RUBY_PROTOBUF)
256
- next if dest_folder.nil?
257
- File.open(File.join(dest_folder, File.basename(entry.name)), 'wb') do |output_stream|
258
- IO.copy_stream(entry.get_input_stream, output_stream)
259
- end
321
+ extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
322
+ subfolder = subfolder_lambda.call(entry_name)
323
+ next if subfolder.nil?
324
+ dest_folder = File.join(folder, subfolder)
325
+ FileUtils.mkdir_p(dest_folder)
326
+ File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
327
+ IO.copy_stream(entry_stream, output_stream)
260
328
  end
261
329
  end
262
- File.unlink(sdk_zip_path) rescue nil # Windows may give error
330
+ File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
331
+ return unless with_exe
263
332
  # ensure license file are generated so that ascp invocation for version works
264
333
  path(:aspera_license)
265
334
  path(:aspera_conf)
266
- ascp_file = Products.ascp_filename
267
- ascp_path = File.join(sdk_folder, ascp_file)
268
- raise "No #{ascp_file} found in SDK archive" unless File.exist?(ascp_path)
269
- Environment.restrict_file_access(ascp_path, mode: 0o755)
270
- Environment.restrict_file_access(ascp_path.gsub('ascp', 'ascp4'), mode: 0o755)
271
- ascp_version = get_ascp_version(ascp_path)
272
- trd_path = transferd_filepath
273
- Log.log.warn{"No #{trd_path} in SDK archive"} unless File.exist?(trd_path)
274
- Environment.restrict_file_access(trd_path, mode: 0o755) if File.exist?(trd_path)
275
- transferd_version = get_exe_version(trd_path, 'version')
276
- sdk_version = transferd_version || ascp_version
277
- File.write(File.join(sdk_folder, Products::INFO_META_FILE), "<product><name>IBM Aspera SDK</name><version>#{sdk_version}</version></product>")
278
- return sdk_version
335
+ sdk_ascp_file = Products.ascp_filename
336
+ sdk_ascp_path = File.join(folder, sdk_ascp_file)
337
+ raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
338
+ EXE_FILES.each do |exe_sym|
339
+ exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
340
+ Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
341
+ end
342
+ sdk_ascp_version = get_ascp_version(sdk_ascp_path)
343
+ sdk_daemon_path = transferd_filepath
344
+ Log.log.warn{"No #{sdk_daemon_path} in SDK archive"} unless File.exist?(sdk_daemon_path)
345
+ Environment.restrict_file_access(sdk_daemon_path, mode: 0o755) if File.exist?(sdk_daemon_path)
346
+ transferd_version = get_exe_version(sdk_daemon_path, 'version')
347
+ sdk_name = 'IBM Aspera Transfer SDK'
348
+ sdk_version = transferd_version || sdk_ascp_version
349
+ File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
350
+ return sdk_name, sdk_version
279
351
  end
280
352
 
281
353
  private
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/assert'
4
+
3
5
  module Aspera
4
6
  module Ascp
5
7
  # processing of ascp management port events
@@ -188,7 +190,7 @@ module Aspera
188
190
  # empty line is separator to end event information
189
191
  MGT_FRAME_SEPARATOR = ''
190
192
  # fields description for JSON generation
191
- # spellchecker: disable
193
+ # cspell: disable
192
194
  INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
193
195
  DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
194
196
  ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
@@ -219,6 +221,9 @@ module Aspera
219
221
  end
220
222
  attr_reader :last_event
221
223
 
224
+ # process line of mgt port event
225
+ # @param line [String] line of mgt port event
226
+ # @return [Hash] event hash or nil if event is not yet complete
222
227
  def process_line(line)
223
228
  # Log.log.debug{"line=[#{line}]"}
224
229
  case line
@@ -234,8 +239,7 @@ module Aspera
234
239
  @last_event = @event_build
235
240
  @event_build = nil
236
241
  return @last_event
237
- else
238
- raise "mgt port: unexpected line: [#{line}]"
242
+ else Aspera.error_unexpected_value(line){'mgt port'}
239
243
  end
240
244
  return nil
241
245
  end
@@ -56,6 +56,18 @@ module Aspera
56
56
  log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
57
57
  run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
58
58
  sub_bin: File.join('Contents', 'Resources')
59
+ }, {
60
+ expected: CONNECT,
61
+ app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
62
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
63
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
64
+ sub_bin: File.join('Contents', 'Resources')
65
+ }, {
66
+ expected: CONNECT,
67
+ app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
68
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
69
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
70
+ sub_bin: File.join('Contents', 'Resources')
59
71
  }, {
60
72
  expected: CLI_V1,
61
73
  app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
@@ -126,7 +138,7 @@ module Aspera
126
138
 
127
139
  # filename for ascp with optional extension (Windows)
128
140
  def ascp_filename
129
- return 'ascp' + Environment.exe_extension
141
+ return "ascp#{Environment.exe_extension}"
130
142
  end
131
143
 
132
144
  # @return folder paths for specified applications
@@ -150,12 +162,6 @@ module Aspera
150
162
  end
151
163
  raise "no connect uri file found in #{folder}"
152
164
  end
153
-
154
- # @ return path to configuration file of aspera CLI
155
- # def cli_conf_file
156
- # connect = folders(PRODUCT_CLI_V1)
157
- # return File.join(connect[:app_root], BIN_SUBFOLDER, '.aspera_cli_conf')
158
- # end
159
165
  end
160
166
  end
161
167
  end
data/lib/aspera/assert.rb CHANGED
@@ -8,13 +8,13 @@ module Aspera
8
8
  end
9
9
  class << self
10
10
  # the block is executed in the context of the Aspera module
11
- def assert(assertion, info = nil, level: 2, exception_class: AssertError)
11
+ def assert(assertion, info = nil, exception_class: AssertError)
12
12
  raise InternalError, 'bad assert: both info and block given' unless info.nil? || !block_given?
13
13
  return if assertion
14
14
  message = 'assertion failed'
15
15
  info = yield if block_given?
16
16
  message = "#{message}: #{info}" if info
17
- message = "#{message}: #{caller(level..level).first}"
17
+ message = "#{message}: #{caller.find{|call|!call.start_with?(__FILE__)}}"
18
18
  raise exception_class, message
19
19
  end
20
20
 
@@ -22,13 +22,18 @@ module Aspera
22
22
  # @param value [Object] the value to check
23
23
  # @param type [Class] the expected type
24
24
  def assert_type(value, type, exception_class: AssertError)
25
- assert(value.is_a?(type), level: 3, exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
25
+ assert(value.is_a?(type), exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
26
26
  end
27
27
 
28
28
  # assert that value is one of the given values
29
+ # @param value value to check
30
+ # @param values accepted values
31
+ # @param exception_class exception in case of no match
29
32
  def assert_values(value, values, exception_class: AssertError)
30
- assert(values.include?(value), level: 3, exception_class: exception_class) do
31
- "#{block_given? ? "#{yield}: " : nil}expecting one of #{values.inspect}, but have #{value.inspect}"
33
+ assert(values.include?(value), exception_class: exception_class) do
34
+ val_list = values.inspect
35
+ val_list = "one of #{val_list}" if values.is_a?(Array)
36
+ "#{block_given? ? "#{yield}: " : nil}expecting #{val_list}, but have #{value.inspect}"
32
37
  end
33
38
  end
34
39
 
@@ -101,8 +101,9 @@ module Aspera
101
101
  DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
102
102
  # user output levels
103
103
  DISPLAY_LEVELS = %i[info data error].freeze
104
+ FIELD_VALUE_HEADINGS = %i[key value].freeze
104
105
 
105
- private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR
106
+ private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR, :FIELD_VALUE_HEADINGS
106
107
  # prefix to display error messages in user messages (terminal)
107
108
  ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
108
109
  WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
@@ -116,7 +117,7 @@ module Aspera
116
117
 
117
118
  def tick(yes)
118
119
  result =
119
- if Environment.terminal_supports_unicode?
120
+ if Environment.instance.terminal_supports_unicode?
120
121
  if yes
121
122
  "\u2713"
122
123
  else
@@ -150,16 +151,9 @@ module Aspera
150
151
  end
151
152
 
152
153
  # Highlight special values
153
- def special_format(what, use_colors: $stdout.isatty)
154
- result = $stdout.isatty ? "<#{what}>" : "&lt;#{what}&gt;"
155
- if use_colors
156
- result = if %w[null empty].any?{|s|what.include?(s)}
157
- result.dim
158
- else
159
- result.reverse_color
160
- end
161
- end
162
- return result
154
+ def special_format(what)
155
+ result = "<#{what}>"
156
+ return %w[null empty].any?{|s|what.include?(s)} ? result.dim : result.reverse_color
163
157
  end
164
158
 
165
159
  # call this after REST calls if several api calls are expected
@@ -173,6 +167,11 @@ module Aspera
173
167
  @spinner.spin
174
168
  end
175
169
 
170
+ def long_operation_terminated
171
+ @spinner&.stop
172
+ @spinner = nil
173
+ end
174
+
176
175
  # options are: format, output, display, fields, select, table_style, flat_hash, transpose_single
177
176
  def option_handler(option_symbol, operation, value=nil)
178
177
  Aspera.assert_values(operation, %i[set get])
@@ -198,12 +197,11 @@ module Aspera
198
197
  end
199
198
 
200
199
  def declare_options(options)
201
- default_table_style = if Environment.terminal_supports_unicode?
200
+ default_table_style = if Environment.instance.terminal_supports_unicode?
202
201
  {border: :unicode_round}
203
202
  else
204
203
  {}
205
204
  end
206
-
207
205
  options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
208
206
  options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
209
207
  options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
@@ -213,8 +211,9 @@ module Aspera
213
211
  default: SpecialValues::DEF)
214
212
  options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
215
213
  options.declare(:table_style, 'Table display style', types: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
216
- options.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
217
- options.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
214
+ options.declare(:flat_hash, '(Table) Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
215
+ options.declare(:transpose_single, '(Table) Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
216
+ options.declare(:multi_table, '(Table) Each element of a table are displayed as a table', values: :bool, handler: {o: self, m: :option_handler}, default: false)
218
217
  options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
219
218
  options.declare(:image, 'Options for image display', types: Hash, handler: {o: self, m: :option_handler}, default: {})
220
219
  end
@@ -326,37 +325,52 @@ module Aspera
326
325
  display_message(:info, special_format('empty')) if @options[:format].eql?(:table)
327
326
  return
328
327
  end
328
+ # if table has only one element, and only one field, display the value
329
329
  if object_array.length == 1 && fields.length == 1
330
330
  display_message(:data, object_array.first[fields.first])
331
331
  return
332
332
  end
333
+ single_transposed = @options[:transpose_single] && object_array.length == 1
333
334
  # Special case if only one row (it could be object_list or single_object)
334
- if @options[:transpose_single] && object_array.length == 1
335
- new_columns = %i[key value]
335
+ if single_transposed
336
336
  single = object_array.first
337
- object_array = fields.map { |i| new_columns.zip([i, single[i]]).to_h }
338
- fields = new_columns
337
+ object_array = fields.map { |i| FIELD_VALUE_HEADINGS.zip([i, single[i]]).to_h }
338
+ fields = FIELD_VALUE_HEADINGS
339
339
  end
340
340
  Log.log.debug{Log.dump(:object_array, object_array)}
341
341
  # convert data to string, and keep only display fields
342
342
  final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
343
+ # remove empty rows
344
+ final_table_rows.select!{|i| !(i.is_a?(Hash) && i.empty?)}
343
345
  # here : fields : list of column names
344
346
  case @options[:format]
345
347
  when :table
346
- # display the table !
347
- display_message(:data, Terminal::Table.new(
348
- headings: fields,
349
- rows: final_table_rows,
350
- style: @options[:table_style]&.symbolize_keys))
348
+ if @options[:multi_table] && !single_transposed
349
+ final_table_rows.each do |row|
350
+ Log.log.debug{Log.dump(:row, row)}
351
+ display_message(:data, Terminal::Table.new(
352
+ headings: FIELD_VALUE_HEADINGS,
353
+ rows: fields.zip(row),
354
+ style: @options[:table_style]&.symbolize_keys))
355
+ end
356
+ else
357
+ # display the table !
358
+ display_message(:data, Terminal::Table.new(
359
+ headings: fields,
360
+ rows: final_table_rows,
361
+ style: @options[:table_style]&.symbolize_keys))
362
+ end
351
363
  when :csv
352
364
  display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
365
+ else
366
+ raise "not expected: #{@options[:format]}"
353
367
  end
354
368
  end
355
369
 
356
370
  # @return text suitable to display an image from url
357
371
  def status_image(blob)
358
372
  begin
359
- raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::DEFAULT_PARSER.make_regexp}\z/)
373
+ raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::RFC2396_PARSER.make_regexp}\z/)
360
374
  # it's a url
361
375
  url = blob
362
376
  unless Environment.instance.url_method.eql?(:text)
@@ -452,6 +466,8 @@ module Aspera
452
466
  else
453
467
  raise "unknown data type: #{type}"
454
468
  end
469
+ else
470
+ raise "not expected: #{@options[:format]}"
455
471
  end
456
472
  end
457
473
  end
@@ -4,6 +4,7 @@ require 'aspera/transfer/error'
4
4
  require 'aspera/rest'
5
5
  require 'aspera/log'
6
6
  require 'aspera/assert'
7
+ require 'aspera/cli/info'
7
8
  require 'net/ssh'
8
9
  require 'openssl'
9
10
 
@@ -18,7 +19,7 @@ module Aspera
18
19
  match: 'Remote host is not who we expected',
19
20
  remediation: [
20
21
  'For this specific error, refer to:',
21
- "#{SRC_URL}#error-remote-host-is-not-who-we-expected",
22
+ "#{Info::SRC_URL}#error-remote-host-is-not-who-we-expected",
22
23
  'Add this to arguments:',
23
24
  %q{--ts=@json:'{"sshfp":null}'"}
24
25
  ]
@@ -2,15 +2,17 @@
2
2
 
3
3
  module Aspera
4
4
  module Cli
5
- # name of command line tool, also used as foldername where config is stored
6
- PROGRAM_NAME = 'ascli'
7
- # name of the containing gem, same as in <gem name>.gemspec
8
- GEM_NAME = 'aspera-cli'
9
- DOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
10
- GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
11
- SRC_URL = 'https://github.com/IBM/aspera-cli'
12
- # set this to warn in advance when minimum required ruby version will increase
13
- # see also required_ruby_version in gemspec file
14
- RUBY_FUTURE_MINIMUM_VERSION = '3.0'
5
+ module Info
6
+ # name of command line tool, also used as foldername where config is stored
7
+ CMD_NAME = 'ascli'
8
+ # name of the containing gem, same as in <gem name>.gemspec
9
+ GEM_NAME = 'aspera-cli'
10
+ DOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
11
+ GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
12
+ SRC_URL = 'https://github.com/IBM/aspera-cli'
13
+ # set this to warn in advance when minimum required ruby version will increase
14
+ # see also required_ruby_version in gemspec file
15
+ RUBY_FUTURE_MINIMUM_VERSION = '3.0'
16
+ end
15
17
  end
16
18
  end