aspera-cli 4.26.0 → 4.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +36 -4
  4. data/CONTRIBUTING.md +2 -5
  5. data/bin/ascli +2 -2
  6. data/lib/aspera/agent/direct.rb +1 -1
  7. data/lib/aspera/agent/factory.rb +4 -0
  8. data/lib/aspera/agent/httpgw.rb +1 -1
  9. data/lib/aspera/agent/node.rb +2 -2
  10. data/lib/aspera/api/aoc.rb +45 -37
  11. data/lib/aspera/api/cos_node.rb +1 -1
  12. data/lib/aspera/api/faspex.rb +7 -6
  13. data/lib/aspera/api/node.rb +9 -9
  14. data/lib/aspera/ascmd.rb +4 -4
  15. data/lib/aspera/ascp/installation.rb +13 -6
  16. data/lib/aspera/assert.rb +17 -13
  17. data/lib/aspera/cli/context.rb +58 -0
  18. data/lib/aspera/cli/extended_value.rb +9 -4
  19. data/lib/aspera/cli/formatter.rb +91 -187
  20. data/lib/aspera/cli/http.rb +167 -0
  21. data/lib/aspera/cli/manager.rb +132 -77
  22. data/lib/aspera/cli/options.schema.yaml +82 -0
  23. data/lib/aspera/cli/plugins/alee.rb +2 -2
  24. data/lib/aspera/cli/plugins/aoc.rb +113 -77
  25. data/lib/aspera/cli/plugins/ats.rb +16 -16
  26. data/lib/aspera/cli/plugins/base.rb +64 -49
  27. data/lib/aspera/cli/plugins/config.rb +172 -444
  28. data/lib/aspera/cli/plugins/console.rb +7 -7
  29. data/lib/aspera/cli/plugins/cos.rb +2 -2
  30. data/lib/aspera/cli/plugins/factory.rb +3 -0
  31. data/lib/aspera/cli/plugins/faspex.rb +22 -22
  32. data/lib/aspera/cli/plugins/faspex5.rb +68 -49
  33. data/lib/aspera/cli/plugins/faspio.rb +1 -1
  34. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  35. data/lib/aspera/cli/plugins/node.rb +80 -80
  36. data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
  37. data/lib/aspera/cli/plugins/preview.rb +9 -9
  38. data/lib/aspera/cli/plugins/server.rb +7 -7
  39. data/lib/aspera/cli/plugins/shares.rb +2 -2
  40. data/lib/aspera/cli/preset_manager.rb +235 -0
  41. data/lib/aspera/cli/result.rb +310 -0
  42. data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
  43. data/lib/aspera/cli/sync_actions.rb +15 -11
  44. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  45. data/lib/aspera/cli/transfer_agent.rb +17 -15
  46. data/lib/aspera/cli/transfer_progress.rb +6 -6
  47. data/lib/aspera/cli/version.rb +1 -1
  48. data/lib/aspera/cli/wizard.rb +5 -6
  49. data/lib/aspera/command_line_builder.rb +22 -18
  50. data/lib/aspera/data_repository.rb +4 -0
  51. data/lib/aspera/dot_container.rb +1 -1
  52. data/lib/aspera/environment.rb +25 -6
  53. data/lib/aspera/faspex_gw.rb +1 -1
  54. data/lib/aspera/formatter_interface.rb +14 -0
  55. data/lib/aspera/hash_ext.rb +6 -0
  56. data/lib/aspera/json_rpc.rb +6 -5
  57. data/lib/aspera/keychain/base.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  59. data/lib/aspera/keychain/factory.rb +1 -1
  60. data/lib/aspera/keychain/macos_security.rb +1 -1
  61. data/lib/aspera/log.rb +10 -5
  62. data/lib/aspera/markdown.rb +4 -1
  63. data/lib/aspera/nagios.rb +2 -2
  64. data/lib/aspera/oauth/base.rb +5 -5
  65. data/lib/aspera/oauth/boot.rb +43 -0
  66. data/lib/aspera/oauth/factory.rb +38 -14
  67. data/lib/aspera/oauth/web.rb +2 -2
  68. data/lib/aspera/oauth.rb +1 -0
  69. data/lib/aspera/persistency_action_once.rb +2 -2
  70. data/lib/aspera/preview/file_types.rb +4 -0
  71. data/lib/aspera/products/connect.rb +3 -0
  72. data/lib/aspera/products/transferd.rb +2 -2
  73. data/lib/aspera/proxy_auto_config.rb +6 -3
  74. data/lib/aspera/rest.rb +9 -5
  75. data/lib/aspera/rest_error_analyzer.rb +4 -0
  76. data/lib/aspera/rest_list.rb +10 -3
  77. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
  78. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
  79. data/lib/aspera/schema/async_tables.yaml +361 -0
  80. data/lib/aspera/schema/documentation.rb +107 -0
  81. data/lib/aspera/schema/reader.rb +75 -0
  82. data/lib/aspera/schema/registry.rb +70 -0
  83. data/lib/aspera/secret_hider.rb +4 -0
  84. data/lib/aspera/sync/conf.schema.yaml +0 -26
  85. data/lib/aspera/sync/database.rb +14 -10
  86. data/lib/aspera/sync/operations.rb +14 -10
  87. data/lib/aspera/temp_file_manager.rb +4 -0
  88. data/lib/aspera/transfer/faux_file.rb +1 -1
  89. data/lib/aspera/transfer/parameters.rb +4 -4
  90. data/lib/aspera/transfer/resumer.rb +2 -2
  91. data/lib/aspera/transfer/spec.rb +3 -3
  92. data/lib/aspera/transfer/spec.schema.yaml +18 -9
  93. data/lib/aspera/transfer/uri.rb +1 -1
  94. data/lib/aspera/uri_reader.rb +1 -1
  95. data/lib/aspera/yaml.rb +4 -2
  96. data.tar.gz.sig +0 -0
  97. metadata +28 -15
  98. metadata.gz.sig +1 -1
  99. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aspera/log'
4
4
  require 'aspera/assert'
5
+ require 'aspera/schema/registry'
5
6
  require 'yaml'
6
7
  module Aspera
7
8
  # Helper class to build command line from a parameter list (key-value hash)
@@ -35,9 +36,9 @@ module Aspera
35
36
 
36
37
  class << self
37
38
  # Called by provider of definition before constructor of this class so that schema has all mandatory fields
38
- def read_schema(folder, name, ascp: false)
39
- schema = YAML.load_file(File.join(folder, "#{name}.schema.yaml"))
40
- validate_schema(schema, ascp: ascp)
39
+ # @return [Aspera::Schema::Reader]
40
+ def read_schema(name_sym, ascp: false)
41
+ validate_schema(Schema::Registry.instance.reader(name_sym), ascp: ascp)
41
42
  end
42
43
 
43
44
  # @param agent [Symbol] Transfer agent name
@@ -49,29 +50,32 @@ module Aspera
49
50
 
50
51
  private
51
52
 
53
+ DIRECT_PROPERTIES = %w[x-cli-option x-cli-envvar x-cli-special].freeze
54
+
52
55
  # Fill default values for some fields in the schema
53
- # @param schema [Hash] The JSON schema
54
- # @param ascp [Boolean] `true` if ascp
56
+ # @param schema [Schema::Reader] The JSON schema
57
+ # @param ascp [Boolean] `true` if `ascp`
58
+ # @return [Schema::Reader] The JSON schema
55
59
  def validate_schema(schema, ascp: false)
56
- direct_props = %w[x-cli-option x-cli-envvar x-cli-special].freeze
57
- schema['properties'].each do |name, info|
58
- Aspera.assert_type(info, Hash){"#{info.class} for #{name}"}
59
- unsupported_keys = info.keys - PROPERTY_KEYS
60
+ Aspera.assert_type(schema, Schema::Reader){'schema'}
61
+ Aspera.assert(schema.current.key?('properties')){"Schema must have 'properties': #{schema}"}
62
+ schema.each_property do |property_schema, name, _full_name|
63
+ node = property_schema.current
64
+ unsupported_keys = node.keys - PROPERTY_KEYS
60
65
  Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
61
- Aspera.assert(info.key?('type') || info.key?('enum')){"Missing type for #{name} in #{schema['description']}"}
62
- Aspera.assert(info['type'].eql?('boolean')){"switch must be bool: #{name}"} if info['x-cli-switch'] && !info['x-cli-special']
63
- info['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if info['x-cli-option'].eql?(true) || (info['x-cli-switch'].eql?(true) && !info.key?('x-cli-option'))
64
- Aspera.assert(direct_props.any?{ |i| info.key?(i)}, type: :warn){name} if ascp && supported_by_agent(:direct, info)
65
- info.freeze
66
- validate_schema(info, ascp: ascp) if info['type'].eql?('object') && info['properties']
67
- validate_schema(info['items'], ascp: ascp) if info['type'].eql?('array') && info['items'] && info['items']['properties']
66
+ Aspera.assert(node.key?('type') || node.key?('enum')){"Missing type for #{name} in #{schema.current.dig('description').current}"}
67
+ Aspera.assert(node['type'].eql?('boolean')){"switch must be bool: #{name}"} if node['x-cli-switch'] && !node['x-cli-special']
68
+ node['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if node['x-cli-option'].eql?(true) || (node['x-cli-switch'].eql?(true) && !node.key?('x-cli-option'))
69
+ Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, name, type: :warn) if ascp && supported_by_agent(:direct, node)
70
+ node.freeze
68
71
  end
69
72
  schema
70
73
  end
71
74
  end
72
75
 
73
- # @param [Hash] object with parameters
74
- # @param [Hash] schema JSON schema
76
+ # @param object [Hash] object with parameters
77
+ # @param schema [Schema::Reader] JSON schema
78
+ # @param convert [Object] function to convert value
75
79
  def initialize(object, schema, convert)
76
80
  @object = object # keep reference so that it can be modified by caller before calling `process_params`
77
81
  @schema = schema
@@ -6,6 +6,10 @@ require 'openssl'
6
6
 
7
7
  module Aspera
8
8
  # a simple binary data repository
9
+ #
10
+ # @!method self.instance
11
+ # Returns the singleton instance of DataRepository
12
+ # @return [DataRepository] the singleton instance
9
13
  class DataRepository
10
14
  include Singleton
11
15
 
@@ -39,7 +39,7 @@ module Aspera
39
39
  # @param container [Hash, Array]
40
40
  # @param index [String, Integer]
41
41
  def array_requires_integer_index!(container, index)
42
- Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)){'Using String index when Integer index used previously'}
42
+ Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer), 'Using String index when Integer index used previously')
43
43
  end
44
44
 
45
45
  # Create a new `Hash` or `Array` depending on type of `key`
@@ -13,6 +13,10 @@ require 'shellwords'
13
13
 
14
14
  module Aspera
15
15
  # detect OS, architecture, and specific stuff
16
+ #
17
+ # @!method self.instance
18
+ # Returns the singleton instance of Environment
19
+ # @return [Environment] the singleton instance
16
20
  class Environment
17
21
  include Singleton
18
22
 
@@ -111,7 +115,7 @@ module Aspera
111
115
  # @raise [RuntimeError] If `:exception` is `true` and the process fails in `:capture` mode.
112
116
  def secure_execute(*cmd, mode: :execute, **kwargs)
113
117
  cmd = cmd.map(&:to_s)
114
- Aspera.assert(cmd.size.positive?, type: ArgumentError){'executable must be present'}
118
+ Aspera.assert(cmd.size.positive?, 'executable must be present', type: ArgumentError)
115
119
  Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
116
120
  Log.log.debug do
117
121
  parts = [mode.to_s, 'command:']
@@ -151,9 +155,9 @@ module Aspera
151
155
  # @param path [String] the file path
152
156
  # @param force [Boolean] if true, overwrite the file
153
157
  # @param mode [Integer] the file mode (permissions)
154
- # @block [Proc] return the content to write to the file
158
+ # @yieldreturn [String] The content to write to the file
155
159
  def write_file_restricted(path, force: false, mode: nil)
156
- Aspera.assert(block_given?, type: Aspera::InternalError)
160
+ Aspera.assert(block_given?, 'block required for write_file_restricted', type: Aspera::InternalError)
157
161
  if force || !File.exist?(path)
158
162
  # Windows may give error
159
163
  File.unlink(path) rescue nil
@@ -192,10 +196,24 @@ module Aspera
192
196
  end
193
197
 
194
198
  # @return true if we can display Unicode characters
199
+ # Uses Encoding.locale_charmap for OS-independent detection.
200
+ # Falls back to locale env vars for systems where charmap is not available.
195
201
  # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
196
202
  # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
197
203
  def terminal_supports_unicode?
198
- terminal? && I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
204
+ return false unless terminal?
205
+ locale_charmap_utf8? || I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
206
+ end
207
+
208
+ private
209
+
210
+ # @return true if the locale charmap resolves to UTF-8
211
+ # Unix: nl_langinfo(CODESET) returns "UTF-8"
212
+ # Windows: GetACP returns "CP65001" which Encoding.find resolves as UTF-8
213
+ def locale_charmap_utf8?
214
+ Encoding.find(Encoding.locale_charmap) == Encoding::UTF_8
215
+ rescue ArgumentError
216
+ false
199
217
  end
200
218
  end
201
219
  attr_accessor :url_method, :file_illegal_characters
@@ -266,6 +284,7 @@ module Aspera
266
284
  return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
267
285
  ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
268
286
  Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
287
+ nil
269
288
  end
270
289
 
271
290
  def graphical?
@@ -280,7 +299,7 @@ module Aspera
280
299
  when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
281
300
  when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
282
301
  when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s)
283
- else Assert.error_unexpected_value(os){'no graphical open method'}
302
+ else Aspera.error_unexpected_value(os){'no graphical open method'}
284
303
  end
285
304
  end
286
305
 
@@ -334,7 +353,7 @@ module Aspera
334
353
  filename = filename.chop while filename.end_with?(' ', '.')
335
354
  if @file_illegal_characters&.size.to_i >= 2
336
355
  # replace all illegal characters with safe_char
337
- filename = filename.tr(@file_illegal_characters[1..-1], safe_char)
356
+ filename = filename.tr(@file_illegal_characters[1..], safe_char)
338
357
  end
339
358
  # ensure only one safe_char is used at a time
340
359
  return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
@@ -68,7 +68,7 @@ module Aspera
68
68
  case request.path
69
69
  when '/aspera/faspex/send'
70
70
  begin
71
- Aspera.assert(!request.body.nil?){'payload missing'}
71
+ Aspera.assert(!request.body.nil?, 'payload missing')
72
72
  faspex_pkg_parameters = JSON.parse(request.body)
73
73
  Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
74
74
  # compare string, as class is not yet known here
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/assert'
4
+
5
+ module Aspera
6
+ # Common interface for documentation formatters
7
+ # Used by Schema::Documentation to format tables
8
+ module FormatterInterface
9
+ Aspera.require_method!(:tick)
10
+ Aspera.require_method!(:special_format)
11
+ Aspera.require_method!(:check_row)
12
+ Aspera.require_method!(:markdown_text)
13
+ end
14
+ end
@@ -9,6 +9,12 @@ class ::Hash
9
9
  merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
10
10
  end
11
11
 
12
+ # Recursively iterate through hash and execute block on leaf values
13
+ # @param memory [Object, nil] Optional memory object passed to block
14
+ # @yieldparam hash [Hash] The current hash
15
+ # @yieldparam key [Object] The current key
16
+ # @yieldparam value [Object] The current value (non-Hash)
17
+ # @yieldparam memory [Object, nil] The memory object
12
18
  def deep_do(memory = nil, &block)
13
19
  each do |key, value|
14
20
  if value.is_a?(Hash)
@@ -36,15 +36,16 @@ module Aspera
36
36
  id: @request_id += 1
37
37
  })
38
38
  Aspera.assert_type(data, Hash){'response'}
39
- Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
40
- Aspera.assert(data.key?('id')){'missing id in response'}
41
- Aspera.assert(!(data.key?('error') && data.key?('result'))){'both error and response'}
39
+ Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION, 'bad version in response')
40
+ Aspera.assert(data.key?('id'), 'missing id in response')
41
+ Aspera.assert(!(data.key?('error') && data.key?('result')), 'both error and response')
42
42
  Aspera.assert(
43
43
  !data.key?('error') ||
44
44
  data['error'].is_a?(Hash) &&
45
45
  data['error']['code'].is_a?(Integer) &&
46
- data['error']['message'].is_a?(String)
47
- ){'bad error response'}
46
+ data['error']['message'].is_a?(String),
47
+ 'bad error response'
48
+ )
48
49
  return data['result']
49
50
  end
50
51
  end
@@ -11,7 +11,7 @@ module Aspera
11
11
  options.each_pair do |k, v|
12
12
  Aspera.assert_type(v, String){k.to_s}
13
13
  end
14
- Aspera.assert(options.key?(:label)){'label is required'}
14
+ Aspera.assert(options.key?(:label), 'label is required')
15
15
  end
16
16
  end
17
17
  end
@@ -28,7 +28,7 @@ module Aspera
28
28
  vault_file = File.read(@path)
29
29
  if vault_file.start_with?('---')
30
30
  vault_info = YAML.parse(vault_file).to_ruby
31
- Aspera.assert(vault_info.keys.sort == FILE_KEYS){'Invalid vault file'}
31
+ Aspera.assert(vault_info.keys.sort == FILE_KEYS, 'Invalid vault file')
32
32
  @cipher_name = vault_info['cipher']
33
33
  vault_encrypted_data = vault_info['data']
34
34
  else
@@ -20,7 +20,7 @@ module Aspera
20
20
  when 'file'
21
21
  info[:file] = name || 'vault.bin'
22
22
  info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
23
- Aspera.assert(!password.nil?){'please provide password'}
23
+ Aspera.assert(!password.nil?, 'please provide password')
24
24
  info[:password] = password
25
25
  # this module requires compilation, so it is optional
26
26
  require 'aspera/keychain/encrypted_hash'
@@ -42,7 +42,7 @@ module Aspera
42
42
  url = options&.delete(:url)
43
43
  if !url.nil?
44
44
  uri = URI.parse(url)
45
- Aspera.assert(uri.scheme.eql?('https')){'only https'}
45
+ Aspera.assert(uri.scheme.eql?('https'), 'only https')
46
46
  options[:protocol] = 'htps' # cspell: disable-line
47
47
  raise Error, 'host required in URL' if uri.host.nil?
48
48
  options[:server] = uri.host
data/lib/aspera/log.rb CHANGED
@@ -54,6 +54,10 @@ $VERBOSE = old_verbose
54
54
 
55
55
  module Aspera
56
56
  # Singleton object for logging
57
+ #
58
+ # @!method self.instance
59
+ # Returns the singleton instance of Log
60
+ # @return [Log] the singleton instance
57
61
  class Log
58
62
  include Singleton
59
63
 
@@ -77,10 +81,10 @@ module Aspera
77
81
  # @param name [String, Symbol] Name of object dumped
78
82
  # @param object [Hash, nil] Data to dump
79
83
  # @param level [Symbol] Debug level
80
- # @param block [Proc, nil] Give computed object
84
+ # @yieldreturn [Object] Computed object to dump (alternative to object parameter)
81
85
  def dump(name, object = nil, level: :debug, &block)
82
86
  return unless instance.logger.send(:"#{level}?")
83
- Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
87
+ Aspera.assert(object.nil? || block.nil?, 'Use either object, or block, not both')
84
88
  object = yield if block_given?
85
89
  instance.logger.send(level, obj_dump(name, object))
86
90
  end
@@ -93,12 +97,13 @@ module Aspera
93
97
  JSON.pretty_generate(object) rescue PP.pp(object, +'')
94
98
  when :ruby
95
99
  PP.pp(object, +'')
96
- else error_unexpected_value(instance.dump_format){'dump format'}
100
+ else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
97
101
  end
98
102
  "#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
99
103
  end
100
104
 
101
105
  # Capture the output of $stderr and log it at debug level
106
+ # @yieldreturn [void] Code block whose stderr output will be captured
102
107
  def capture_stderr
103
108
  real_stderr = $stderr
104
109
  $stderr = StringIO.new
@@ -167,7 +172,7 @@ module Aspera
167
172
  # Get symbol of debug level of underlying logger
168
173
  # @return [Symbol] One of LEVELS
169
174
  def level
170
- Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level))
175
+ Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level), 'unexpected logger level value')
171
176
  Logger::SEVERITY_LABEL[@logger.level].downcase
172
177
  end
173
178
 
@@ -192,7 +197,7 @@ module Aspera
192
197
  end
193
198
  # Use `local2` facility, like other Aspera components
194
199
  @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
195
- else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
200
+ else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
196
201
  end
197
202
  @logger.level = current_severity_integer
198
203
  @logger_type = new_log_type
@@ -8,11 +8,14 @@ module Aspera
8
8
  HTML_BREAK = '<br/>'
9
9
 
10
10
  class << self
11
+ COL_WIDTH = 80
11
12
  # Generate markdown from the provided 2D table
13
+ # @param table [Array<Array<String>>] 2D array of strings
14
+ # @return [String] markdown table
12
15
  def table(table)
13
16
  # get max width of each columns
14
17
  col_widths = table.transpose.map do |col|
15
- [col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, 80].min
18
+ [col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, COL_WIDTH].min
16
19
  end
17
20
  headings = table.shift
18
21
  table.unshift(col_widths.map{ |col_width| '-' * col_width})
data/lib/aspera/nagios.rb CHANGED
@@ -24,7 +24,7 @@ module Aspera
24
24
  # Process results of a analysis and display status and exit with code
25
25
  def process(data)
26
26
  Aspera.assert_type(data, Array)
27
- Aspera.assert(!data.empty?){'data is empty'}
27
+ Aspera.assert(!data.empty?, 'data is empty')
28
28
  %w[status component message].each do |c|
29
29
  Aspera.assert(data.first.key?(c)){"result must have #{c}"}
30
30
  end
@@ -79,7 +79,7 @@ module Aspera
79
79
  # Readable status list
80
80
  # @return [Array] of Hash
81
81
  def status_list
82
- Aspera.assert(!@data.empty?){'missing result'}
82
+ Aspera.assert(!@data.empty?, 'missing result')
83
83
  @data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
84
84
  end
85
85
  end
@@ -15,15 +15,15 @@ module Aspera
15
15
  class Base
16
16
  Aspera.require_method!(:create_token)
17
17
  # @param params [Hash] Parameters for token creation (client_id, client_secret, scope, etc...)
18
- # @param use_query [Boolean] Provide parameters in query instead of body
19
18
  # @param path_token [String] API end point to create a token from base URL
19
+ # @param use_query [Boolean] Provide parameters in query instead of body
20
20
  # @param token_field [String] Field in result that contains the token
21
21
  # @param cache_ids [Array] List of unique identifiers for cache id generation
22
22
  # @param **rest_params [Hash] Parameters for REST
23
23
  def initialize(
24
24
  params: {},
25
+ path_token: 'token',
25
26
  use_query: false,
26
- path_token: 'token',
27
27
  token_field: Factory::TOKEN_FIELD,
28
28
  cache_ids: [],
29
29
  **rest_params
@@ -42,10 +42,10 @@ module Aspera
42
42
 
43
43
  # The OAuth API Object
44
44
  attr_reader :api
45
- # Sub path to generate token
46
- attr_reader :path_token
47
45
  # Parameters to generate token
48
46
  attr_reader :params
47
+ # Sub path to generate token
48
+ attr_reader :path_token
49
49
 
50
50
  # Helper method to create token as per RFC
51
51
  # @return [HTTPResponse]
@@ -122,7 +122,7 @@ module Aspera
122
122
  token_data = JSON.parse(json_data)
123
123
  Factory.instance.persist_mgr.put(@token_cache_id, json_data)
124
124
  end
125
- Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
125
+ Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"} unless token_data.nil?
126
126
  # ok we shall have a token here
127
127
  return token_data[@token_field]
128
128
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+ require 'aspera/assert'
5
+ require 'json'
6
+
7
+ module Aspera
8
+ module OAuth
9
+ # Token provider bootstrapped from an existing cookie (e.g. AoC browser cookie).
10
+ # Injects the bearer token and optional refresh token directly into the cache.
11
+ # Never generates a new token from scratch — raises if cache+refresh are both exhausted.
12
+ class Boot < Base
13
+ # @param cookie [String, nil] Raw cookie string (--password), nil to rely on existing cache
14
+ # @param username [String, nil] Expected subject; if provided, must match token's `sub` claim
15
+ # @param **base_params Forwarded to Base (base_url:, params: {client_id:, scope:}, etc.)
16
+ def initialize(cookie: nil, username: nil, **base_params)
17
+ if cookie.nil?
18
+ # No cookie: rely on existing cache, identified by username if provided
19
+ Aspera.assert(username, 'Provide --password (cookie) on first use, or --username for cache lookup', type: ParameterError)
20
+ super(**base_params, cache_ids: [username])
21
+ else
22
+ cookies = cookie.split('; ').map{ |p| p.split('=', 2)}.to_h
23
+ Aspera.assert(cookies.key?('aoc.token'), '--password cookie does not contain aoc.token', type: ParameterError)
24
+ token = cookies['aoc.token']
25
+ decoded = Factory.instance.decode_token(token)
26
+ Aspera.assert_type(decoded, Hash){'Boot: token is not a decodable JWT'}
27
+ sub = decoded['sub']
28
+ Aspera.assert(username.nil? || username.eql?(sub)){"Boot: --username #{username} does not match token subject #{sub}"}
29
+ super(**base_params, cache_ids: [sub])
30
+ token_data = {Factory::TOKEN_FIELD => token}
31
+ token_data['refresh_token'] = cookies['aoc.refresh'] if cookies.key?('aoc.refresh')
32
+ Factory.instance.persist_mgr.put(@token_cache_id, JSON.generate(token_data))
33
+ end
34
+ end
35
+
36
+ # Should never be reached: if cache and refresh are both exhausted, re-authenticate via browser
37
+ def create_token
38
+ Aspera.report_error(AssertError, 'Boot: token expired and no refresh available — re-authenticate in browser')
39
+ end
40
+ end
41
+ Factory.instance.register_token_creator(Boot)
42
+ end
43
+ end
@@ -7,10 +7,14 @@ require 'base64'
7
7
  module Aspera
8
8
  module OAuth
9
9
  # Factory to create tokens and manage their cache
10
+ #
11
+ # @!method self.instance
12
+ # Returns the singleton instance of Factory
13
+ # @return [Factory] the singleton instance
10
14
  class Factory
11
15
  include Singleton
12
16
 
13
- # a prefix for persistency of tokens (simplify garbage collect)
17
+ # prefix for persistency of tokens (simplify garbage collect)
14
18
  PERSIST_CATEGORY_TOKEN = 'token'
15
19
  # prefix for bearer authorization when in header
16
20
  SPACE_BEARER_AUTH_SCHEME = 'Bearer '
@@ -19,33 +23,40 @@ module Aspera
19
23
  private_constant :PERSIST_CATEGORY_TOKEN, :SPACE_BEARER_AUTH_SCHEME
20
24
 
21
25
  class << self
26
+ # Format a token for use in Authorization header
22
27
  # @param token [String] The token alone
23
28
  # @return [String] Value suitable for Authorization header
24
29
  def bearer_authorization(token)
25
30
  return "#{SPACE_BEARER_AUTH_SCHEME}#{token}"
26
31
  end
27
32
 
28
- # @return true if the authorization contains a bearer token , i.e. auth scheme is bearer
33
+ # Check if the authorization contains a bearer token
34
+ # @param authorization [String] The authorization header value
35
+ # @return [Boolean] true if the authorization contains a bearer token, i.e. auth scheme is bearer
29
36
  def bearer_auth?(authorization)
30
37
  return authorization.start_with?(SPACE_BEARER_AUTH_SCHEME)
31
38
  end
32
39
 
33
40
  # Extract only token from Authorization (remove scheme)
41
+ # @param authorization [String] The authorization header value
42
+ # @return [String] The bearer token without the scheme prefix
34
43
  def bearer_token(authorization)
35
44
  Aspera.assert(bearer_auth?(authorization)){'not a bearer token, wrong prefix scheme'}
36
- return authorization[SPACE_BEARER_AUTH_SCHEME.length..-1]
45
+ return authorization.delete_prefix(SPACE_BEARER_AUTH_SCHEME)
37
46
  end
38
47
 
39
48
  # Generate a unique cache id for a token creator
40
49
  # @param url [String] Base URL of the OAuth server
41
50
  # @param creator_class [Class] Class of the token creator
42
51
  # @param params [Array] List of parameters (can be nested) to uniquely identify the token
43
- # @return a unique cache identifier
52
+ # @return [String] a unique cache identifier
44
53
  def cache_id(url, creator_class, *params)
45
54
  return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
46
55
  end
47
56
 
48
- # @return snake version of class name
57
+ # Convert a class name to snake_case symbol
58
+ # @param creator_class [Class] The class to convert
59
+ # @return [Symbol] snake_case version of class name
49
60
  def class_to_id(creator_class)
50
61
  return creator_class.name.split('::').last.capital_to_snake.to_sym
51
62
  end
@@ -53,6 +64,7 @@ module Aspera
53
64
 
54
65
  private
55
66
 
67
+ # Initialize the factory with default parameters and empty collections
56
68
  def initialize
57
69
  # persistency manager
58
70
  @persist = nil
@@ -73,12 +85,16 @@ module Aspera
73
85
 
74
86
  attr_reader :parameters
75
87
 
88
+ # Set the persistence manager for token caching
89
+ # @param manager [Object] The persistence manager instance
76
90
  def persist_mgr=(manager)
77
91
  @persist = manager
78
92
  # cleanup expired tokens
79
93
  @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_max_age])
80
94
  end
81
95
 
96
+ # Get or initialize the persistence manager
97
+ # @return [Object] The persistence manager instance
82
98
  def persist_mgr
83
99
  if @persist.nil?
84
100
  # use OAuth::Factory.instance.persist_mgr=PersistencyFolder.new)
@@ -91,11 +107,14 @@ module Aspera
91
107
  return @persist
92
108
  end
93
109
 
94
- # delete all existing tokens
110
+ # Delete all existing tokens in cache
111
+ # @return [void]
95
112
  def flush_tokens
96
113
  persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN)
97
114
  end
98
115
 
116
+ # Retrieve all persisted tokens with their decoded information
117
+ # @return [Array<Hash>] Array of token information hashes
99
118
  def persisted_tokens
100
119
  data = persist_mgr.current_items(PERSIST_CATEGORY_TOKEN)
101
120
  data.each.map do |k, v|
@@ -134,12 +153,16 @@ module Aspera
134
153
  return info
135
154
  end
136
155
 
137
- # register a bearer token decoder, mainly to inspect expiry date
156
+ # Register a bearer token decoder for inspecting token properties
157
+ # @param method [Proc] The decoder lambda/proc to register
158
+ # @return [void]
138
159
  def register_decoder(method)
139
160
  @decoders.push(method)
140
161
  end
141
162
 
142
- # decode token using all registered decoders
163
+ # Decode a token using all registered decoders
164
+ # @param token [String] The token to decode
165
+ # @return [Hash, nil] Decoded token data or nil if no decoder succeeded
143
166
  def decode_token(token)
144
167
  @decoders.each do |decoder|
145
168
  result = decoder.call(token) rescue nil
@@ -148,10 +171,9 @@ module Aspera
148
171
  return
149
172
  end
150
173
 
151
- # register a token creation method
152
- # @param id creation type from field :grant_method in constructor
153
- # @param lambda_create called to create token
154
- # @param id_create called to generate unique id for token, for cache
174
+ # Register a token creation method
175
+ # @param creator_class [Class] The token creator class to register
176
+ # @return [void]
155
177
  def register_token_creator(creator_class)
156
178
  Aspera.assert_type(creator_class, Class)
157
179
  id = Factory.class_to_id(creator_class)
@@ -159,7 +181,9 @@ module Aspera
159
181
  @token_type_classes[id] = creator_class
160
182
  end
161
183
 
162
- # @return one of the registered creators for the given create type
184
+ # Create a token creator instance for the specified grant method
185
+ # @param parameters [Hash] Parameters including :grant_method and creator-specific options
186
+ # @return [Object] An instance of the registered token creator class
163
187
  def create(**parameters)
164
188
  Aspera.assert_type(parameters, Hash)
165
189
  id = parameters[:grant_method]
@@ -169,6 +193,6 @@ module Aspera
169
193
  end
170
194
  end
171
195
  # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
172
- Factory.instance.register_decoder(lambda{ |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not JWS token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon
196
+ Factory.instance.register_decoder(lambda{ |token| parts = token.split('.'); Aspera.assert_values(parts.length, [3]){'JWS token parts'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon
173
197
  end
174
198
  end
@@ -22,8 +22,8 @@ module Aspera
22
22
  @redirect_uri = redirect_uri
23
23
  @path_authorize = path_authorize
24
24
  uri = URI.parse(@redirect_uri)
25
- Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
26
- Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
25
+ Aspera.assert_values(uri.scheme, %w[http https]){'redirect_uri scheme must be http or https'}
26
+ Aspera.assert(!uri.port.nil?, 'redirect_uri must have a port')
27
27
  # TODO: we could check that host is localhost or local address, as we are going to listen locally
28
28
  end
29
29
 
data/lib/aspera/oauth.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/oauth/factory'
4
+ require 'aspera/oauth/boot'
4
5
  require 'aspera/oauth/generic'
5
6
  require 'aspera/oauth/jwt'
6
7
  require 'aspera/oauth/web'
@@ -23,9 +23,9 @@ module Aspera
23
23
  # @param :merge Optional merge data from file to current data
24
24
  def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
25
25
  Aspera.assert(MANAGER_METHODS.all?{ |i| manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
26
- Aspera.assert(!data.nil?)
26
+ Aspera.assert(!data.nil?, 'data must not be nil')
27
27
  Aspera.assert_type(id, String)
28
- Aspera.assert(!id.empty?)
28
+ Aspera.assert(!id.empty?, 'id must not be empty')
29
29
  Aspera.assert_type(delete, Proc)
30
30
  Aspera.assert_type(parse, Proc)
31
31
  Aspera.assert_type(format, Proc)
@@ -8,6 +8,10 @@ require 'marcel'
8
8
  module Aspera
9
9
  module Preview
10
10
  # function conversion_type returns one of the types: CONVERSION_TYPES
11
+ #
12
+ # @!method self.instance
13
+ # Returns the singleton instance of FileTypes
14
+ # @return [FileTypes] the singleton instance
11
15
  class FileTypes
12
16
  include Singleton
13
17