aspera-cli 4.27.2 → 4.27.4

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 (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -4,6 +4,8 @@ require 'aspera/oauth/jwt'
4
4
  require 'aspera/assert'
5
5
  require 'aspera/cli/option_declarator'
6
6
  require 'aspera/cli/plugins/factory'
7
+ require 'aspera/rainbow'
8
+ using Rainbow
7
9
 
8
10
  module Aspera
9
11
  module Cli
@@ -15,6 +17,7 @@ module Aspera
15
17
  DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
16
18
  private_constant :WIZARD_RESULT_KEYS, :DEFAULT_PRIV_KEY_FILENAME
17
19
 
20
+ # Declared by the `config` plugin (`use_options Wizard`)
18
21
  option :override, description: 'Wizard: override existing value', allowed: Type::BOOLEAN, default: false
19
22
  option :default, description: 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Type::BOOLEAN, default: true
20
23
  option :key_path, description: 'Wizard: path to private key for JWT'
@@ -22,7 +25,6 @@ module Aspera
22
25
  def initialize(parent, main_folder)
23
26
  @parent = parent
24
27
  @main_folder = main_folder
25
- self.class.declare_options(options)
26
28
  end
27
29
 
28
30
  # @return [Boolean] false if in test mode to avoid interactive input
@@ -60,7 +62,7 @@ module Aspera
60
62
  detection_info = nil
61
63
  begin
62
64
  Log.log.debug { "detecting #{plugin_name_sym} at #{app_url}" }
63
- RestParameters.instance.spinner_cb.call(plugin_name_sym.to_s)
65
+ Rest::Parameters.instance.spinner_cb.call(plugin_name_sym.to_s)
64
66
  detection_info = plugin_klass.detect(app_url)
65
67
  rescue OpenSSL::SSL::SSLError => e
66
68
  Log.log.warn(e.message)
@@ -76,7 +78,7 @@ module Aspera
76
78
  # If there is a redirect, then the detector can override the url.
77
79
  found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
78
80
  end
79
- RestParameters.instance.spinner_cb.call(action: :success)
81
+ Rest::Parameters.instance.spinner_cb.call(action: :success)
80
82
  Aspera.assert(!found_apps.empty?) { "No known application found at #{app_url}" }
81
83
  Aspera.assert(found_apps.all? { |a| a.keys.all?(Symbol) }, 'all app info keys must be symbols')
82
84
  return found_apps
@@ -4,6 +4,7 @@
4
4
  if ENV.key?('ENABLE_COVERAGE')
5
5
  require 'simplecov'
6
6
  require 'securerandom'
7
+ require 'fileutils'
7
8
  # compute development top folder based on this source location
8
9
  development_root = File.dirname(File.realpath(__FILE__), 3)
9
10
  coverage_dir = 'tmp/coverage'
@@ -32,7 +33,6 @@ if ENV.key?('ENABLE_COVERAGE')
32
33
  false
33
34
  end
34
35
  # no coverage in these files
35
- add_filter 'lib/aspera/node_simulator.rb'
36
36
  add_filter 'lib/aspera/keychain/macos_security.rb'
37
37
  add_filter 'lib/aspera/assert.rb'
38
38
  end
@@ -5,11 +5,14 @@ require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
7
  require 'singleton'
8
+ require 'openssl'
8
9
  require 'open3'
9
10
  require 'English'
10
11
  require 'shellwords'
12
+ require 'aspera/rainbow'
13
+ using Rainbow
11
14
 
12
- # cspell:words MEBI mswin bccwin
15
+ # cspell:words mswin bccwin
13
16
 
14
17
  module Aspera
15
18
  # detect OS, architecture, and specific stuff
@@ -34,8 +37,6 @@ module Aspera
34
37
  CPU_LIST = [CPU_X86_64, CPU_ARM64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
35
38
 
36
39
  BITS_PER_BYTE = 8
37
- MEBI = 1024 * 1024
38
- BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
39
40
 
40
41
  I18N_VARS = %w(LC_ALL LC_CTYPE LANG).freeze
41
42
 
@@ -48,6 +49,14 @@ module Aspera
48
49
 
49
50
  PROCESS_MODES = %i[execute background capture].freeze
50
51
 
52
+ # System CA certificate bundles, used when the default locations of Ruby's OpenSSL do not exist
53
+ CA_BUNDLE_FILES = [
54
+ '/etc/ssl/certs/ca-certificates.crt', # Debian, Ubuntu, Alpine, Arch
55
+ '/etc/pki/tls/certs/ca-bundle.crt', # RHEL, Fedora
56
+ '/etc/ssl/ca-bundle.pem', # SUSE
57
+ '/etc/ssl/cert.pem' # macOS, BSD
58
+ ].freeze
59
+
51
60
  class << self
52
61
  def ruby_version
53
62
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
@@ -163,8 +172,8 @@ module Aspera
163
172
  if force || !File.exist?(path)
164
173
  # Windows may give error
165
174
  File.unlink(path) rescue nil
166
- # content provided by block
167
- File.write(path, yield)
175
+ # content provided by block, file created with restricted access: never readable by others, even briefly
176
+ File.write(path, yield, perm: mode || 0o600)
168
177
  restrict_file_access(path, mode: mode)
169
178
  end
170
179
  return path
@@ -201,12 +210,16 @@ module Aspera
201
210
  I18N_VARS.each { |var| ENV[var] = 'C' }
202
211
  end
203
212
 
213
+ # Override detection of Unicode support: `true` or `false`, or `nil` for auto-detection
214
+ attr_writer :unicode
215
+
204
216
  # @return [Boolean] true if we can display Unicode characters
205
217
  # Uses Encoding.locale_charmap for OS-independent detection.
206
218
  # Falls back to locale env vars for systems where charmap is not available.
207
219
  # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
208
220
  # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
209
221
  def terminal_supports_unicode?
222
+ return @unicode unless @unicode.nil?
210
223
  return false unless terminal?
211
224
  locale_charmap_utf8? || I18N_VARS.any? { |var| ENV[var]&.include?('UTF-8') }
212
225
  end
@@ -293,6 +306,23 @@ module Aspera
293
306
  nil
294
307
  end
295
308
 
309
+ # When Ruby's OpenSSL was built on another system (e.g. single executable built on RHEL, used on Ubuntu),
310
+ # its default CA locations may not exist: use the system CA bundle instead.
311
+ # @return [String, nil] CA bundle file used, or nil if no change
312
+ def fix_ca_certificates
313
+ return if defined?(JRUBY_VERSION)
314
+ return if [OpenSSL::X509::DEFAULT_CERT_FILE_ENV, OpenSSL::X509::DEFAULT_CERT_DIR_ENV].any? { |var| ENV.key?(var) }
315
+ return if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) || Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR)
316
+ ca_file = CA_BUNDLE_FILES.find { |file| File.file?(file) }
317
+ return if ca_file.nil?
318
+ # Used by OpenSSL for new default stores, and by child processes
319
+ ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] = ca_file
320
+ # Default store of SSL contexts is initialized when openssl is loaded
321
+ OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE.add_file(ca_file)
322
+ Log.log.debug { "Using system CA certificates: #{ca_file}" }
323
+ ca_file
324
+ end
325
+
296
326
  def graphical?
297
327
  @default_gui_mode == :graphical
298
328
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aspera/log'
4
4
  require 'aspera/assert'
5
+ require 'aspera/mime'
5
6
  require 'webrick'
6
7
  require 'json'
7
8
 
@@ -10,7 +11,7 @@ module Aspera
10
11
  class Faspex4GWServlet < WEBrick::HTTPServlet::AbstractServlet
11
12
  AOC_API = 'Aspera::Api::AoC'
12
13
  FX_API = 'Aspera::Api::Faspex'
13
- # @param app_api [Rest] API object
14
+ # @param app_api [Rest::Client] API object
14
15
  # @param app_context [String] workspace id (aoc only)
15
16
  def initialize(server, app_api, app_context)
16
17
  Aspera.assert_values(app_api.class.name, [AOC_API, FX_API])
@@ -6,6 +6,7 @@ require 'English'
6
6
  require 'webrick'
7
7
  require 'aspera/log'
8
8
  require 'aspera/assert'
9
+ require 'aspera/mime'
9
10
 
10
11
  module Aspera
11
12
  # Start a Faspex-4 style post-processing script using Faspex-5 webhook
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/rest_call_error'
3
+ require 'aspera/rest/call_error'
4
4
 
5
5
  module Aspera
6
- # Helper to execute GraphQL queries via a Rest object.
6
+ # Helper to execute GraphQL queries via a Rest::Client object.
7
7
  # Follows the GraphQL-over-HTTP convention: POST {query, variables} to endpoint,
8
8
  # parse top-level `data` key, raise on top-level `errors` array.
9
9
  module GraphQL
@@ -12,16 +12,16 @@ module Aspera
12
12
 
13
13
  class << self
14
14
  # Execute a GraphQL query loaded from a .graphql file in lib/aspera/api/queries/
15
- # @param rest [Rest] API object pointing at the GraphQL endpoint
15
+ # @param rest [Rest::Client] API object pointing at the GraphQL endpoint
16
16
  # @param name [String] Base filename without extension (e.g. 'my_query')
17
17
  # @param variables [Hash] GraphQL variables
18
18
  # @return [Hash] The `data` object from the response
19
- # @raise [RestCallError] if the response contains GraphQL errors
19
+ # @raise [Rest::CallError] if the response contains GraphQL errors
20
20
  def execute(rest, name, variables = {})
21
21
  query = load_query(name)
22
22
  response = rest.create(nil, {query: query, variables: variables})
23
23
  errors = response['errors']
24
- raise RestCallError, errors.map { |e| e['message'] }.join("\n") if errors.is_a?(Array) && !errors.empty?
24
+ raise Rest::CallError, errors.map { |e| e['message'] }.join("\n") if errors.is_a?(Array) && !errors.empty?
25
25
  response['data']
26
26
  end
27
27
 
@@ -2,19 +2,19 @@
2
2
 
3
3
  # cspell:ignore blankslate jsonrpc
4
4
 
5
- require 'aspera/rest_error_analyzer'
5
+ require 'aspera/rest/error_analyzer'
6
6
  require 'aspera/assert'
7
7
  require 'aspera/json_rpc/version'
8
8
  require 'blankslate'
9
9
 
10
- Aspera::RestErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
10
+ Aspera::Rest::ErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
11
11
 
12
12
  module Aspera
13
13
  module JsonRpc
14
- # JSON-RPC 2.0 client over an Aspera::Rest HTTP endpoint.
14
+ # JSON-RPC 2.0 client over an Aspera::Rest::Client HTTP endpoint.
15
15
  # Methods are dispatched dynamically via method_missing.
16
16
  # Example:
17
- # client = JsonRpc::Client.new(Rest.new(base_url: 'http://127.0.0.1:33024'))
17
+ # client = JsonRpc::Client.new(Rest::Client.new(base_url: 'http://127.0.0.1:33024'))
18
18
  # client.get_info
19
19
  # client.start_transfer(app_id: '...', transfer_spec: {...})
20
20
  class Client < BlankSlate
@@ -22,7 +22,7 @@ module Aspera
22
22
  reveal :inspect
23
23
  reveal :to_s
24
24
 
25
- # @param api [Rest] Aspera REST object pointing at the JSON-RPC endpoint
25
+ # @param api [Rest::Client] Aspera REST object pointing at the JSON-RPC endpoint
26
26
  # @param namespace [String, nil] optional method prefix, e.g. "myns."
27
27
  def initialize(api, namespace = nil)
28
28
  super()
@@ -138,7 +138,7 @@ module Aspera
138
138
  end
139
139
  end
140
140
 
141
- # Derive an AES key from +new_password+.
141
+ # Derive an AES key from `new_password`.
142
142
  # @legacy_key is true for vaults created by v4.26.0 or earlier (no kdf field):
143
143
  # key = password truncated/zero-padded to key_bytes (same as symmetric-encryption did).
144
144
  # Otherwise PBKDF2 is used. When @kdf_params is nil (new vault or password change)
@@ -181,7 +181,7 @@ module Aspera
181
181
  'kdf' => @kdf_params,
182
182
  'data' => @cipher.encrypt(YAML.dump(@all_secrets))
183
183
  }
184
- File.write(@path, YAML.dump(vault_info))
184
+ Environment.write_file_restricted(@path, force: true) { YAML.dump(vault_info) }
185
185
  end
186
186
  end
187
187
  end
@@ -18,7 +18,8 @@ module Aspera
18
18
  Aspera.assert_values(vault_type, LIST.map(&:to_s)) { 'vault.type' }
19
19
  case vault_type
20
20
  when 'file'
21
- info[:file] = info.delete(:name) || 'vault.bin' if info.key?(:name)
21
+ # `name` (or `file`), default: `vault.bin`
22
+ info[:file] = info.delete(:name) || info[:file] || 'vault.bin'
22
23
  info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
23
24
  Aspera.assert(!password.nil?, 'please provide password')
24
25
  info[:password] = password
@@ -16,7 +16,7 @@ module Aspera
16
16
  def initialize(url:, token:, vault_id:)
17
17
  super()
18
18
  @vault_id = vault_id
19
- @api = Rest.new(
19
+ @api = Rest::Client.new(
20
20
  base_url: url,
21
21
  headers: {'Authorization' => "Bearer #{token}"}
22
22
  )
@@ -31,7 +31,7 @@ module Aspera
31
31
  @links = links
32
32
  end
33
33
 
34
- # Return the href of the first link whose +rel+ attribute matches +rel+.
34
+ # Return the href of the first link whose `rel` attribute matches `rel`.
35
35
  # Comparison is case-insensitive per RFC 7230 s.3.2 and RFC 8288 s.3.
36
36
  # Returns nil if no link with that relation exists.
37
37
  # @param rel [String]
@@ -41,7 +41,7 @@ module Aspera
41
41
  end
42
42
 
43
43
  class << self
44
- # Parse a raw Link header value into a +LinkHeader+ instance.
44
+ # Parse a raw Link header value into a `LinkHeader` instance.
45
45
  # Uses StringScanner so that commas inside <URI> are not treated as separators.
46
46
  # @param raw [String, nil]
47
47
  # @return [LinkHeader]
data/lib/aspera/log.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/assert'
4
- require 'aspera/colors'
5
4
  require 'aspera/secret_hider'
6
5
  require 'logger'
7
6
  require 'pp'
8
7
  require 'json'
9
8
  require 'singleton'
10
9
  require 'stringio'
10
+ require 'aspera/rainbow'
11
+ using Rainbow
11
12
 
12
13
  # Ignore warnings
13
14
  old_verbose = $VERBOSE
@@ -67,6 +68,26 @@ module Aspera
67
68
  # Where logs are sent to
68
69
  LOG_TYPES = %i[stderr stdout syslog].freeze
69
70
 
71
+ # Logger device for a terminal: the status line (e.g. progress bar) is suspended while a log line is written
72
+ class TerminalDevice
73
+ # @param io [IO] Output stream
74
+ def initialize(io)
75
+ @io = io
76
+ end
77
+
78
+ # Called by `Logger`
79
+ # @param message [String] Formatted log line
80
+ def write(message)
81
+ status_line = Log.instance.status_line if @io.tty?
82
+ return @io.write(message) if status_line.nil?
83
+ status_line.suspend { @io.write(message) }
84
+ end
85
+
86
+ # Standard streams are not closed
87
+ def close; end
88
+ end
89
+ private_constant :TerminalDevice
90
+
70
91
  # Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown
71
92
  LEVELS = Logger::Severity.constants.sort { |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b) }.map { |c| c.downcase.to_sym }.freeze
72
93
 
@@ -118,13 +139,13 @@ module Aspera
118
139
 
119
140
  # Returns the last 2 containers (module/class) and method caller
120
141
  def caller_method
121
- stack = caller
122
- i = stack.rindex { |line| line.include?('Logger') }
123
- frame = stack[i + 1] if i && stack[i + 1]
142
+ locations = caller_locations
143
+ # Outermost frame of logging code: Ruby logger, or this file (level methods, dump)
144
+ i = locations.rindex { |loc| loc.path.end_with?('/logger.rb') || loc.path.eql?(__FILE__) }
145
+ frame = locations[i + 1] if i
124
146
  return '???' unless frame
125
- # Extract the "Class::Module::Method" or "Class#method" part
126
- full = frame[/'([^']+)'/, 1]
127
- return '???' unless full
147
+ # "Class::Module#method" (Ruby >= 3.4) or "method" (Ruby < 3.4)
148
+ full = frame.label
128
149
  # Split into class/module and method parts
129
150
  parts = full.split(/(::|#)/)
130
151
  # Reconstruct keeping only last two class/module names + separator + method
@@ -138,12 +159,15 @@ module Aspera
138
159
  end
139
160
  class_parts = classes.split('::')
140
161
  selected_classes = class_parts.last(2).join('::')
162
+ return method if selected_classes.empty?
141
163
  "#{selected_classes}.#{method}"
142
164
  end
143
165
  end
144
166
 
145
167
  attr_reader :logger_type, :logger
146
168
  attr_accessor :dump_format
169
+ # [#suspend, nil] Object displaying a status line on the terminal (e.g. progress bar), see TerminalDevice
170
+ attr_accessor :status_line
147
171
 
148
172
  # Set the program name used in log output
149
173
  # @param value [String] Program name
@@ -195,9 +219,9 @@ module Aspera
195
219
  current_severity_integer = @logger&.level || ENV['AS_LOG_LEVEL']&.to_i || Logger::Severity::INFO
196
220
  case new_log_type
197
221
  when :stderr
198
- @logger = Logger.new($stderr, progname: @program_name, formatter: DEFAULT_FORMATTER)
222
+ @logger = Logger.new(TerminalDevice.new($stderr), progname: @program_name, formatter: DEFAULT_FORMATTER)
199
223
  when :stdout
200
- @logger = Logger.new($stdout, progname: @program_name, formatter: DEFAULT_FORMATTER)
224
+ @logger = Logger.new(TerminalDevice.new($stdout), progname: @program_name, formatter: DEFAULT_FORMATTER)
201
225
  when :syslog
202
226
  require 'syslog/logger'
203
227
  # The syslog class automatically creates methods from the severity names.
@@ -223,6 +247,7 @@ module Aspera
223
247
 
224
248
  def initialize
225
249
  @logger = nil
250
+ @status_line = nil
226
251
  @program_name = 'aspera'
227
252
  @dump_format = :json
228
253
  @logger_type = :stderr
@@ -230,34 +255,29 @@ module Aspera
230
255
  self.logger_type = @logger_type
231
256
  end
232
257
 
233
- # Define decoration of levels
234
- LVL_DECO = {
235
- TRACE2: %i{dim},
236
- TRACE1: %i{blue},
237
- DEBUG: %i{cyan},
238
- INFO: %i{green},
239
- WARN: %i{bg_brown black},
240
- ERROR: %i{bg_red blink},
241
- FATAL: %i{magenta},
242
- UNKNOWN: %i{blink}
258
+ # Short (4-letters) levels with color (evaluated on each call, as colors can be enabled or disabled by option)
259
+ LVL_COLOR = {
260
+ TRACE2: -> { short_levl(:TRACE2).faint },
261
+ TRACE1: -> { short_levl(:TRACE1).blue },
262
+ DEBUG: -> { short_levl(:DEBUG).cyan },
263
+ INFO: -> { short_levl(:INFO).green },
264
+ WARN: -> { short_levl(:WARN).bg(:yellow).black },
265
+ ERROR: -> { short_levl(:ERROR).bg(:red).blink },
266
+ FATAL: -> { short_levl(:FATAL).magenta },
267
+ UNKNOWN: -> { short_levl(:UNKNOWN).blink }
243
268
  }.freeze
244
269
 
245
- # Short (4-letters) levels with color
246
- LVL_COLOR = LVL_DECO.to_h do |k, v|
247
- [k, short_levl(k).apply(*v)]
248
- end.freeze
249
-
250
- DEFAULT_FORMATTER = ->(s, _d, _p, m) { "#{LVL_COLOR[s]} #{m}\n" }
270
+ DEFAULT_FORMATTER = ->(s, _d, _p, m) { "#{LVL_COLOR[s]&.call} #{m}\n" }
251
271
 
252
272
  # pre-defined formatters
253
273
  FORMATTER_LAMBDAS = {
254
274
  standard: Logger::Formatter.new,
255
275
  default: DEFAULT_FORMATTER,
256
- caller: ->(s, _d, _p, m) { "#{LVL_COLOR[s]} #{Log.caller_method}\n#{m}\n" }
276
+ caller: ->(s, _d, _p, m) { "#{LVL_COLOR[s]&.call} #{Log.caller_method}\n#{m}\n" }
257
277
  }.freeze
258
278
 
259
279
  FORMATTERS = FORMATTER_LAMBDAS.keys
260
280
 
261
- private_constant :LVL_DECO, :DEFAULT_FORMATTER, :FORMATTER_LAMBDAS
281
+ private_constant :DEFAULT_FORMATTER, :FORMATTER_LAMBDAS
262
282
  end
263
283
  end
@@ -82,6 +82,8 @@ module Aspera
82
82
  col_widths = table.transpose.map do |col|
83
83
  [col.flat_map { |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size) }.max, COL_WIDTH].min
84
84
  end
85
+ # do not modify caller's table
86
+ table = table.dup
85
87
  headings = table.shift
86
88
  table.unshift(col_widths.map { |col_width| '-' * col_width })
87
89
  table.unshift(headings)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ # MIME types used in `Content-Type` and `Accept`
5
+ module Mime
6
+ # JSON body
7
+ JSON = 'application/json'
8
+ # URL encoded form body
9
+ WWW = 'application/x-www-form-urlencoded'
10
+ # Plain text body
11
+ TEXT = 'text/plain'
12
+ # JSON:API body (https://jsonapi.org)
13
+ JSON_API = 'application/vnd.api+json'
14
+ # Check if a media type is JSON: `application/json` or structured syntax suffix `+json` (RFC 6839)
15
+ # @param mime [String] Media type, without parameters (see `Rest.parse_header`)
16
+ # @return [Boolean] `true` if JSON
17
+ def json?(mime) = mime == JSON || mime.end_with?(JSON_SUFFIX) || mime == LEGACY_JSON
18
+ module_function :json?
19
+ # Structured syntax suffix for JSON
20
+ JSON_SUFFIX = '+json'
21
+ # Non standard type used by some servers for JSON
22
+ LEGACY_JSON = 'application/x-javascript'
23
+ private_constant :JSON_SUFFIX, :LEGACY_JSON
24
+ end
25
+ end