aspera-cli 4.24.1 → 4.25.0.pre

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 +1064 -745
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +1281 -720
  6. data/bin/ascli +20 -1
  7. data/bin/asession +23 -27
  8. data/lib/aspera/agent/base.rb +10 -21
  9. data/lib/aspera/agent/connect.rb +2 -3
  10. data/lib/aspera/agent/desktop.rb +2 -2
  11. data/lib/aspera/agent/direct.rb +49 -32
  12. data/lib/aspera/agent/factory.rb +31 -0
  13. data/lib/aspera/api/aoc.rb +134 -76
  14. data/lib/aspera/api/cos_node.rb +3 -2
  15. data/lib/aspera/api/faspex.rb +213 -0
  16. data/lib/aspera/api/node.rb +107 -94
  17. data/lib/aspera/ascmd.rb +1 -2
  18. data/lib/aspera/ascp/installation.rb +73 -58
  19. data/lib/aspera/ascp/management.rb +119 -23
  20. data/lib/aspera/assert.rb +39 -11
  21. data/lib/aspera/cli/error.rb +4 -2
  22. data/lib/aspera/cli/extended_value.rb +91 -67
  23. data/lib/aspera/cli/formatter.rb +62 -27
  24. data/lib/aspera/cli/hints.rb +8 -0
  25. data/lib/aspera/cli/info.rb +4 -4
  26. data/lib/aspera/cli/main.rb +76 -84
  27. data/lib/aspera/cli/manager.rb +352 -248
  28. data/lib/aspera/cli/plugins/alee.rb +5 -4
  29. data/lib/aspera/cli/plugins/aoc.rb +175 -195
  30. data/lib/aspera/cli/plugins/ats.rb +4 -4
  31. data/lib/aspera/cli/plugins/base.rb +343 -0
  32. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  33. data/lib/aspera/cli/plugins/config.rb +283 -269
  34. data/lib/aspera/cli/plugins/console.rb +27 -22
  35. data/lib/aspera/cli/plugins/cos.rb +3 -3
  36. data/lib/aspera/cli/plugins/factory.rb +78 -0
  37. data/lib/aspera/cli/plugins/faspex.rb +49 -46
  38. data/lib/aspera/cli/plugins/faspex5.rb +113 -225
  39. data/lib/aspera/cli/plugins/faspio.rb +19 -18
  40. data/lib/aspera/cli/plugins/httpgw.rb +14 -13
  41. data/lib/aspera/cli/plugins/node.rb +162 -149
  42. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  43. data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
  44. data/lib/aspera/cli/plugins/preview.rb +30 -50
  45. data/lib/aspera/cli/plugins/server.rb +21 -21
  46. data/lib/aspera/cli/plugins/shares.rb +45 -47
  47. data/lib/aspera/cli/sync_actions.rb +50 -39
  48. data/lib/aspera/cli/transfer_agent.rb +35 -49
  49. data/lib/aspera/cli/transfer_progress.rb +6 -6
  50. data/lib/aspera/cli/version.rb +3 -3
  51. data/lib/aspera/cli/wizard.rb +70 -55
  52. data/lib/aspera/colors.rb +6 -0
  53. data/lib/aspera/command_line_builder.rb +59 -61
  54. data/lib/aspera/command_line_converter.rb +2 -1
  55. data/lib/aspera/coverage.rb +2 -2
  56. data/lib/aspera/data_repository.rb +1 -1
  57. data/lib/aspera/environment.rb +51 -41
  58. data/lib/aspera/faspex_gw.rb +7 -5
  59. data/lib/aspera/faspex_postproc.rb +1 -1
  60. data/lib/aspera/keychain/factory.rb +1 -2
  61. data/lib/aspera/keychain/macos_security.rb +1 -1
  62. data/lib/aspera/log.rb +37 -9
  63. data/lib/aspera/markdown.rb +31 -0
  64. data/lib/aspera/nagios.rb +7 -6
  65. data/lib/aspera/oauth/base.rb +25 -28
  66. data/lib/aspera/oauth/factory.rb +9 -9
  67. data/lib/aspera/oauth/url_json.rb +2 -1
  68. data/lib/aspera/oauth/web.rb +2 -2
  69. data/lib/aspera/preview/file_types.rb +23 -37
  70. data/lib/aspera/products/connect.rb +7 -6
  71. data/lib/aspera/products/desktop.rb +1 -4
  72. data/lib/aspera/products/other.rb +9 -1
  73. data/lib/aspera/products/transferd.rb +0 -1
  74. data/lib/aspera/rest.rb +168 -113
  75. data/lib/aspera/rest_error_analyzer.rb +4 -4
  76. data/lib/aspera/ssh.rb +7 -4
  77. data/lib/aspera/ssl.rb +41 -0
  78. data/lib/aspera/sync/args.schema.yaml +46 -3
  79. data/lib/aspera/sync/conf.schema.yaml +307 -123
  80. data/lib/aspera/sync/database.rb +2 -1
  81. data/lib/aspera/sync/operations.rb +135 -79
  82. data/lib/aspera/temp_file_manager.rb +17 -5
  83. data/lib/aspera/transfer/error.rb +16 -7
  84. data/lib/aspera/transfer/parameters.rb +35 -22
  85. data/lib/aspera/transfer/resumer.rb +74 -0
  86. data/lib/aspera/transfer/spec.rb +5 -5
  87. data/lib/aspera/transfer/spec.schema.yaml +170 -59
  88. data/lib/aspera/transfer/spec_doc.rb +49 -43
  89. data/lib/aspera/uri_reader.rb +2 -2
  90. data/lib/aspera/web_auth.rb +6 -6
  91. data/lib/transferd_pb.rb +2 -2
  92. data.tar.gz.sig +0 -0
  93. metadata +26 -11
  94. metadata.gz.sig +0 -0
  95. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  96. data/lib/aspera/cli/plugin.rb +0 -333
  97. data/lib/aspera/cli/plugin_factory.rb +0 -81
  98. data/lib/aspera/resumer.rb +0 -77
  99. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/extended_value'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ module Plugins
9
+ # Base class for command plugins
10
+ class Base
11
+ # Operations without id (create list)
12
+ GLOBAL_OPS = %i[create list].freeze
13
+ # Operations with id (modify delete show)
14
+ INSTANCE_OPS = %i[modify delete show].freeze
15
+ # All standard operations (create list modify delete show)
16
+ ALL_OPS = (GLOBAL_OPS + INSTANCE_OPS).freeze
17
+ # Special query parameter: `max`: max number of items for list command
18
+ MAX_ITEMS = 'max'
19
+ # Special query parameter: `pmax`: max number of pages for list command
20
+ MAX_PAGES = 'pmax'
21
+
22
+ class << self
23
+ def declare_options(options)
24
+ options.declare(:query, 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass])
25
+ options.declare(:property, 'Name of property to set (modify operation)')
26
+ options.declare(:bulk, 'Bulk operation (only some)', allowed: Allowed::TYPES_BOOLEAN, default: false)
27
+ options.declare(:bfail, 'Bulk operation error handling', allowed: Allowed::TYPES_BOOLEAN, default: true)
28
+ end
29
+
30
+ # @return [Hash,NilClass] `{field:,value:}` if identifier is a percent selector, else `nil`
31
+ def percent_selector(identifier)
32
+ Aspera.assert_type(identifier, String)
33
+ if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
34
+ return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
35
+ end
36
+ return
37
+ end
38
+ end
39
+
40
+ def initialize(context:)
41
+ # Check presence in descendant of mandatory method and constant
42
+ Aspera.assert(respond_to?(:execute_action), type: InternalError){"Missing method 'execute_action' in #{self.class}"}
43
+ Aspera.assert(self.class.constants.include?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
44
+ @context = context
45
+ add_manual_header if @context.man_header
46
+ end
47
+
48
+ # Global objects
49
+ attr_reader :context
50
+
51
+ def options; @context.options; end
52
+ def transfer; @context.transfer; end
53
+ def config; @context.config; end
54
+ def formatter; @context.formatter; end
55
+ def persistency; @context.persistency; end
56
+
57
+ def add_manual_header(has_options = true)
58
+ # Manual header for all plugins
59
+ options.parser.separator('')
60
+ options.parser.separator("COMMAND: #{self.class.name.split('::').last.downcase}")
61
+ options.parser.separator("SUBCOMMANDS: #{self.class.const_get(:ACTIONS).map(&:to_s).sort.join(' ')}")
62
+ options.parser.separator('OPTIONS:') if has_options
63
+ end
64
+
65
+ # Resource identifier as positional parameter
66
+ #
67
+ # @param description [String] description of the identifier
68
+ # @param block [Proc] block to search for identifier based on attribute value
69
+ # @return [String, Array] identifier or list of ids
70
+ def instance_identifier(description: 'identifier', &block)
71
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
72
+ # Can be an Array
73
+ if res_id.is_a?(String) && (m = Base.percent_selector(res_id))
74
+ Aspera.assert(block, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
75
+ res_id = yield(m[:field], m[:value])
76
+ end
77
+ return res_id
78
+ end
79
+
80
+ # For create and delete operations: execute one action or multiple if bulk is yes
81
+ # @param command [Symbol] operation: :create, :delete, ...
82
+ # @param descr [String] description of the value
83
+ # @param values [Object] the value(s), or the type of value to get from user
84
+ # @param id_result [String] key in result hash to use as identifier
85
+ # @param fields [Array] fields to display
86
+ # @param &block [Proc] block to execute for each value
87
+ def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default)
88
+ Aspera.assert(block_given?){'missing block'}
89
+ is_bulk = options.get_option(:bulk)
90
+ case values
91
+ when :identifier
92
+ values = instance_identifier(description: descr)
93
+ when Class
94
+ values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk)
95
+ end
96
+ # If not bulk, there is a single value
97
+ params = is_bulk ? values : [values]
98
+ Log.log.warn('Empty list given for bulk operation') if params.empty?
99
+ Log.dump(:bulk_operation, params)
100
+ result_list = []
101
+ params.each do |param|
102
+ # Init for delete
103
+ result = {id_result => param}
104
+ begin
105
+ # Execute custom code
106
+ res = yield(param)
107
+ # If block returns a hash, let's use this (create)
108
+ result = res if res.is_a?(Hash)
109
+ # TODO: remove when faspio gw api fixes this
110
+ result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
111
+ # Create -> created
112
+ result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
113
+ rescue StandardError => e
114
+ raise e if options.get_option(:bfail)
115
+ result['status'] = e.to_s
116
+ end
117
+ result_list.push(result)
118
+ end
119
+ display_fields = [id_result, 'status']
120
+ if is_bulk
121
+ return Main.result_object_list(result_list, fields: display_fields)
122
+ else
123
+ display_fields = fields unless fields.eql?(:default)
124
+ return Main.result_single_object(result_list.first, fields: display_fields)
125
+ end
126
+ end
127
+
128
+ # Operations: Create, Delete, Show, List, Modify
129
+ # @param api [Rest] api to use
130
+ # @param entity [String] sub path in URL to resource relative to base url
131
+ # @param command [Symbol] command to execute: create show list modify delete
132
+ # @param display_fields [Array] fields to display by default
133
+ # @param items_key [String] result is in a sub key of the json
134
+ # @param delete_style [String] if set, the delete operation by array in payload
135
+ # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
136
+ # @param is_singleton [Boolean] if true, entity is the full path to the resource
137
+ # @param tclo [Bool] if set, :list use paging with total_count, limit, offset
138
+ # @param block [Proc] block to search for identifier based on attribute value
139
+ # @return result suitable for CLI result
140
+ def entity_execute(
141
+ api:,
142
+ entity:,
143
+ command: nil,
144
+ display_fields: nil,
145
+ items_key: nil,
146
+ delete_style: nil,
147
+ id_as_arg: false,
148
+ is_singleton: false,
149
+ list_query: nil,
150
+ tclo: false,
151
+ &block
152
+ )
153
+ command = options.get_next_command(ALL_OPS) if command.nil?
154
+ if is_singleton
155
+ one_res_path = entity
156
+ elsif INSTANCE_OPS.include?(command)
157
+ one_res_id = instance_identifier(&block)
158
+ one_res_path = "#{entity}/#{one_res_id}"
159
+ one_res_path = "#{entity}?#{id_as_arg}=#{one_res_id}" if id_as_arg
160
+ end
161
+
162
+ case command
163
+ when :create
164
+ raise BadArgument, 'cannot create singleton' if is_singleton
165
+ return do_bulk_operation(command: command, descr: 'data', fields: display_fields) do |params|
166
+ api.create(entity, params)
167
+ end
168
+ when :delete
169
+ raise BadArgument, 'cannot delete singleton' if is_singleton
170
+ if !delete_style.nil?
171
+ one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
172
+ Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
173
+ api.delete(
174
+ entity,
175
+ nil,
176
+ content_type: Rest::MIME_JSON,
177
+ body: {delete_style => one_res_id}
178
+ )
179
+ return Main.result_status('deleted')
180
+ end
181
+ return do_bulk_operation(command: command, values: one_res_id) do |one_id|
182
+ api.delete("#{entity}/#{one_id}", query_read_delete)
183
+ {'id' => one_id}
184
+ end
185
+ when :show
186
+ return Main.result_single_object(api.read(one_res_path), fields: display_fields)
187
+ when :list
188
+ if tclo
189
+ data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
190
+ return Main.result_object_list(data, total: total, fields: display_fields)
191
+ end
192
+ data, http = api.read(entity, query_read_delete, ret: :both)
193
+ return Main.result_empty if http.code == '204'
194
+ # TODO: not generic : which application is this for ?
195
+ if http['Content-Type'].start_with?('application/vnd.api+json')
196
+ Log.log.debug('is vnd.api')
197
+ data = data[entity]
198
+ end
199
+ data = data[items_key] if items_key
200
+ case data
201
+ when Hash
202
+ return Main.result_single_object(data, fields: display_fields)
203
+ when Array
204
+ return Main.result_object_list(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
205
+ return Main.result_value_list(data)
206
+ else
207
+ raise "An error occurred: unexpected result type for list: #{data.class}"
208
+ end
209
+ when :modify
210
+ parameters = value_create_modify(command: command)
211
+ property = options.get_option(:property)
212
+ parameters = {property => parameters} unless property.nil?
213
+ api.update(one_res_path, parameters)
214
+ return Main.result_status('modified')
215
+ else
216
+ raise "unknown action: #{command}"
217
+ end
218
+ end
219
+
220
+ # Query parameters in URL suitable for REST: list/GET and delete/DELETE
221
+ def query_read_delete(default: nil)
222
+ # Dup default, as it could be frozen
223
+ query = options.get_option(:query) || default.dup
224
+ Log.log.debug{"query_read_delete=#{query}".bg_red}
225
+ begin
226
+ # Check it is suitable
227
+ URI.encode_www_form(query) unless query.nil?
228
+ rescue StandardError => e
229
+ raise Cli::BadArgument, "Query must be an extended value (Hash, Array) which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
230
+ end
231
+ return query
232
+ end
233
+
234
+ # Retrieves an extended value from command line, used for creation or modification of entities
235
+ # @param command [Symbol] command name for error message
236
+ # @param type [Class] expected type of value, either a Class, an Array of Class
237
+ # @param bulk [Boolean] if true, value must be an Array of <type>
238
+ # @param default [Object] default value if not provided
239
+ def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
240
+ value = options.get_next_argument(
241
+ "parameters for #{command}#{" (#{description})" unless description.nil?}", mandatory: default.nil?,
242
+ validation: bulk ? Array : type
243
+ )
244
+ value = default if value.nil?
245
+ unless type.nil?
246
+ type = [type] unless type.is_a?(Array)
247
+ Aspera.assert_array_all(type, Class){'check types'}
248
+ if bulk
249
+ Aspera.assert_type(value, Array, type: Cli::BadArgument)
250
+ value.each do |v|
251
+ Aspera.assert_values(v.class, type, type: Cli::BadArgument)
252
+ end
253
+ else
254
+ Aspera.assert_values(value.class, type, type: Cli::BadArgument)
255
+ end
256
+ end
257
+ return value
258
+ end
259
+
260
+ # Get a (full or partial) list of all entities of a given type with query: offset/limit
261
+ # @param api [Rest] API object
262
+ # @param entity [String,Symbol] API endpoint of entity to list
263
+ # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
264
+ # @param query [Hash,nil] Additional query parameters
265
+ # @return [Array] items, total_count
266
+ def list_entities_limit_offset_total_count(
267
+ api:,
268
+ entity:,
269
+ items_key: nil,
270
+ query: nil
271
+ )
272
+ entity = entity.to_s if entity.is_a?(Symbol)
273
+ items_key = entity.split('/').last if items_key.nil?
274
+ query = {} if query.nil?
275
+ Aspera.assert_type(entity, String)
276
+ Aspera.assert_type(items_key, String)
277
+ Aspera.assert_type(query, Hash)
278
+ Log.log.debug{"list_entities t=#{entity} k=#{items_key} q=#{query}"}
279
+ result = []
280
+ offset = 0
281
+ max_items = query.delete(MAX_ITEMS)
282
+ remain_pages = query.delete(MAX_PAGES)
283
+ # Merge default parameters, by default 100 per page
284
+ query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
285
+ total_count = nil
286
+ loop do
287
+ query['offset'] = offset
288
+ page_result = api.read(entity, query)
289
+ Aspera.assert_type(page_result[items_key], Array)
290
+ result.concat(page_result[items_key])
291
+ # Reach the limit set by user ?
292
+ if !max_items.nil? && (result.length >= max_items)
293
+ result = result.slice(0, max_items)
294
+ break
295
+ end
296
+ total_count ||= page_result['total_count']
297
+ break if result.length >= total_count
298
+ remain_pages -= 1 unless remain_pages.nil?
299
+ break if remain_pages == 0
300
+ offset += page_result[items_key].length
301
+ formatter.long_operation_running
302
+ end
303
+ formatter.long_operation_terminated
304
+ return result, total_count
305
+ end
306
+
307
+ # Lookup an entity id from its name.
308
+ # Uses query `q` if `query` is `:default` and `field` is `name`.
309
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
310
+ # @param value [String] Value to lookup
311
+ # @param field [String] Field to match, by default it is `'name'`
312
+ # @param items_key [String] Key in the result to get the list of items (override entity)
313
+ # @param query [Hash] Additional query parameters (Default: `:default`)
314
+ def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
315
+ if query.eql?(:default)
316
+ Aspera.assert(field.eql?('name')){'Default query is on name only'}
317
+ query = {'q'=> value}
318
+ end
319
+ lookup_entity_generic(entity: entity, field: field, value: value){list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first}
320
+ end
321
+
322
+ # Lookup entity by field and value. Extract single result from list of result returned by block.
323
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
324
+ # @param value [String] Value to lookup
325
+ # @param field [String] Field to match, by default it is `'name'`
326
+ # @param block [Proc] Get list of entity matching query.
327
+ def lookup_entity_generic(entity:, value:, field: 'name', &block)
328
+ Aspera.assert(block_given?)
329
+ found = yield
330
+ Aspera.assert_array_all(found, Hash)
331
+ found = found.select{ |i| i[field].eql?(value)}
332
+ return found.first if found.length.eql?(1)
333
+ raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
334
+ end
335
+
336
+ PER_PAGE_DEFAULT = 1000
337
+ # Percent selector: select by this field for this value
338
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
339
+ private_constant :PER_PAGE_DEFAULT, :REGEX_LOOKUP_ID_BY_FIELD
340
+ end
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest'
4
+ require 'aspera/cli/plugins/base'
5
+
6
+ module Aspera
7
+ module Cli
8
+ module Plugins
9
+ # base class for applications supporting basic authentication
10
+ class BasicAuth < Base
11
+ class << self
12
+ def declare_options(options)
13
+ options.declare(:url, 'URL of application, e.g. https://app.example.com/aspera/app')
14
+ options.declare(:username, "User's identifier")
15
+ options.declare(:password, "User's password")
16
+ options.parse_options!
17
+ end
18
+ end
19
+
20
+ def initialize(context:, basic_options: true)
21
+ super(context: context)
22
+ BasicAuth.declare_options(options) if basic_options
23
+ end
24
+
25
+ # returns a Rest object with basic auth
26
+ def basic_auth_params(subpath = nil)
27
+ api_url = options.get_option(:url, mandatory: true)
28
+ api_url = "#{api_url}/#{subpath}" unless subpath.nil?
29
+ return {
30
+ base_url: api_url,
31
+ auth: {
32
+ type: :basic,
33
+ username: options.get_option(:username, mandatory: true),
34
+ password: options.get_option(:password, mandatory: true)
35
+ }
36
+ }
37
+ end
38
+
39
+ def basic_auth_api(subpath = nil)
40
+ return Rest.new(**basic_auth_params(subpath))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end