aspera-cli 4.26.2 → 4.27.1

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 (159) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +310 -21
  4. data/CONTRIBUTING.md +99 -14
  5. data/TODO.md +50 -0
  6. data/bin/ascli +3 -3
  7. data/docs/README.md +10977 -0
  8. data/docs/test-mcp-with-ai.md +202 -0
  9. data/lib/aspera/agent/base.rb +29 -7
  10. data/lib/aspera/agent/connect.rb +58 -21
  11. data/lib/aspera/agent/desktop.rb +53 -18
  12. data/lib/aspera/agent/direct.rb +71 -40
  13. data/lib/aspera/agent/factory.rb +3 -3
  14. data/lib/aspera/agent/httpgw.rb +55 -14
  15. data/lib/aspera/agent/node.rb +34 -2
  16. data/lib/aspera/agent/transferd.rb +48 -12
  17. data/lib/aspera/api/alee.rb +1 -1
  18. data/lib/aspera/api/aoc.rb +42 -44
  19. data/lib/aspera/api/cos_node.rb +10 -9
  20. data/lib/aspera/api/faspex.rb +16 -15
  21. data/lib/aspera/api/httpgw.rb +23 -23
  22. data/lib/aspera/api/node.rb +51 -70
  23. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  24. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  25. data/lib/aspera/ascmd.rb +32 -30
  26. data/lib/aspera/ascp/installation.rb +75 -69
  27. data/lib/aspera/ascp/management.rb +7 -8
  28. data/lib/aspera/assert.rb +12 -13
  29. data/lib/aspera/cli/ascp_actions.rb +155 -0
  30. data/lib/aspera/cli/async_transfer_store.rb +125 -0
  31. data/lib/aspera/cli/bootstrapper.rb +197 -0
  32. data/lib/aspera/cli/command_registry.rb +145 -0
  33. data/lib/aspera/cli/command_spec.rb +141 -0
  34. data/lib/aspera/cli/context.rb +19 -6
  35. data/lib/aspera/cli/error.rb +16 -0
  36. data/lib/aspera/cli/extended_value.rb +27 -26
  37. data/lib/aspera/cli/formatter.rb +95 -59
  38. data/lib/aspera/cli/gem_checker.rb +65 -0
  39. data/lib/aspera/cli/hints.rb +3 -3
  40. data/lib/aspera/cli/http.rb +74 -30
  41. data/lib/aspera/cli/info.rb +2 -0
  42. data/lib/aspera/cli/mailer.rb +97 -0
  43. data/lib/aspera/cli/mcp_tool.rb +225 -0
  44. data/lib/aspera/cli/option_declarator.rb +77 -0
  45. data/lib/aspera/cli/options.schema.yaml +634 -7
  46. data/lib/aspera/cli/parser.rb +1217 -0
  47. data/lib/aspera/cli/plugins/alee.rb +20 -22
  48. data/lib/aspera/cli/plugins/aoc.rb +1178 -881
  49. data/lib/aspera/cli/plugins/ats.rb +205 -162
  50. data/lib/aspera/cli/plugins/base.rb +703 -175
  51. data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
  52. data/lib/aspera/cli/plugins/config.rb +434 -757
  53. data/lib/aspera/cli/plugins/console.rb +107 -65
  54. data/lib/aspera/cli/plugins/cos.rb +46 -33
  55. data/lib/aspera/cli/plugins/factory.rb +6 -6
  56. data/lib/aspera/cli/plugins/faspex5.rb +627 -388
  57. data/lib/aspera/cli/plugins/faspio.rb +54 -51
  58. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  59. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  60. data/lib/aspera/cli/plugins/node.rb +988 -840
  61. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  62. data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
  63. data/lib/aspera/cli/plugins/preview.rb +195 -157
  64. data/lib/aspera/cli/plugins/server.rb +141 -91
  65. data/lib/aspera/cli/plugins/shares.rb +343 -110
  66. data/lib/aspera/cli/preset_actions.rb +159 -0
  67. data/lib/aspera/cli/preset_manager.rb +82 -39
  68. data/lib/aspera/cli/result.rb +78 -26
  69. data/lib/aspera/cli/runner.rb +323 -153
  70. data/lib/aspera/cli/secret_finder.rb +40 -0
  71. data/lib/aspera/cli/special_values.rb +1 -0
  72. data/lib/aspera/cli/sync_actions.rb +89 -64
  73. data/lib/aspera/cli/terminal_formatter.rb +3 -3
  74. data/lib/aspera/cli/transfer_actions.rb +90 -0
  75. data/lib/aspera/cli/transfer_agent.rb +129 -64
  76. data/lib/aspera/cli/transfer_progress.rb +9 -9
  77. data/lib/aspera/cli/vault_manager.rb +79 -0
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +28 -30
  80. data/lib/aspera/colors.rb +3 -3
  81. data/lib/aspera/command_line_builder.rb +34 -34
  82. data/lib/aspera/command_line_converter.rb +1 -1
  83. data/lib/aspera/coverage.rb +1 -2
  84. data/lib/aspera/data_repository.rb +2 -1
  85. data/lib/aspera/dot_container.rb +12 -12
  86. data/lib/aspera/environment.rb +30 -23
  87. data/lib/aspera/exec_spec.rb +13 -0
  88. data/lib/aspera/faspex_gw.rb +5 -5
  89. data/lib/aspera/faspex_postproc.rb +16 -10
  90. data/lib/aspera/graphql.rb +37 -0
  91. data/lib/aspera/hash_ext.rb +8 -2
  92. data/lib/aspera/json_rpc/client.rb +62 -0
  93. data/lib/aspera/json_rpc/version.rb +7 -0
  94. data/lib/aspera/keychain/base.rb +10 -3
  95. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  96. data/lib/aspera/keychain/factory.rb +20 -8
  97. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  98. data/lib/aspera/keychain/macos_security.rb +23 -25
  99. data/lib/aspera/keychain/one_password_api.rb +86 -0
  100. data/lib/aspera/keychain/one_password_base.rb +34 -0
  101. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  102. data/lib/aspera/link_header.rb +82 -0
  103. data/lib/aspera/log.rb +29 -15
  104. data/lib/aspera/markdown.rb +90 -7
  105. data/lib/aspera/nagios.rb +8 -8
  106. data/lib/aspera/node_simulator.rb +35 -27
  107. data/lib/aspera/oauth/base.rb +9 -10
  108. data/lib/aspera/oauth/boot.rb +5 -5
  109. data/lib/aspera/oauth/factory.rb +12 -10
  110. data/lib/aspera/oauth/jwt.rb +9 -11
  111. data/lib/aspera/oauth/web.rb +6 -6
  112. data/lib/aspera/persistency_action_once.rb +12 -12
  113. data/lib/aspera/persistency_folder.rb +18 -11
  114. data/lib/aspera/preview/file_types.rb +5 -5
  115. data/lib/aspera/preview/generator.rb +52 -49
  116. data/lib/aspera/preview/options.rb +3 -2
  117. data/lib/aspera/preview/terminal.rb +5 -5
  118. data/lib/aspera/preview/utils.rb +66 -19
  119. data/lib/aspera/products/connect.rb +2 -2
  120. data/lib/aspera/products/desktop.rb +1 -1
  121. data/lib/aspera/products/other.rb +3 -3
  122. data/lib/aspera/products/transferd.rb +4 -4
  123. data/lib/aspera/proxy_auto_config.rb +15 -14
  124. data/lib/aspera/rest.rb +159 -151
  125. data/lib/aspera/rest_error_analyzer.rb +6 -6
  126. data/lib/aspera/rest_errors_aspera.rb +0 -10
  127. data/lib/aspera/rest_list.rb +11 -10
  128. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  129. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  130. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  131. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  132. data/lib/aspera/schema/documentation.rb +65 -28
  133. data/lib/aspera/schema/reader.rb +136 -13
  134. data/lib/aspera/schema/registry.rb +60 -8
  135. data/lib/aspera/secret_hider.rb +12 -7
  136. data/lib/aspera/ssh.rb +64 -31
  137. data/lib/aspera/ssl.rb +5 -5
  138. data/lib/aspera/sync/conf.schema.yaml +2 -2
  139. data/lib/aspera/sync/database.rb +2 -2
  140. data/lib/aspera/sync/operations.rb +22 -25
  141. data/lib/aspera/temp_file_manager.rb +25 -5
  142. data/lib/aspera/timer_limiter.rb +1 -1
  143. data/lib/aspera/transfer/faux_file.rb +24 -11
  144. data/lib/aspera/transfer/parameters.rb +36 -34
  145. data/lib/aspera/transfer/result.rb +74 -0
  146. data/lib/aspera/transfer/resumer.rb +10 -10
  147. data/lib/aspera/transfer/spec.rb +18 -1
  148. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  149. data/lib/aspera/uri_reader.rb +52 -15
  150. data/lib/aspera/web_auth.rb +8 -7
  151. data/lib/aspera/web_server_simple.rb +15 -12
  152. data/lib/aspera/yaml.rb +5 -4
  153. data.tar.gz.sig +0 -0
  154. metadata +34 -6
  155. metadata.gz.sig +0 -0
  156. data/lib/aspera/cli/manager.rb +0 -766
  157. data/lib/aspera/cli/plugins/faspex.rb +0 -539
  158. data/lib/aspera/json_rpc.rb +0 -52
  159. data/lib/aspera/transfer/uri.rb +0 -56
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore protobuf ckpt
4
+ require 'aspera/yaml'
4
5
  require 'aspera/environment'
5
6
  require 'aspera/data_repository'
6
7
  require 'aspera/log'
@@ -46,9 +47,9 @@ module Aspera
46
47
  def sdk_locations
47
48
  location_url = @transferd_urls
48
49
  transferd_locations = UriReader.read(location_url)
49
- Log.log.debug{"Retrieving SDK locations from #{location_url}"}
50
+ Log.log.debug { "Retrieving SDK locations from #{location_url}" }
50
51
  begin
51
- return YAML.load(transferd_locations)
52
+ return Yaml.safe_load(transferd_locations)
52
53
  rescue Psych::SyntaxError
53
54
  raise "Error when parsing yaml data from: #{location_url}"
54
55
  end
@@ -60,23 +61,23 @@ module Aspera
60
61
  # - "product:PRODUCT_NAME" to use ascp from named product
61
62
  # - "product:FIRST" to use ascp from first found product
62
63
  def sdk_folder=(ascp_location)
63
- Aspera.assert_type(ascp_location, String){'ascp_location'}
64
+ Aspera.assert_type(ascp_location, String) { 'ascp_location' }
64
65
  Aspera.assert(!ascp_location.empty?, 'ascp location cannot be empty: check your config file')
65
66
  folder =
66
67
  if ascp_location.start_with?(USE_PRODUCT_PREFIX)
67
68
  product_name = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
68
69
  if product_name.eql?(FIRST_FOUND)
69
70
  pl = installed_products.first
70
- raise "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" if pl.nil?
71
+ Aspera.assert(!pl.nil?) { "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" }
71
72
  else
72
- pl = installed_products.find{ |i| i[:name].eql?(product_name)}
73
- raise "No such product installed: #{product_name}" if pl.nil?
73
+ pl = installed_products.find { |i| i[:name].eql?(product_name) }
74
+ Aspera.assert(!pl.nil?) { "No such product installed: #{product_name}" }
74
75
  end
75
76
  File.dirname(pl[:ascp_path])
76
77
  else
77
78
  ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
78
79
  end
79
- Log.log.debug{"ascp_folder=#{folder}"}
80
+ Log.log.debug { "ascp_folder=#{folder}" }
80
81
  Products::Transferd.sdk_directory = folder
81
82
  nil
82
83
  end
@@ -88,14 +89,14 @@ module Aspera
88
89
  # @return [Hash] with key = file name (String), and value = path to file
89
90
  def file_paths
90
91
  return SDK_FILES.to_h do |v|
91
- [v.to_s, begin
92
- path(v)
93
- rescue Errno::ENOENT => e
94
- e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
95
- rescue => e
96
- e.message
97
- end]
98
- end
92
+ [v.to_s, begin
93
+ path(v)
94
+ rescue Errno::ENOENT => e
95
+ e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
96
+ rescue => e
97
+ e.message
98
+ end]
99
+ end
99
100
  end
100
101
 
101
102
  # TODO: if using another product than SDK, should use files from there
@@ -106,23 +107,23 @@ module Aspera
106
107
 
107
108
  # Get path of one resource file of currently activated product
108
109
  # keys and certs are generated locally... (they are well known values, arch. independent)
109
- # @param k [Symbol] key of the resource file
110
+ # @param file_type [Symbol] key of the resource file
110
111
  # @return [String, nil] Full path to the resource file or nil if not found
111
- def path(k)
112
+ def path(file_type)
112
113
  file_is_required = true
113
- case k
114
+ case file_type
114
115
  when *EXE_FILES
115
- file_is_required = k.eql?(:ascp)
116
+ file_is_required = file_type.eql?(:ascp)
116
117
  file = Products::Transferd.transferd_path
117
- file = File.join(File.dirname(file), Environment.instance.exe_file(k.to_s)) unless k.eql?(:transferd)
118
+ file = File.join(File.dirname(file), Environment.instance.exe_file(file_type.to_s)) unless file_type.eql?(:transferd)
118
119
  when :ssh_private_dsa, :ssh_private_rsa
119
120
  # assume last 3 letters are type
120
- type = k.to_s[-3..].to_sym
121
- file = check_or_create_sdk_file("aspera_bypass_#{type}.pem"){DataRepository.instance.item(type)}
121
+ type = file_type.to_s[-3..].to_sym
122
+ file = check_or_create_sdk_file("aspera_bypass_#{type}.pem") { DataRepository.instance.item(type) }
122
123
  when :aspera_license
123
- file = check_or_create_sdk_file('aspera-license'){DataRepository.instance.item(:license)}
124
+ file = check_or_create_sdk_file('aspera-license') { DataRepository.instance.item(:license) }
124
125
  when :aspera_conf
125
- file = check_or_create_sdk_file('aspera.conf'){DEFAULT_ASPERA_CONF}
126
+ file = check_or_create_sdk_file('aspera.conf') { DEFAULT_ASPERA_CONF }
126
127
  when :fallback_certificate, :fallback_private_key
127
128
  file_key = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert_private_key.pem')
128
129
  file_cert = File.join(Products::Transferd.sdk_directory, 'aspera_fallback_cert.pem')
@@ -131,30 +132,31 @@ module Aspera
131
132
  # create new self signed certificate for http fallback
132
133
  private_key = OpenSSL::PKey::RSA.new(4096)
133
134
  cert = WebServerSimple.self_signed_cert(private_key)
134
- check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true){private_key.to_pem}
135
- check_or_create_sdk_file('aspera_fallback_cert.pem', force: true){cert.to_pem}
135
+ check_or_create_sdk_file('aspera_fallback_cert_private_key.pem', force: true) { private_key.to_pem }
136
+ check_or_create_sdk_file('aspera_fallback_cert.pem', force: true) { cert.to_pem }
136
137
  end
137
- file = k.eql?(:fallback_certificate) ? file_cert : file_key
138
- else Aspera.error_unexpected_value(k)
138
+ file = file_type.eql?(:fallback_certificate) ? file_cert : file_key
139
+ else Aspera.error_unexpected_value(file_type)
139
140
  end
140
141
  return unless file_is_required || File.exist?(file)
141
- Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{k} not found (#{file})"}
142
+ Aspera.assert(File.exist?(file), type: Errno::ENOENT) { "#{file_type} not found (#{file})" }
142
143
  return file
143
144
  end
144
145
 
145
146
  # default bypass key phrase
146
- # @return [String]
147
+ # @return [String] UUID used as bypass key phrase for SSH certificate
147
148
  def ssh_cert_uuid
148
149
  return DataRepository.instance.item(:uuid)
149
150
  end
150
151
 
151
152
  # get paths of SSH keys to use for ascp client
152
- # @param types [Symbol] types to use
153
+ # @param types [Symbol] types to use (one of CLIENT_SSH_KEY_OPTIONS)
154
+ # @return [Array<String>] list of private key file paths
153
155
  def aspera_token_ssh_key_paths(types)
154
156
  Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
155
157
  return case types
156
158
  when :dsa_rsa, :rsa
157
- types.to_s.split('_').map{ |i| Installation.instance.path("ssh_private_#{i}".to_sym)}
159
+ types.to_s.split('_').map { |i| Installation.instance.path("ssh_private_#{i}".to_sym) }
158
160
  when :per_client
159
161
  Aspera.error_not_implemented
160
162
  end
@@ -170,13 +172,11 @@ module Aspera
170
172
  Aspera.assert_type(exe_path, String)
171
173
  Aspera.assert_type(vers_arg, String)
172
174
  return unless File.exist?(exe_path)
173
- exe_version = nil
174
- cmd_out = %x("#{exe_path}" #{vers_arg})
175
- raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
175
+ cmd_out, _err, status = Environment.secure_execute(exe_path, vers_arg, mode: :capture, exception: false)
176
+ raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless status.success?
176
177
  # get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
177
178
  m = cmd_out.match(/ version ([0-9.]+)/)
178
- exe_version = m[1].gsub(/\.$/, '') unless m.nil?
179
- return exe_version
179
+ m.nil? ? nil : m[1].gsub(/\.$/, '')
180
180
  end
181
181
 
182
182
  # Extract some stings from ascp logs
@@ -234,30 +234,30 @@ module Aspera
234
234
  return ascp_data
235
235
  end
236
236
 
237
- # @return the url for download of SDK archive for the given platform and version
237
+ # @return [String] the url for download of SDK archive for the given platform and version
238
238
  def sdk_url_for_platform(platform: nil, version: nil)
239
239
  all_locations = sdk_locations
240
240
  platform = Environment.instance.architecture if platform.nil?
241
- locations = all_locations.select{ |l| l['platform'].eql?(platform)}
242
- raise "No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}" if locations.empty?
243
- version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
244
- info = locations.select{ |entry| entry['version'].eql?(version)}
245
- raise "No such version: #{version} for #{platform}" if info.empty?
241
+ locations = all_locations.select { |l| l['platform'].eql?(platform) }
242
+ Aspera.assert(!locations.empty?) { "No SDK for platform: #{platform}, available: #{all_locations.map { |i| i['platform'] }.uniq}" }
243
+ version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
244
+ info = locations.select { |entry| entry['version'].eql?(version) }
245
+ Aspera.assert(!info.empty?) { "No such version: #{version} for #{platform}" }
246
246
  return info.first['url']
247
247
  end
248
248
 
249
- # @param sdk_archive_path [String] path to SDK archive
249
+ # @param url [String] URL or filename used to detect archive format
250
+ # @param archive_io [StringIO] archive content as an in-memory IO stream
250
251
  # @yieldparam entry_name [String] File path in archive
251
252
  # @yieldparam entry_stream [IO, Gem::Package::TarReader::Entry] Data stream
252
253
  # @yieldparam link_target [String, nil] Link target if symlink, nil otherwise
253
- def extract_archive_files(sdk_archive_path)
254
+ def extract_archive_files(url, archive_io)
254
255
  Aspera.assert(block_given?, 'missing block')
255
- case sdk_archive_path
256
+ case url
256
257
  # Windows and Mac use zip
257
258
  when /\.zip$/
258
259
  require 'zip'
259
- # extract files from archive
260
- Zip::File.open(sdk_archive_path) do |zip_file|
260
+ Zip::File.open_buffer(archive_io) do |zip_file|
261
261
  zip_file.each do |entry|
262
262
  next if entry.name.end_with?('/')
263
263
  entry.get_input_stream do |io|
@@ -269,7 +269,7 @@ module Aspera
269
269
  when /\.tar\.gz/
270
270
  require 'zlib'
271
271
  require 'rubygems/package'
272
- Zlib::GzipReader.open(sdk_archive_path) do |gzip|
272
+ Zlib::GzipReader.wrap(archive_io) do |gzip|
273
273
  Gem::Package::TarReader.new(gzip) do |tar|
274
274
  tar.each do |entry|
275
275
  next if entry.directory?
@@ -278,19 +278,18 @@ module Aspera
278
278
  end
279
279
  end
280
280
  else
281
- raise "unknown archive extension: #{sdk_archive_path}"
281
+ raise "unknown archive extension: #{url}"
282
282
  end
283
283
  end
284
284
 
285
- # Retrieves ascp binary for current system architecture from URL or file
285
+ # Downloads and extracts the SDK archive for the current platform into +folder+
286
286
  # @param folder [String] Destination folder path
287
287
  # @param url [nil, String] URL to SDK archive, if nil: default url for version
288
288
  # @param version [nil, String] Specific version, if nil: latest version
289
289
  # @param backup [Boolean] If destination folder exists, then rename
290
290
  # @yieldparam entry_name [String] File path from archive
291
291
  # @yieldreturn [String, nil] Destination sub folder (end with /) or file, or nil to not extract
292
- # @return [Array] name, ascp version (from execution), folder
293
- def install_sdk(folder:, url: nil, version: nil, backup: true)
292
+ def download_sdk(folder:, url: nil, version: nil, backup: true)
294
293
  url ||= sdk_url_for_platform(version: version)
295
294
  # Rename old install
296
295
  if backup && Dir.exist?(folder) && !Dir.empty?(folder)
@@ -303,13 +302,19 @@ module Aspera
303
302
  extracted_files = {}
304
303
  # Security: Get canonical path of installation directory for boundary checks
305
304
  install_boundary = File.realpath(folder)
306
- sdk_archive_path = UriReader.read_as_file(url)
307
- extract_archive_files(sdk_archive_path) do |entry_name, entry_stream, link_target|
305
+ archive_io = StringIO.new
306
+ if UriReader.file?(url)
307
+ archive_io.write(File.binread(UriReader.file_path(url)))
308
+ else
309
+ Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
310
+ archive_io.rewind
311
+ end
312
+ extract_archive_files(url, archive_io) do |entry_name, entry_stream, link_target|
308
313
  dest_folder = if block_given?
309
314
  yield(entry_name)
310
315
  else
311
316
  # default files to extract directly to main folder if in selected source folders
312
- Products::Transferd::RUNTIME_FOLDERS.any?{ |i| entry_name.match?(%r{^[^/]*/#{i}/})} ? '/' : nil
317
+ Products::Transferd::RUNTIME_FOLDERS.any? { |i| entry_name.match?(%r{^[^/]*/#{i}/}) } ? '/' : nil
313
318
  end
314
319
  next if dest_folder.nil?
315
320
  dest_folder = File.join(folder, dest_folder)
@@ -322,7 +327,7 @@ module Aspera
322
327
  # Security: Detect basename collisions that could overwrite symlinks
323
328
  file_basename = File.basename(dest_file)
324
329
  if extracted_files.key?(file_basename)
325
- Log.log.warn{"Rejecting file with duplicate basename: #{entry_name} (basename: #{file_basename}, previous: #{extracted_files[file_basename]})"}
330
+ Log.log.warn { "Rejecting file with duplicate basename: #{entry_name} (basename: #{file_basename}, previous: #{extracted_files[file_basename]})" }
326
331
  next
327
332
  end
328
333
  extracted_files[file_basename] = entry_name
@@ -330,7 +335,7 @@ module Aspera
330
335
  if link_target.nil?
331
336
  # Security: Check if destination already exists
332
337
  if File.exist?(dest_file)
333
- Log.log.warn{"Rejecting write to existing file or link: #{dest_file}"}
338
+ Log.log.warn { "Rejecting write to existing file or link: #{dest_file}" }
334
339
  next
335
340
  end
336
341
  # Security: Verify the resolved path stays within installation boundary
@@ -340,17 +345,17 @@ module Aspera
340
345
  # Check where the file would resolve to (handles existing symlinks in path)
341
346
  resolved_dest = File.realpath(File.dirname(dest_file))
342
347
  unless resolved_dest.start_with?(install_boundary)
343
- Log.log.warn{"Rejecting file outside installation directory: #{dest_file} resolves to #{resolved_dest}"}
348
+ Log.log.warn { "Rejecting file outside installation directory: #{dest_file} resolves to #{resolved_dest}" }
344
349
  next
345
350
  end
346
351
  rescue Errno::ENOENT
347
352
  # Directory doesn't exist yet, verify the intended path
348
353
  unless dest_file.start_with?(folder)
349
- Log.log.warn{"Rejecting file with path outside installation directory: #{dest_file}"}
354
+ Log.log.warn { "Rejecting file with path outside installation directory: #{dest_file}" }
350
355
  next
351
356
  end
352
357
  end
353
- File.open(dest_file, 'wb'){ |output_stream| IO.copy_stream(entry_stream, output_stream)}
358
+ File.open(dest_file, 'wb') { |output_stream| IO.copy_stream(entry_stream, output_stream) }
354
359
  else
355
360
  # Security: Validate symlink target stays within installation boundary
356
361
  # Resolve the symlink target relative to its location
@@ -364,7 +369,7 @@ module Aspera
364
369
  end
365
370
  # Check if resolved target would be outside installation directory
366
371
  unless resolved_target.start_with?(install_boundary)
367
- Log.log.warn{"Rejecting symlink pointing outside installation directory: #{entry_name} -> #{link_target} (resolves to #{resolved_target})"}
372
+ Log.log.warn { "Rejecting symlink pointing outside installation directory: #{entry_name} -> #{link_target} (resolves to #{resolved_target})" }
368
373
  next
369
374
  end
370
375
  File.symlink(link_target, dest_file)
@@ -372,12 +377,13 @@ module Aspera
372
377
  end
373
378
  end
374
379
 
375
- # Retrieve SDK either from specified URL, or specified version from standard location, or latest version
376
- # @param url [nil, String] URL
377
- # @param version [nil, String] URL
378
- def retrieve_sdk(url: nil, version: nil)
380
+ # Downloads, installs and configures the SDK, returns name, version and install folder
381
+ # @param url [nil, String] URL to SDK archive, if nil: default url for version
382
+ # @param version [nil, String] Specific version, if nil: latest version
383
+ # @return [Array<String>] sdk name, sdk version, install folder
384
+ def install_sdk(url: nil, version: nil)
379
385
  folder = Products::Transferd.sdk_directory
380
- install_sdk(folder: folder, url: url, version: version)
386
+ download_sdk(folder: folder, url: url, version: version)
381
387
  # Ensure necessary files are there, or generate them, restrict file access on SDK executables
382
388
  SDK_FILES.each do |file_id_sym|
383
389
  file_path = path(file_id_sym)
@@ -420,7 +426,7 @@ module Aspera
420
426
  # all executable files from SDK
421
427
  EXE_FILES = %i[ascp ascp4 async transferd].freeze
422
428
  # IDs of files present in SDK
423
- SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
429
+ SDK_FILES = (EXE_FILES + %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key]).freeze
424
430
  TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
425
431
  # filename for ascp with optional extension (Windows)
426
432
  private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
@@ -433,7 +439,7 @@ module Aspera
433
439
 
434
440
  public
435
441
 
436
- # @return the list of installed products in format of product_locations_on_current_os
442
+ # @return [Array<Hash>] the list of installed products in format of product_locations_on_current_os
437
443
  def installed_products
438
444
  return @found_products unless @found_products.nil?
439
445
  # :expected M app name is taken from the manifest if present, else defaults to this value
@@ -12,7 +12,7 @@ module Aspera
12
12
  # https://www.ibm.com/support/pages/error-code-reference-tables
13
13
  # mgmtmess.c : as_mgmt_err_is_retryable
14
14
  # Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
15
- # rubocop:disable Layout/FirstHashElementLineBreak
15
+ # rubocop:disable-next Layout/FirstHashElementLineBreak
16
16
  ERRORS = {
17
17
  # id retry-able mnemo/code message additional info
18
18
  0 => {r: false, c: 'UNKNOWN', m: 'unknown', a: 'unknown'},
@@ -93,7 +93,6 @@ module Aspera
93
93
  66 => {r: false, c: 'PEER_REQUIRES_FIPS', m: 'Peer rejects cipher due to FIPS mode enabled on peer',
94
94
  a: 'Peer rejects cipher due to FIPS mode enabled on peer'}
95
95
  }.freeze
96
- # rubocop:enable Layout/FirstHashElementLineBreak
97
96
  # cspell: disable
98
97
  OPERATIONS = %w[
99
98
  NOP
@@ -306,8 +305,8 @@ module Aspera
306
305
  # @param name [String] Field name
307
306
  def field_snake_to_native(name)
308
307
  field = name.delete('_')
309
- result = PARAMETERS.find{ |w| w.casecmp?(field)}
310
- raise "No such field: #{name}" if result.nil?
308
+ result = PARAMETERS.find { |w| w.casecmp?(field) }
309
+ Aspera.assert(!result.nil?) { "No such field: #{name}" }
311
310
  result
312
311
  end
313
312
 
@@ -328,7 +327,7 @@ module Aspera
328
327
  # @return [String] frame to send on management port
329
328
  def command_to_stream(data)
330
329
  data
331
- .map{ |key, value| "#{field_snake_to_native(key)}: #{value}"}
330
+ .map { |key, value| "#{field_snake_to_native(key)}: #{value}" }
332
331
  .unshift(MGT_HEADER)
333
332
  .push(MGT_FRAME_SEPARATOR, '')
334
333
  .join("\n")
@@ -353,15 +352,15 @@ module Aspera
353
352
  # begin event
354
353
  @event_build = {}
355
354
  when /^([^:]+): (.*)$/
356
- Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: data without header'}
355
+ Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: data without header' }
357
356
  # event field
358
357
  @event_build[Regexp.last_match(1)] = Regexp.last_match(2)
359
358
  when MGT_FRAME_SEPARATOR
360
- Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: end frame without header'}
359
+ Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: end frame without header' }
361
360
  @last_event = @event_build
362
361
  @event_build = nil
363
362
  return @last_event
364
- else Aspera.error_unexpected_value(line){'mgt port'}
363
+ else Aspera.error_unexpected_value(line) { 'mgt port' }
365
364
  end
366
365
  return
367
366
  end
data/lib/aspera/assert.rb CHANGED
@@ -34,9 +34,9 @@ module Aspera
34
34
  end
35
35
 
36
36
  # Assert that a condition is true, else raise exception
37
- # @param assertion [TrueClass, FalseClass] Must be true
38
- # @param info [String,nil] Fixed message in case assert fails, else use block
39
- # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
37
+ # @param assertion [Boolean] Must be true
38
+ # @param info [String,nil] Fixed message in case assert fails, else use block
39
+ # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log level
40
40
  # @yieldreturn [String] A string that describes the problem for complex messages
41
41
  # The block is executed in the context of the Aspera module
42
42
  def assert(assertion, info = nil, type: AssertError)
@@ -45,7 +45,6 @@ module Aspera
45
45
  message = 'assertion failed'
46
46
  info = yield if block_given?
47
47
  message = type.eql?(AssertError) ? "#{message}: #{info}" : info if info
48
- # message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
49
48
  report_error(type, message)
50
49
  end
51
50
 
@@ -55,7 +54,7 @@ module Aspera
55
54
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
56
55
  # @yieldreturn [String] Additional description to prepend to the error message
57
56
  def assert_type(value, *classes, type: AssertError)
58
- assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting type #{classes.join(', ')}, but have (#{value.class})#{value.inspect}"}
57
+ assert(classes.any? { |k| value.is_a?(k) }, type: type) { "#{"#{yield}: " if block_given?}expecting type #{classes.join(', ')}, but have (#{value.class})#{value.inspect}" }
59
58
  end
60
59
 
61
60
  # Assert that all value of array are of the same specified type.
@@ -64,20 +63,20 @@ module Aspera
64
63
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
65
64
  # @yieldreturn [String] Additional description to prepend to the error message
66
65
  def assert_array_all(array, klass, type: AssertError)
67
- assert_type(array, Array, type: AssertError){'array'}
68
- assert(array.all?(klass), type: type){"#{"#{yield}: " if block_given?}expecting all as #{klass}, but have #{array.map(&:class).uniq}"}
66
+ assert_type(array, Array, type: AssertError) { 'array' }
67
+ assert(array.all?(klass), type: type) { "#{"#{yield}: " if block_given?}expecting all as #{klass}, but have #{array.map(&:class).uniq}" }
69
68
  end
70
69
 
71
70
  # Assert value is Hash, keys have type, and Values have type
72
71
  # @param hash [Hash] The hash to check
73
- # @param key_class [Class] The expected type of keys (or nil)
74
- # @param value_class [Class] The expected type of values (or nil)
72
+ # @param key_class [Class] The expected type of keys (or nil for no check)
73
+ # @param value_class [Class] The expected type of values (or nil for no check)
75
74
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
76
75
  # @yieldreturn [String] Additional description to prepend to the error message
77
76
  def assert_hash_all(hash, key_class, value_class, type: AssertError)
78
- assert_type(hash, Hash, type: AssertError){'hash'}
79
- assert_array_all(hash.keys, key_class, type: type){"#{"#{yield}: " if block_given?}keys"} unless key_class.nil?
80
- assert_array_all(hash.values, value_class, type: type){"#{"#{yield}: " if block_given?}values"} unless value_class.nil?
77
+ assert_type(hash, Hash, type: AssertError) { 'hash' }
78
+ assert_array_all(hash.keys, key_class, type: type) { "#{"#{yield}: " if block_given?}keys" } unless key_class.nil?
79
+ assert_array_all(hash.values, value_class, type: type) { "#{"#{yield}: " if block_given?}values" } unless value_class.nil?
81
80
  end
82
81
 
83
82
  # Assert that value is one of the given values
@@ -86,7 +85,7 @@ module Aspera
86
85
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
87
86
  # @yieldreturn [String] Additional description to prepend to the error message
88
87
  def assert_values(value, values, type: AssertError)
89
- assert_type(values, Array, type: AssertError){'values'}
88
+ assert_type(values, Array, type: AssertError) { 'values' }
90
89
  assert(values.include?(value), type: type) do
91
90
  val_list = values.inspect
92
91
  val_list = "one of #{val_list}" if values.is_a?(Array)
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/agent/factory'
4
+ require 'aspera/schema/registry'
5
+ require 'aspera/schema/reader'
6
+
7
+ module Aspera
8
+ module Cli
9
+ # Mixin for Config plugin: ASCP / Transferd related actions
10
+ module AscpActions
11
+ TRANSFERD_APP_NAME = 'sdk'
12
+
13
+ # Mapping from agent symbol to schema key in options.schema.yaml
14
+ # Agents without dedicated schema (connect, desktop) are omitted.
15
+ AGENT_SCHEMA_KEY = {
16
+ direct: Schema::Registry::DIRECT_AGENT_OPTIONS,
17
+ node: Schema::Registry::NODE_AGENT_OPTIONS,
18
+ httpgw: Schema::Registry::HTTPGW_AGENT_OPTIONS,
19
+ transferd: Schema::Registry::TRANSFERD_AGENT_OPTIONS
20
+ }.freeze
21
+ private_constant :AGENT_SCHEMA_KEY
22
+
23
+ # Set the SDK directory, checking default and former locations
24
+ def set_sdk_dir
25
+ sdk_dir = Products::Transferd.sdk_directory rescue nil
26
+ if sdk_dir.nil?
27
+ @sdk_default_location = true
28
+ Log.log.debug('SDK folder is not set, checking default')
29
+ sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
30
+ Log.log.debug { "Checking: #{sdk_dir}" }
31
+ if !Dir.exist?(sdk_dir)
32
+ Log.log.debug { "No such folder: #{sdk_dir}" }
33
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
34
+ Log.log.debug { "Checking: #{former_sdk_folder}" }
35
+ sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
36
+ end
37
+ Log.log.debug { "Using: #{sdk_dir}" }
38
+ Products::Transferd.sdk_directory = sdk_dir
39
+ end
40
+ end
41
+
42
+ # Install the transfer SDK (ascp + transferd) from a URL or using the default source.
43
+ # Version defaults to +Info::SDK_VERSION+; pass +LATEST+ as argument to install the latest available version.
44
+ # @param version [String, nil] version to install; nil means use the default SDK version
45
+ # @return [Result::Status] installation result message
46
+ def install_transfer_sdk(version: nil)
47
+ asked_version = version.nil? ? Info::SDK_VERSION : version
48
+ asked_version = nil if asked_version.eql?(SpecialValues::LATEST)
49
+ sdk_url = options.get_option(:sdk_url, mandatory: true)
50
+ sdk_url = nil if sdk_url.eql?(SpecialValues::DEF)
51
+ name, ver, folder = Ascp::Installation.instance.install_sdk(url: sdk_url, version: asked_version)
52
+ return Result::Status.new("Installed #{name} version #{ver} in #{folder}")
53
+ end
54
+
55
+ def action_ascp_show(**)
56
+ Result::Text.new(Ascp::Installation.instance.path(:ascp))
57
+ end
58
+
59
+ def action_ascp_info(**)
60
+ data = Ascp::Installation.instance.ascp_info
61
+ data['ts'] = transfer.user_transfer_spec
62
+ DataRepository::ELEMENTS.each_with_object(data) { |i, h| h[i.to_s] = DataRepository.instance.item(i) }
63
+ SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
64
+ Result::SingleObject.new(data)
65
+ end
66
+
67
+ def action_ascp_install(version: nil, **)
68
+ install_transfer_sdk(version: version)
69
+ end
70
+
71
+ def action_ascp_spec(**)
72
+ builder = Schema::Documentation.new(TerminalFormatter, Transfer::Spec::SCHEMA, include_option: true, agent_columns: true).build
73
+ Result::ObjectList.new(builder.rows, fields: builder.columns)
74
+ end
75
+
76
+ def action_ascp_schema(agent_name: nil, **)
77
+ schema = Transfer::Spec::SCHEMA.current.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
78
+ schema['properties'] = schema['properties'].select { |_k, v| CommandLineBuilder.supported_by_agent(agent_name, v) } unless agent_name.nil?
79
+ schema['properties'] = schema['properties'].sort.to_h
80
+ Result::SingleObject.new(schema)
81
+ end
82
+
83
+ def action_ascp_errors(**)
84
+ error_data = []
85
+ Ascp::Management::ERRORS.each_pair do |code, prop|
86
+ error_data.push(code: code, mnemonic: prop[:c], retry: prop[:r], info: prop[:a])
87
+ end
88
+ Result::ObjectList.new(error_data)
89
+ end
90
+
91
+ def action_ascp_products_list(**)
92
+ Result::ObjectList.new(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
93
+ end
94
+
95
+ def action_agents_list(**)
96
+ rows = Agent::Factory::ALL.map do |sym, names|
97
+ schema_key = AGENT_SCHEMA_KEY[sym]
98
+ param_names =
99
+ if schema_key
100
+ Schema::Registry.instance.reader(schema_key).current['properties']&.keys&.sort&.join(', ') || ''
101
+ else
102
+ ''
103
+ end
104
+ {
105
+ 'name' => sym.to_s,
106
+ 'short' => names[:short].to_s,
107
+ 'parameters' => param_names
108
+ }
109
+ end.sort_by { |r| r['name'] }
110
+ Result::ObjectList.new(rows, fields: %w[name short parameters])
111
+ end
112
+
113
+ def action_agents_show(agent_name:, **)
114
+ names = Agent::Factory::ALL[agent_name]
115
+ schema_key = AGENT_SCHEMA_KEY[agent_name]
116
+ agent_info = {
117
+ 'name' => agent_name.to_s,
118
+ 'short' => names[:short].to_s,
119
+ 'description' => schema_key ? Schema::Registry.instance.reader(schema_key).current['description'].to_s : '(no configurable parameters)'
120
+ }
121
+ if schema_key
122
+ properties = Schema::Registry.instance.reader(schema_key).current['properties'] || {}
123
+ rows = properties.map do |pname, pdef|
124
+ row = {'parameter' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
125
+ row['required'] = (Schema::Registry.instance.reader(schema_key).current['required'] || []).include?(pname) ? 'yes' : 'no'
126
+ row['default'] = pdef.key?('default') ? pdef['default'].inspect : ''
127
+ row['enum'] = pdef.key?('enum') ? pdef['enum'].join(', ') : ''
128
+ row
129
+ end
130
+ return Result::ObjectList.new(rows, fields: %w[parameter required type default enum description])
131
+ end
132
+ Result::SingleObject.new(agent_info)
133
+ end
134
+
135
+ def action_agents_parameters(agent_name:, **)
136
+ schema_key = AGENT_SCHEMA_KEY[agent_name]
137
+ return Result::Nothing.new if schema_key.nil?
138
+ properties = Schema::Registry.instance.reader(schema_key).current['properties'] || {}
139
+ rows = properties.map do |pname, pdef|
140
+ {'name' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
141
+ end
142
+ Result::ObjectList.new(rows, fields: %w[name type description])
143
+ end
144
+
145
+ def action_transferd_install(**)
146
+ install_transfer_sdk
147
+ end
148
+
149
+ def action_transferd_list(**)
150
+ sdk_list = Ascp::Installation.instance.sdk_locations
151
+ Result::ObjectList.new(sdk_list, fields: sdk_list.first.keys - ['url'])
152
+ end
153
+ end
154
+ end
155
+ end