aspera-cli 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +54 -3
  4. data/CONTRIBUTING.md +7 -7
  5. data/README.md +1457 -880
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/proxy.pac +1 -1
  9. data/lib/aspera/aoc.rb +198 -127
  10. data/lib/aspera/ascmd.rb +24 -14
  11. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  12. data/lib/aspera/cli/error.rb +17 -0
  13. data/lib/aspera/cli/extended_value.rb +47 -12
  14. data/lib/aspera/cli/formatter.rb +260 -171
  15. data/lib/aspera/cli/hints.rb +80 -0
  16. data/lib/aspera/cli/main.rb +101 -147
  17. data/lib/aspera/cli/manager.rb +160 -124
  18. data/lib/aspera/cli/plugin.rb +70 -59
  19. data/lib/aspera/cli/plugins/alee.rb +0 -1
  20. data/lib/aspera/cli/plugins/aoc.rb +239 -273
  21. data/lib/aspera/cli/plugins/ats.rb +8 -5
  22. data/lib/aspera/cli/plugins/bss.rb +2 -2
  23. data/lib/aspera/cli/plugins/config.rb +516 -375
  24. data/lib/aspera/cli/plugins/console.rb +40 -0
  25. data/lib/aspera/cli/plugins/cos.rb +4 -5
  26. data/lib/aspera/cli/plugins/faspex.rb +99 -84
  27. data/lib/aspera/cli/plugins/faspex5.rb +179 -148
  28. data/lib/aspera/cli/plugins/node.rb +219 -153
  29. data/lib/aspera/cli/plugins/orchestrator.rb +52 -17
  30. data/lib/aspera/cli/plugins/preview.rb +46 -32
  31. data/lib/aspera/cli/plugins/server.rb +57 -17
  32. data/lib/aspera/cli/plugins/shares.rb +34 -12
  33. data/lib/aspera/cli/sync_actions.rb +68 -0
  34. data/lib/aspera/cli/transfer_agent.rb +45 -55
  35. data/lib/aspera/cli/transfer_progress.rb +74 -0
  36. data/lib/aspera/cli/version.rb +1 -1
  37. data/lib/aspera/colors.rb +3 -1
  38. data/lib/aspera/command_line_builder.rb +14 -11
  39. data/lib/aspera/cos_node.rb +3 -2
  40. data/lib/aspera/environment.rb +17 -6
  41. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  42. data/lib/aspera/fasp/agent_base.rb +31 -77
  43. data/lib/aspera/fasp/agent_connect.rb +21 -22
  44. data/lib/aspera/fasp/agent_direct.rb +88 -102
  45. data/lib/aspera/fasp/agent_httpgw.rb +196 -192
  46. data/lib/aspera/fasp/agent_node.rb +41 -34
  47. data/lib/aspera/fasp/agent_trsdk.rb +75 -34
  48. data/lib/aspera/fasp/error_info.rb +2 -2
  49. data/lib/aspera/fasp/faux_file.rb +52 -0
  50. data/lib/aspera/fasp/installation.rb +43 -184
  51. data/lib/aspera/fasp/management.rb +244 -0
  52. data/lib/aspera/fasp/parameters.rb +59 -26
  53. data/lib/aspera/fasp/parameters.yaml +75 -8
  54. data/lib/aspera/fasp/products.rb +162 -0
  55. data/lib/aspera/fasp/transfer_spec.rb +1 -1
  56. data/lib/aspera/fasp/uri.rb +4 -4
  57. data/lib/aspera/faspex_gw.rb +2 -2
  58. data/lib/aspera/faspex_postproc.rb +2 -2
  59. data/lib/aspera/hash_ext.rb +2 -2
  60. data/lib/aspera/json_rpc.rb +49 -0
  61. data/lib/aspera/line_logger.rb +23 -0
  62. data/lib/aspera/log.rb +57 -16
  63. data/lib/aspera/node.rb +97 -14
  64. data/lib/aspera/oauth.rb +36 -18
  65. data/lib/aspera/open_application.rb +4 -4
  66. data/lib/aspera/persistency_folder.rb +2 -2
  67. data/lib/aspera/preview/file_types.rb +4 -2
  68. data/lib/aspera/preview/generator.rb +22 -35
  69. data/lib/aspera/preview/options.rb +2 -0
  70. data/lib/aspera/preview/terminal.rb +24 -13
  71. data/lib/aspera/preview/utils.rb +19 -26
  72. data/lib/aspera/rest.rb +103 -72
  73. data/lib/aspera/rest_call_error.rb +1 -1
  74. data/lib/aspera/rest_error_analyzer.rb +15 -14
  75. data/lib/aspera/rest_errors_aspera.rb +37 -34
  76. data/lib/aspera/secret_hider.rb +14 -16
  77. data/lib/aspera/ssh.rb +4 -1
  78. data/lib/aspera/sync.rb +128 -122
  79. data/lib/aspera/temp_file_manager.rb +10 -3
  80. data/lib/aspera/web_auth.rb +10 -7
  81. data/lib/aspera/web_server_simple.rb +9 -4
  82. data.tar.gz.sig +0 -0
  83. metadata +33 -15
  84. metadata.gz.sig +0 -0
  85. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  86. data/lib/aspera/cli/listener/logger.rb +0 -22
  87. data/lib/aspera/cli/listener/progress.rb +0 -50
  88. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  89. data/lib/aspera/cli/plugins/sync.rb +0 -44
  90. data/lib/aspera/fasp/listener.rb +0 -13
@@ -1,95 +1,180 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore jsonpp
3
4
  require 'aspera/secret_hider'
5
+ require 'aspera/environment'
4
6
  require 'terminal-table'
5
7
  require 'yaml'
6
8
  require 'pp'
7
9
 
8
10
  module Aspera
9
11
  module Cli
12
+ CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
13
+ # This class is used to transform a complex structure into a simple hash
14
+ class Flattener
15
+ def initialize
16
+ @result = nil
17
+ end
18
+
19
+ # General method
20
+ def flatten(something)
21
+ raise 'only Hash' unless something.is_a?(Hash)
22
+ @result = {}
23
+ flatten_any(something, '')
24
+ return @result
25
+ end
26
+
27
+ # Special method for configuration overview
28
+ def config_over(something)
29
+ @result = []
30
+ something.each do |config, preset|
31
+ preset.each do |parameter, value|
32
+ @result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
33
+ end
34
+ end
35
+ return @result
36
+ end
37
+
38
+ private
39
+
40
+ # Recursive function to flatten any type
41
+ # @param something [Object] to be flattened
42
+ # @param name [String] name of englobing key
43
+ def flatten_any(something, name)
44
+ if something.is_a?(Hash)
45
+ flattened_hash(something, name)
46
+ elsif something.is_a?(Array)
47
+ flatten_array(something, name)
48
+ elsif something.is_a?(String) && something.empty?
49
+ @result[name] = Formatter.special('empty string')
50
+ elsif something.nil?
51
+ @result[name] = Formatter.special('null')
52
+ # elsif something.eql?(true) || something.eql?(false)
53
+ # @result[name] = something
54
+ else
55
+ @result[name] = something
56
+ end
57
+ end
58
+
59
+ # Recursive function to flatten an array
60
+ # @param array [Array] to be flattened
61
+ # @param name [String] name of englobing key
62
+ def flatten_array(array, name)
63
+ if array.empty?
64
+ @result[name] = Formatter.special('empty list')
65
+ elsif array.all?(String)
66
+ @result[name] = array.join("\n")
67
+ elsif array.all?{|i| i.is_a?(Hash) && i.keys.eql?(%w[name])}
68
+ @result[name] = array.map(&:values).join(', ')
69
+ elsif array.all?{|i| i.is_a?(Hash) && i.keys.sort.eql?(%w[name value])}
70
+ flattened_hash(array.each_with_object({}){|i, h|h[i['name']] = i['value']}, name)
71
+ else
72
+ array.each_with_index { |item, index| flatten_any(item, "#{name}.#{index}")}
73
+ end
74
+ nil
75
+ end
76
+
77
+ # Recursive function to flatten a Hash
78
+ # @param hash [Hash] to be flattened
79
+ # @param name [String] name of englobing key
80
+ def flattened_hash(hash, name)
81
+ prefix = name.empty? ? '' : "#{name}."
82
+ hash.each do |k, v|
83
+ flatten_any(v, "#{prefix}#{k}")
84
+ end
85
+ end
86
+ end # class
87
+
10
88
  # Take care of output
11
89
  class Formatter
12
- FIELDS_ALL = 'ALL'
13
- FIELDS_DEFAULT = 'DEF'
90
+ FIELDS_LESS = '-'
14
91
  CSV_RECORD_SEPARATOR = "\n"
15
92
  CSV_FIELD_SEPARATOR = ','
16
93
  # supported output formats
17
94
  DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv].freeze
18
95
  # user output levels
19
96
  DISPLAY_LEVELS = %i[info data error].freeze
20
- CONF_OVERVIEW_KEYS = %w[config parameter value].freeze
21
- KEY_VALUE = %w[key value].freeze
22
97
 
23
- private_constant :FIELDS_ALL, :FIELDS_DEFAULT, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR,
24
- :CONF_OVERVIEW_KEYS, :KEY_VALUE
98
+ private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR
99
+ # prefix to display error messages in user messages (terminal)
100
+ ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
101
+ WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
102
+ HINT_FLASH = 'HINT:'.bg_green.gray.blink.freeze
25
103
 
26
104
  class << self
27
- # special for Aspera on Cloud display node
28
- # {"param" => [{"name"=>"foo","value"=>"bar"}]} will be expanded to {"param.foo" : "bar"}
29
- def flatten_name_value_list(hash)
30
- hash.keys.each do |k| # rubocop:disable Style/HashEachMethods
31
- v = hash[k]
32
- next unless v.is_a?(Array) && v.map(&:class).uniq.eql?([Hash]) && v.map(&:keys).flatten.sort.uniq.eql?(%w[name value])
33
- v.each do |pair|
34
- hash["#{k}.#{pair['name']}"] = pair['value']
105
+ # Highlight special values
106
+ def special(what, use_colors: $stdout.isatty)
107
+ result = "<#{what}>"
108
+ if use_colors
109
+ result = if %w[null empty].any?{|s|what.include?(s)}
110
+ result.dim
111
+ else
112
+ result.reverse_color
35
113
  end
36
- hash.delete(k)
37
114
  end
115
+ return result
38
116
  end
39
117
 
40
- def flatten_config_overview(hash_array_conf)
41
- r = []
42
- hash_array_conf.each do |config, preset|
43
- preset.each do |parameter, value|
44
- r.push(CONF_OVERVIEW_KEYS.zip([config, parameter, SecretHider.deep_remove_secret(value).to_s]).to_h)
45
- end
46
- end
47
- return r
118
+ def all_but(list)
119
+ list = [list] unless list.is_a?(Array)
120
+ return list.map{|i|"#{FIELDS_LESS}#{i}"}.unshift(ExtendedValue::ALL)
48
121
  end
49
122
 
50
- def simple_hash?(h)
51
- !(h.values.any?{|v|[Hash, Array].any?{|c|v.is_a?(c)}})
123
+ def tick(yes)
124
+ result =
125
+ if Environment.use_unicode?
126
+ if yes
127
+ "\u2713"
128
+ else
129
+ "\u2717"
130
+ end
131
+ elsif yes
132
+ 'Y'
133
+ else
134
+ ' '
135
+ end
136
+ return result.green if yes
137
+ return result.red
52
138
  end
53
139
 
54
- # recursive function to modify a hash
55
- # @param source [Hash] to be modified
56
- # @param expand_last [TrueClass,FalseClass] true if last level is not
57
- # @param result [Hash] new hash flattened
58
- # @param prefix [String] true if last level is not
59
- def flattened_object(source, result: {}, prefix: '', expand_last: false)
60
- Log.log.debug{"(#{expand_last})[#{simple_hash?(source)}] -#{source.values}- \n-#{source}-"}
61
- source.each do |k, v|
62
- if v.is_a?(Hash) && !(expand_last && simple_hash?(v))
63
- flattened_object(v, result: result, prefix: prefix + k.to_s + '.', expand_last: expand_last)
64
- else
65
- result[prefix + k.to_s] = v
140
+ def auto_type(data)
141
+ result = {type: :other_struct, data: data}
142
+ result[:type] = :single_object if result[:data].is_a?(Hash)
143
+ if result[:data].is_a?(Array)
144
+ if result[:data].all?(Hash)
145
+ result[:type] = :object_list
66
146
  end
67
147
  end
68
148
  return result
69
149
  end
150
+ end # self
151
+
152
+ # initialize the formatter
153
+ def initialize
154
+ @options = {}
70
155
  end
71
156
 
72
- attr_accessor :option_flat_hash, :option_transpose_single, :option_format, :option_display, :option_fields, :option_table_style,
73
- :option_select, :option_show_secrets
157
+ def option_handler(option_symbol, operation, value=nil)
158
+ case operation
159
+ when :set then @options[option_symbol] = value
160
+ when :get then return @options[option_symbol]
161
+ else raise "internal error: no such operation: #{operation}"
162
+ end
163
+ nil
164
+ end
74
165
 
75
- # adds options but does not parse
76
- def initialize(opt_mgr)
77
- @option_format = :table
78
- @option_display = :info
79
- @option_fields = FIELDS_DEFAULT
80
- @option_select = nil
81
- @option_table_style = ':.:'
82
- @option_flat_hash = true
83
- @option_transpose_single = true
84
- @option_show_secrets = false
85
- opt_mgr.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_format})
86
- opt_mgr.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_display})
87
- opt_mgr.declare(:fields, "Comma separated list of fields, or #{FIELDS_ALL}, or #{FIELDS_DEFAULT}", handler: {o: self, m: :option_fields})
88
- opt_mgr.declare(:select, 'Select only some items in lists: column, value', types: Hash, handler: {o: self, m: :option_select})
89
- opt_mgr.declare(:table_style, 'Table display style', handler: {o: self, m: :option_table_style})
90
- opt_mgr.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_flat_hash})
91
- opt_mgr.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_transpose_single})
92
- opt_mgr.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_show_secrets})
166
+ def declare_options(options)
167
+ options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
168
+ options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
169
+ options.declare(
170
+ :fields, "Comma separated list of: fields, or #{ExtendedValue::ALL}, or #{ExtendedValue::DEF}", handler: {o: self, m: :option_handler},
171
+ types: [String, Array, Regexp, Proc],
172
+ default: ExtendedValue::DEF)
173
+ options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
174
+ options.declare(:table_style, 'Table display style', handler: {o: self, m: :option_handler}, default: ':.:')
175
+ options.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
176
+ options.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
177
+ options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
93
178
  end
94
179
 
95
180
  # main output method
@@ -98,8 +183,8 @@ module Aspera
98
183
  # error: always displayed on stderr
99
184
  def display_message(message_level, message)
100
185
  case message_level
101
- when :data then $stdout.puts(message) unless @option_display.eql?(:error)
102
- when :info then $stdout.puts(message) if @option_display.eql?(:info)
186
+ when :data then $stdout.puts(message) unless @options[:display].eql?(:error)
187
+ when :info then $stdout.puts(message) if @options[:display].eql?(:info)
103
188
  when :error then $stderr.puts(message)
104
189
  else raise "wrong message_level:#{message_level}"
105
190
  end
@@ -110,155 +195,159 @@ module Aspera
110
195
  end
111
196
 
112
197
  def display_item_count(number, total)
198
+ number = number.to_i
199
+ total = total.to_i
200
+ return if total.eql?(0) && number.eql?(0)
113
201
  count_msg = "Items: #{number}/#{total}"
114
- count_msg = count_msg.bg_red unless number.to_i.eql?(total.to_i)
202
+ count_msg = count_msg.bg_red unless number.eql?(total)
115
203
  display_status(count_msg)
116
204
  end
117
205
 
118
- def result_default_fields(results, table_rows_hash_val)
119
- unless results[:fields].nil?
120
- raise "internal error: [fields] must be Array, not #{results[:fields].class}" unless results[:fields].is_a?(Array)
121
- if results[:fields].first.eql?(:all_but) && !table_rows_hash_val.empty?
122
- filter = results[:fields][1..-1]
123
- return table_rows_hash_val.first.keys.reject{|i|filter.include?(i)}
206
+ def all_fields(data)
207
+ data.each_with_object({}){|v, m|v.each_key{|c|m[c] = true}}.keys
208
+ end
209
+
210
+ # this method computes the list of fields to display
211
+ # data: array of hash
212
+ # default: list of fields to display by default (may contain special values)
213
+ def compute_fields(data, default)
214
+ Log.log.debug{"compute_fields: data:#{data.class} default:#{default.class} #{default}"}
215
+ request =
216
+ case @options[:fields]
217
+ when NilClass then [ExtendedValue::DEF]
218
+ when String then @options[:fields].split(',')
219
+ when Array then @options[:fields]
220
+ when Regexp then return all_fields(data).select{|i|i.match(@options[:fields])}
221
+ when Proc then return all_fields(data).select{|i|@options[:fields].call(i)}
222
+ else raise "internal error: option: fields: #{@options[:fields]}"
223
+ end
224
+ result = []
225
+ until request.empty?
226
+ item = request.shift
227
+ removal = false
228
+ if item[0].eql?(FIELDS_LESS)
229
+ removal = true
230
+ item = item[1..-1]
231
+ end
232
+ case item
233
+ when ExtendedValue::ALL
234
+ # get the list of all column names used in all lines, not just first one, as all lines may have different columns
235
+ request.unshift(*all_fields(data))
236
+ when ExtendedValue::DEF
237
+ default = all_fields(data).select{|i|default.call(i)} if default.is_a?(Proc)
238
+ default = all_fields(data) if default.nil?
239
+ request.unshift(*default)
240
+ else
241
+ if removal
242
+ result = result.reject{|i|i.eql?(item)}
243
+ else
244
+ result.push(item)
245
+ end
124
246
  end
125
- return results[:fields]
126
247
  end
127
- return ['empty'] if table_rows_hash_val.empty?
128
- return table_rows_hash_val.first.keys
248
+ return result
129
249
  end
130
250
 
131
- def result_all_fields(_results, table_rows_hash_val)
132
- raise 'internal error: must be array' unless table_rows_hash_val.is_a?(Array)
133
- # get the list of all column names used in all lines, not just first one, as all lines may have different columns
134
- return table_rows_hash_val.each_with_object({}){|v, m|v.each_key{|c|m[c] = true}; }.keys
251
+ # this method displays a table
252
+ # object_array: array of hash
253
+ # fields: list of column names
254
+ def display_table(object_array, fields)
255
+ raise 'internal error: no field specified' if fields.nil?
256
+ case @options[:select]
257
+ when Proc
258
+ object_array.select!{|i|@options[:select].call(i)}
259
+ when Hash
260
+ @options[:select].each{|k, v|object_array.select!{|i|i[k].eql?(v)}}
261
+ end
262
+ if object_array.empty?
263
+ # no display for csv
264
+ display_message(:info, Formatter.special('empty')) if @options[:format].eql?(:table)
265
+ return
266
+ end
267
+ if object_array.length == 1 && fields.length == 1
268
+ display_message(:data, object_array.first[fields.first])
269
+ return
270
+ end
271
+ # Special case if only one row (it could be object_list or single_object)
272
+ if @options[:transpose_single] && object_array.length == 1
273
+ new_columns = %i[key value]
274
+ single = object_array.first
275
+ object_array = fields.map { |i| new_columns.zip([i, single[i]]).to_h }
276
+ fields = new_columns
277
+ end
278
+ Log.log.debug{Log.dump(:object_array, object_array)}
279
+ # convert data to string, and keep only display fields
280
+ final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
281
+ # here : fields : list of column names
282
+ case @options[:format]
283
+ when :table
284
+ style = @options[:table_style].chars
285
+ # display the table !
286
+ display_message(:data, Terminal::Table.new(
287
+ headings: fields,
288
+ rows: final_table_rows,
289
+ border_x: style[0],
290
+ border_y: style[1],
291
+ border_i: style[2]))
292
+ when :csv
293
+ display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
294
+ end
135
295
  end
136
296
 
137
297
  # this method displays the results, especially the table format
138
298
  def display_results(results)
299
+ raise "INTERNAL ERROR, result unsupported key: #{results.keys - %i[type data fields name]}" unless (results.keys - %i[type data fields name]).empty?
300
+ # :type :data :fields :name
139
301
  raise "INTERNAL ERROR, result must be Hash (got: #{results.class}: #{results})" unless results.is_a?(Hash)
140
- raise 'INTERNAL ERROR, result must have type' unless results.key?(:type)
302
+ raise "INTERNAL ERROR, result must have type (#{results})" unless results.key?(:type)
141
303
  raise 'INTERNAL ERROR, result must have data' unless results.key?(:data) || %i[empty nothing].include?(results[:type])
142
- res_data = results[:data]
143
- # for config overview, it is name and value
144
- is_config_overview = res_data.is_a?(Array) && !res_data.empty? && res_data.first.is_a?(Hash) && res_data.first.keys.sort.eql?(CONF_OVERVIEW_KEYS)
145
- SecretHider.deep_remove_secret(res_data, is_name_value: is_config_overview) unless @option_show_secrets || @option_display.eql?(:data)
146
- # comma separated list in string format
147
- user_asked_fields_list_str = @option_fields
148
- case @option_format
304
+ Log.log.debug{"display_results: #{results[:data].class} #{results[:type]}"}
305
+ SecretHider.deep_remove_secret(results[:data]) unless @options[:show_secrets] || @options[:display].eql?(:data)
306
+ case @options[:format]
149
307
  when :text
150
- display_message(:data, res_data.to_s)
308
+ display_message(:data, results[:data].to_s)
151
309
  when :nagios
152
- Nagios.process(res_data)
310
+ Nagios.process(results[:data])
153
311
  when :ruby
154
- display_message(:data, PP.pp(res_data, +''))
312
+ display_message(:data, PP.pp(results[:data], +''))
155
313
  when :json
156
- display_message(:data, JSON.generate(res_data))
314
+ display_message(:data, JSON.generate(results[:data]))
157
315
  when :jsonpp
158
- display_message(:data, JSON.pretty_generate(res_data))
316
+ display_message(:data, JSON.pretty_generate(results[:data]))
159
317
  when :yaml
160
- display_message(:data, res_data.to_yaml)
318
+ display_message(:data, results[:data].to_yaml)
161
319
  when :table, :csv
162
- if !@option_transpose_single && results[:type].eql?(:single_object)
163
- results[:type] = :object_list
164
- res_data = [res_data]
165
- end
166
320
  case results[:type]
167
- when :object_list # goes to table display
168
- raise "internal error: unexpected type: #{res_data.class}, expecting Array" unless res_data.is_a?(Array)
321
+ when :config_over
322
+ display_table(Flattener.new.config_over(results[:data]), CONF_OVERVIEW_KEYS)
323
+ when :object_list, :single_object
324
+ obj_list = results[:data]
325
+ obj_list = [obj_list] if results[:type].eql?(:single_object)
326
+ raise "internal error: expecting Array: got #{obj_list.class}" unless obj_list.is_a?(Array)
327
+ raise 'internal error: expecting Array of Hash' unless obj_list.all?(Hash)
169
328
  # :object_list is an array of hash tables, where key=colum name
170
- table_rows_hash_val = res_data
171
- final_table_columns = nil
172
- if @option_flat_hash
173
- table_rows_hash_val.map!{|obj|self.class.flattened_object(obj, expand_last: results[:option_expand_last])}
174
- end
175
- final_table_columns =
176
- case user_asked_fields_list_str
177
- when FIELDS_DEFAULT then result_default_fields(results, table_rows_hash_val)
178
- when FIELDS_ALL then result_all_fields(results, table_rows_hash_val)
179
- else
180
- if user_asked_fields_list_str.start_with?('+')
181
- result_default_fields(results, table_rows_hash_val).push(*user_asked_fields_list_str.gsub(/^\+/, '').split(','))
182
- elsif user_asked_fields_list_str.start_with?('-')
183
- result_default_fields(results, table_rows_hash_val).reject{|i| user_asked_fields_list_str.gsub(/^-/, '').split(',').include?(i)}
184
- else
185
- user_asked_fields_list_str.split(',')
186
- end
187
- end
188
- when :single_object # goes to table display
189
- # :single_object is a simple hash table (can be nested)
190
- raise "internal error: expecting Hash: got #{res_data.class}: #{res_data}" unless res_data.is_a?(Hash)
191
- final_table_columns = results[:columns] || KEY_VALUE
192
- if @option_flat_hash
193
- res_data = self.class.flattened_object(res_data, expand_last: results[:option_expand_last])
194
- self.class.flatten_name_value_list(res_data)
195
- end
196
- asked_fields =
197
- case user_asked_fields_list_str
198
- when FIELDS_DEFAULT then results[:fields] || res_data.keys
199
- when FIELDS_ALL then res_data.keys
200
- else user_asked_fields_list_str.split(',')
201
- end
202
- table_rows_hash_val = asked_fields.map { |i| { final_table_columns.first => i, final_table_columns.last => res_data[i] } }
203
- # if only one row, and columns are key/value, then display the value only
204
- if table_rows_hash_val.length == 1 && final_table_columns.eql?(KEY_VALUE)
205
- display_message(:data, res_data.values.first)
206
- return
207
- end
208
- when :value_list # goes to table display
329
+ obj_list = obj_list.map{|obj|Flattener.new.flatten(obj)} if @options[:flat_hash]
330
+ display_table(obj_list, compute_fields(obj_list, results[:fields]))
331
+ when :value_list
209
332
  # :value_list is a simple array of values, name of column provided in the :name
210
- final_table_columns = [results[:name]]
211
- table_rows_hash_val = res_data.map { |i| { results[:name] => i } }
333
+ display_table(results[:data].map { |i| { results[:name] => i } }, [results[:name]])
212
334
  when :empty # no table
213
- display_message(:info, 'empty')
335
+ display_message(:info, Formatter.special('empty'))
214
336
  return
215
337
  when :nothing # no result expected
216
338
  Log.log.debug('no result expected')
217
- return
218
339
  when :status # no table
219
340
  # :status displays a simple message
220
- display_message(:info, res_data)
221
- return
341
+ display_message(:info, results[:data])
222
342
  when :text # no table
223
343
  # :status displays a simple message
224
- display_message(:data, res_data)
225
- return
344
+ display_message(:data, results[:data])
226
345
  when :other_struct # no table
227
346
  # :other_struct is any other type of structure
228
- display_message(:data, PP.pp(res_data, +''))
229
- return
347
+ display_message(:data, PP.pp(results[:data], +''))
230
348
  else
231
349
  raise "unknown data type: #{results[:type]}"
232
350
  end
233
- # here we expect: table_rows_hash_val and final_table_columns
234
- raise 'no field specified' if final_table_columns.nil?
235
- if table_rows_hash_val.empty?
236
- display_message(:info, 'empty'.gray) if @option_format.eql?(:table)
237
- return
238
- end
239
- # convert to string with special function. here table_rows_hash_val is an array of hash
240
- table_rows_hash_val = results[:textify].call(table_rows_hash_val) if results.key?(:textify)
241
- unless @option_select.nil? || (@option_select.respond_to?(:empty?) && @option_select.empty?)
242
- raise CliBadArgument, "expecting hash for select, have #{@option_select.class}: #{@option_select}" unless @option_select.is_a?(Hash)
243
- @option_select.each{|k, v|table_rows_hash_val.select!{|i|i[k].eql?(v)}}
244
- end
245
- # convert data to string, and keep only display fields
246
- final_table_rows = table_rows_hash_val.map { |r| final_table_columns.map { |c| r[c].to_s } }
247
- # here : final_table_columns : list of column names
248
- # here: final_table_rows : array of list of value
249
- case @option_format
250
- when :table
251
- style = @option_table_style.chars
252
- # display the table !
253
- display_message(:data, Terminal::Table.new(
254
- headings: final_table_columns,
255
- rows: final_table_rows,
256
- border_x: style[0],
257
- border_y: style[1],
258
- border_i: style[2]))
259
- when :csv
260
- display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
261
- end
262
351
  end
263
352
  end
264
353
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/fasp/error'
4
+ require 'aspera/rest'
5
+ require 'net/ssh'
6
+ require 'openssl'
7
+
8
+ module Aspera
9
+ module Cli
10
+ # Provide hints on errors
11
+ class Hints
12
+ # Well know issues that users may get
13
+ ERROR_HINTS = [
14
+ {
15
+ exception: Fasp::Error,
16
+ match: 'Remote host is not who we expected',
17
+ remediation: [
18
+ 'For this specific error, refer to:',
19
+ "#{SRC_URL}#error-remote-host-is-not-who-we-expected",
20
+ 'Add this to arguments:',
21
+ %q{--ts=@json:'{"sshfp":null}'"}
22
+ ]
23
+ },
24
+ {
25
+ exception: Aspera::RestCallError,
26
+ match: /Signature has expired/,
27
+ remediation: [
28
+ 'There is too much time difference between your computer and the server',
29
+ 'Check your local time: is time synchronization enabled?'
30
+ ]
31
+ },
32
+ {
33
+ exception: OpenSSL::SSL::SSLError,
34
+ match: /(does not match the server certificate|certificate verify failed)/,
35
+ remediation: [
36
+ 'You can ignore SSL errors with option:',
37
+ '--insecure=yes'
38
+ ]
39
+ },
40
+ {
41
+ exception: OpenSSL::PKey::RSAError,
42
+ match: /Neither PUB key nor PRIV key/,
43
+ remediation: [
44
+ 'option: private_key expects a key PEM value, not path to file',
45
+ 'if you provide a path: prefix with @file:',
46
+ 'e.g. --private-key=@file:/path/to/key.pem'
47
+ ]
48
+ }
49
+ ]
50
+
51
+ private_constant :ERROR_HINTS
52
+
53
+ class << self
54
+ def hint_for(error, formatter)
55
+ ERROR_HINTS.each do |hint|
56
+ next unless error.is_a?(hint[:exception])
57
+ message = error.message
58
+ matches = hint[:match]
59
+ matches = [matches] unless matches.is_a?(Array)
60
+ matches.each do |m|
61
+ case m
62
+ when String
63
+ next unless message.eql?(m)
64
+ when Regexp
65
+ next unless message.match?(m)
66
+ else
67
+ Log.log.warn("Internal error: hint is a #{m.class}")
68
+ next
69
+ end
70
+ remediation = hint[:remediation]
71
+ remediation = [remediation] unless remediation.is_a?(Array)
72
+ remediation.each{|r|formatter.display_message(:error, "#{Formatter::HINT_FLASH} #{r}")}
73
+ break
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end