aspera-cli 4.14.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
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