aspera-cli 4.26.0 → 4.26.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +17 -3
  4. data/lib/aspera/api/aoc.rb +2 -1
  5. data/lib/aspera/api/node.rb +2 -2
  6. data/lib/aspera/ascp/installation.rb +7 -4
  7. data/lib/aspera/assert.rb +17 -13
  8. data/lib/aspera/cli/extended_value.rb +6 -2
  9. data/lib/aspera/cli/formatter.rb +65 -60
  10. data/lib/aspera/cli/main.rb +69 -10
  11. data/lib/aspera/cli/manager.rb +130 -76
  12. data/lib/aspera/cli/options.schema.yaml +82 -0
  13. data/lib/aspera/cli/plugins/aoc.rb +36 -11
  14. data/lib/aspera/cli/plugins/base.rb +46 -37
  15. data/lib/aspera/cli/plugins/config.rb +9 -9
  16. data/lib/aspera/cli/plugins/faspex.rb +1 -1
  17. data/lib/aspera/cli/plugins/faspex5.rb +4 -5
  18. data/lib/aspera/cli/plugins/node.rb +1 -1
  19. data/lib/aspera/cli/sync_actions.rb +1 -1
  20. data/lib/aspera/cli/transfer_agent.rb +17 -15
  21. data/lib/aspera/cli/version.rb +1 -1
  22. data/lib/aspera/command_line_builder.rb +22 -18
  23. data/lib/aspera/environment.rb +3 -3
  24. data/lib/aspera/formatter_interface.rb +14 -0
  25. data/lib/aspera/hash_ext.rb +6 -0
  26. data/lib/aspera/log.rb +4 -3
  27. data/lib/aspera/markdown.rb +4 -1
  28. data/lib/aspera/oauth/factory.rb +1 -1
  29. data/lib/aspera/proxy_auto_config.rb +3 -0
  30. data/lib/aspera/rest.rb +1 -1
  31. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
  32. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
  33. data/lib/aspera/schema/documentation.rb +107 -0
  34. data/lib/aspera/schema/reader.rb +75 -0
  35. data/lib/aspera/schema/registry.rb +63 -0
  36. data/lib/aspera/sync/conf.schema.yaml +0 -26
  37. data/lib/aspera/sync/operations.rb +9 -5
  38. data/lib/aspera/transfer/faux_file.rb +1 -1
  39. data/lib/aspera/transfer/resumer.rb +1 -1
  40. data/lib/aspera/transfer/spec.rb +3 -3
  41. data/lib/aspera/transfer/spec.schema.yaml +1 -1
  42. data/lib/aspera/uri_reader.rb +1 -1
  43. data/lib/aspera/yaml.rb +4 -2
  44. data.tar.gz.sig +0 -0
  45. metadata +9 -3
  46. metadata.gz.sig +0 -0
  47. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -304,7 +304,7 @@ module Aspera
304
304
  # @param fields fields to display
305
305
  # @param base_query a query applied always
306
306
  # @param default_query default query unless overridden by user
307
- # @param &block (Optional) calls block with user's or default query
307
+ # @yieldparam query [Hash] The user's or default query for modification
308
308
  def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {})
309
309
  Aspera.assert_type(base_query, Hash)
310
310
  Aspera.assert_type(default_query, Hash)
@@ -402,34 +402,50 @@ module Aspera
402
402
  # Execute an action on admin resources
403
403
  # @param resource_type [Symbol] One of ADMIN_OBJECTS
404
404
  def execute_resource_action(resource_type)
405
+ # Set to `true` is resource creation requires a workspace id
405
406
  require_workspace_id = false
407
+ # Default fields to display
406
408
  list_default_fields = %w[id name]
409
+ # Default query for `list` action
407
410
  list_default_query = {}
411
+ # In result of create, what is the field that contains the id of the created resource
412
+ id_result = 'id'
408
413
  supported_operations = Operations::ALL
414
+ # API path
409
415
  resource_class_path = "#{resource_type}s"
416
+ # path in openapi where post is located to get creation schema
417
+ create_schema_path = resource_class_path
410
418
  case resource_type
411
419
  when :client
412
420
  supported_operations += %i[set_pub_key]
421
+ # schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{resource_class_path}.post")
413
422
  when :client_access_key
414
423
  resource_class_path = "admin/#{resource_type}s"
415
424
  when :client_registration_token
416
425
  resource_class_path = "admin/#{resource_type}s"
417
- list_default_fields = %w[id value data.client_subject_scopes created_at]
426
+ list_default_fields = %w[id value data.client_subject_scopes data.name created_at]
427
+ id_result = 'token'
418
428
  when :contact
419
429
  list_default_fields = %w[source_type source_id name email]
420
430
  # list_default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
421
431
  when :dropbox
422
432
  require_workspace_id = true
423
433
  resource_class_path = "#{resource_type}es"
434
+ create_schema_path = resource_class_path
435
+ when :group, :saml_configuration
436
+ create_schema_path = nil
424
437
  when :group_membership
425
438
  list_default_fields = %w[id group_id member_type member_id]
439
+ create_schema_path = nil
426
440
  when :kms_profile
427
441
  resource_class_path = "integrations/#{resource_type}s"
442
+ create_schema_path = nil
428
443
  when :node
429
444
  list_default_fields = %w[id name host access_key]
430
445
  supported_operations += %i[do bearer_token]
431
446
  when :operation
432
- list_default_fields = nil
447
+ list_default_fields = %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id]
448
+ supported_operations = %i[list show modify]
433
449
  when :organization, :self
434
450
  supported_operations = Operations::SINGLETON
435
451
  resource_instance_path = resource_class_path = resource_type
@@ -443,6 +459,8 @@ module Aspera
443
459
  when :workspace_membership
444
460
  list_default_fields = %w[id workspace_id member_type member_id]
445
461
  end
462
+ # Default location of creation payload in schema
463
+ schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{create_schema_path}.post") if create_schema_path && supported_operations.include?(:create)
446
464
  command = options.get_next_command(supported_operations)
447
465
  # Require identifier for non global commands
448
466
  if (supported_operations != Operations::SINGLETON) && !Operations::GLOBAL.include?(command)
@@ -451,12 +469,10 @@ module Aspera
451
469
  end
452
470
  case command
453
471
  when :create
454
- id_result = 'id'
455
- id_result = 'token' if resource_class_path.eql?('admin/client_registration_tokens')
456
472
  # TODO: report inconsistency: creation url is !=, and does not return id.
457
473
  resource_class_path = 'admin/client_registration/token' if resource_class_path.eql?('admin/client_registration_tokens')
458
474
  workspace_id = aoc_api.workspace_info[:id] if require_workspace_id
459
- return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result) do |params|
475
+ return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result, schema: schema_create_modify) do |params|
460
476
  params['workspace_id'] = workspace_id if require_workspace_id && workspace_id && !params.key?('workspace_id')
461
477
  aoc_api.create(resource_class_path, params)
462
478
  end
@@ -466,7 +482,7 @@ module Aspera
466
482
  object = aoc_api.read(resource_instance_path, query_read_delete)
467
483
  return Main.result_single_object(object, fields: Formatter.all_but('certificate'))
468
484
  when :modify
469
- changes = options.get_next_argument('properties', validation: Hash)
485
+ changes = options.get_next_argument('properties', validation: Hash, schema: schema_create_modify)
470
486
  return do_bulk_operation(command: command, values: res_id) do |one_id|
471
487
  aoc_api.update("#{resource_class_path}/#{one_id}", changes)
472
488
  {'id' => one_id}
@@ -503,12 +519,18 @@ module Aspera
503
519
  query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
504
520
  shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
505
521
  # inside a workspace
506
- command_shared = options.get_next_command(%i[list member])
522
+ command_shared = options.get_next_command(%i[list node member])
507
523
  case command_shared
508
524
  when :list
509
525
  return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
526
+ when :node
527
+ shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
528
+ shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
529
+ Aspera.assert(shared_folder)
530
+ command_repo = options.get_next_command(FILES_COMMANDS)
531
+ return execute_nodegen4_command(command_repo, shared_folder['node_id'], file_id: shared_folder['file_id'], scope: Api::Node::Scope::ADMIN)
510
532
  when :member
511
- shared_folder_id = options.instance_identifier
533
+ shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
512
534
  shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
513
535
  Aspera.assert(shared_folder)
514
536
  command_shared_member = options.get_next_command(%i[list])
@@ -825,7 +847,10 @@ module Aspera
825
847
  # - a resource id, e.g. `scQ7uXPbvQ`
826
848
  # - a short URL path, e.g. `dxyRpT9`
827
849
  # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
828
- # @param &perm_block [Proc] Optional: create/modify/delete permissions on node
850
+ # @param shared_data [Hash] Shared data for the short link
851
+ # @yieldparam operation [Symbol] Operation type (:create, :update, :delete)
852
+ # @yieldparam resource_id [String] Resource ID for permission management
853
+ # @yieldparam access_levels [Object] Access levels for permissions
829
854
  def short_link_command(**shared_data, &perm_block)
830
855
  link_type = options.get_next_argument('link access (public or private)', accept_list: %i[public private])
831
856
  if shared_data.keys.sort == %i[dropbox_id name]
@@ -1043,7 +1068,7 @@ module Aspera
1043
1068
  return short_link_command(dropbox_id: get_resource_id_from_args('dropboxes'), name: '')
1044
1069
  end
1045
1070
  when :send
1046
- package_data = value_create_modify(command: package_command)
1071
+ package_data = value_create_modify(command: package_command, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post'))
1047
1072
  new_user_option = options.get_option(:new_user_option)
1048
1073
  option_validate = options.get_option(:validate_metadata)
1049
1074
  # Works for both normal user auth and link auth.
@@ -37,15 +37,15 @@ module Aspera
37
37
  # Global objects
38
38
  attr_reader :context
39
39
 
40
- # @return [Manager]
40
+ # @return [Aspera::Cli::Manager]
41
41
  def options; @context.options; end
42
- # @return [TransferAgent]
42
+ # @return [Aspera::Cli::TransferAgent]
43
43
  def transfer; @context.transfer; end
44
- # @return [Config]
44
+ # @return [Aspera::Cli::Plugins::Config]
45
45
  def config; @context.config; end
46
- # @return [Formatter]
46
+ # @return [Aspera::Cli::Formatter]
47
47
  def formatter; @context.formatter; end
48
- # @return [PersistencyFolder]
48
+ # @return [Aspera::PersistencyFolder]
49
49
  def persistency; @context.persistency; end
50
50
 
51
51
  def add_manual_header(has_options = true)
@@ -57,20 +57,23 @@ module Aspera
57
57
  end
58
58
 
59
59
  # For create and delete operations: execute one action or multiple if bulk is yes
60
- # @param command [Symbol] Operation: :create, :delete, ...
61
- # @param descr [String] Description of the value
62
- # @param values [Object] Value(s), or type of value to get from user
63
- # @param id_result [String] Key in result hash to use as identifier
64
- # @param fields [Array] Fields to display
65
- # @param block [Proc] Block to execute for each value
66
- def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default, &block)
60
+ # @param command [Symbol] Operation: :create, :delete, ...
61
+ # @param descr [String, nil] Description of the value
62
+ # @param values [Class, Array, Symbol] Type, or list of values, or :identifier, result is given to the block in loop
63
+ # @param id_result [String] Key in result Hash to use as identifier
64
+ # @param fields [Symbol, Array] Fields to display
65
+ # @param schema [Hash, nil] JSON schema for validation
66
+ # @yieldparam param [Object] The parameter value to process
67
+ # @yieldreturn [Hash, nil] Result hash for the operation (optional)
68
+ # @return [Hash] Result suitable for CLI output
69
+ def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default, schema: nil, &block)
67
70
  Aspera.assert(block_given?){'missing block'}
68
71
  is_bulk = options.get_option(:bulk)
69
72
  case values
70
73
  when :identifier
71
74
  values = options.instance_identifier(description: descr)
72
75
  when Class
73
- values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk)
76
+ values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk, schema: schema)
74
77
  end
75
78
  # If not bulk, there is a single value
76
79
  params = is_bulk ? values : [values]
@@ -105,15 +108,17 @@ module Aspera
105
108
  end
106
109
 
107
110
  # Operations: Create, Delete, Show, List, Modify
108
- # @param api [Rest] api to use
109
- # @param entity [String] sub path in URL to resource relative to base url
110
- # @param command [Symbol] command to execute: create show list modify delete
111
- # @param display_fields [Array] fields to display by default
112
- # @param items_key [String] result is in a sub key of the json
113
- # @param delete_style [String] If set, the delete operation by array in payload
114
- # @param id_as_arg [String] If set, the id is provided as url argument ?<id_as_arg>=<id>
115
- # @param is_singleton [Boolean] If `true`, entity is the full path to the resource
116
- # @param &block [Proc] Block to search for identifier based on attribute value
111
+ # @param api [Aspera::Rest] API to use
112
+ # @param entity [String] Sub path in URL to resource relative to base url
113
+ # @param command [Symbol, nil] Command to execute: :create, :show, :list, :modify, :delete
114
+ # @param display_fields [Array, nil] Fields to display by default
115
+ # @param items_key [String, nil] Result is in a sub key of the JSON
116
+ # @param delete_style [String, nil] If set, the delete operation by array in payload
117
+ # @param id_as_arg [Boolean, String] If set, the id is provided as url argument ?<id_as_arg>=<id>
118
+ # @param is_singleton [Boolean] If `true`, entity is the full path to the resource
119
+ # @param list_query [Hash, nil] Query parameters for list operation
120
+ # @yieldparam value [String] Value to search for identifier
121
+ # @yieldreturn [String] The identifier
117
122
  # @return [Hash] Result suitable for CLI result
118
123
  def entity_execute(
119
124
  api:,
@@ -189,6 +194,8 @@ module Aspera
189
194
  end
190
195
 
191
196
  # Query parameters in URL suitable for REST: list/`GET` and delete/`DELETE`
197
+ # @param default [Hash, nil] Default query parameters
198
+ # @return [Hash, nil] Query parameters
192
199
  def query_read_delete(default: nil)
193
200
  # Dup default, as it could be frozen
194
201
  query = options.get_option(:query) || default.dup
@@ -202,27 +209,29 @@ module Aspera
202
209
  return query
203
210
  end
204
211
 
205
- # Retrieves an extended value from command line, used for creation or modification of entities
206
- # @param command [Symbol] command name for error message
207
- # @param type [Class] expected type of value, either a Class, an Array of Class
208
- # @param bulk [Boolean] if true, value must be an Array of <type>
209
- # @param default [Object] default value if not provided
210
- def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
212
+ # Retrieves an extended value from command line.
213
+ # Used for creation or modification of entities.
214
+ # @param command [Symbol] Command name for error message
215
+ # @param description [String, nil] Description of the value
216
+ # @param type [Class, nil] Expected type of value
217
+ # @param bulk [Boolean] If `true`, value must be an Array of `type`
218
+ # @param default [Object, nil] Default value if not provided
219
+ # @param schema [Hash, nil] JSON schema for validation
220
+ # @return [Hash, Array<Hash>] The value(s) to create object(s)
221
+ def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil, schema: nil)
211
222
  value = options.get_next_argument(
212
- "parameters for #{command}#{" (#{description})" unless description.nil?}", mandatory: default.nil?,
213
- validation: bulk ? Array : type
223
+ "parameters for #{command}#{" (#{description})" unless description.nil?}",
224
+ mandatory: default.nil?,
225
+ validation: bulk ? Array : type,
226
+ schema: schema
214
227
  )
215
228
  value = default if value.nil?
216
229
  unless type.nil?
217
- type = [type] unless type.is_a?(Array)
218
- Aspera.assert_array_all(type, Class){'check types'}
230
+ Aspera.assert_type(type, Class){'type'}
219
231
  if bulk
220
- Aspera.assert_type(value, Array, type: Cli::BadArgument)
221
- value.each do |v|
222
- Aspera.assert_values(v.class, type, type: Cli::BadArgument)
223
- end
232
+ Aspera.assert_array_all(value, type, type: Cli::BadArgument){'type'}
224
233
  else
225
- Aspera.assert_values(value.class, type, type: Cli::BadArgument)
234
+ Aspera.assert_type(value, type, type: Cli::BadArgument){'type'}
226
235
  end
227
236
  end
228
237
  return value
@@ -16,7 +16,7 @@ require 'aspera/sync/operations'
16
16
  require 'aspera/products/transferd'
17
17
  require 'aspera/transfer/parameters'
18
18
  require 'aspera/transfer/spec'
19
- require 'aspera/transfer/spec_doc'
19
+ require 'aspera/schema/documentation'
20
20
  require 'aspera/keychain/macos_security'
21
21
  require 'aspera/proxy_auto_config'
22
22
  require 'aspera/environment'
@@ -436,7 +436,7 @@ module Aspera
436
436
  end
437
437
 
438
438
  def set_preset_key(preset, param_name, param_value)
439
- Aspera.assert_values(param_name.class, [String, Symbol]){'parameter'}
439
+ Aspera.assert_type(param_name, String, Symbol){'parameter'}
440
440
  param_name = param_name.to_s
441
441
  selected_preset = @config_presets[preset]
442
442
  if selected_preset.nil?
@@ -598,10 +598,10 @@ module Aspera
598
598
  when :install
599
599
  return install_transfer_sdk
600
600
  when :spec
601
- fields, data = Transfer::SpecDoc.man_table(Formatter, include_option: true)
602
- return Main.result_object_list(data, fields: fields.map(&:to_s))
601
+ builder = Schema::Documentation.new(TerminalFormatter, Transfer::Spec::SCHEMA, include_option: true, agent_columns: true).build
602
+ return Main.result_object_list(builder.rows, fields: builder.columns)
603
603
  when :schema
604
- schema = Transfer::Spec::SCHEMA.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
604
+ schema = Transfer::Spec::SCHEMA.current.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
605
605
  agent = options.get_next_argument('transfer agent name', mandatory: false)
606
606
  schema['properties'] = schema['properties'].select{ |_k, v| CommandLineBuilder.supported_by_agent(agent, v)} unless agent.nil?
607
607
  schema['properties'] = schema['properties'].sort.to_h
@@ -834,8 +834,8 @@ module Aspera
834
834
  plugin_class = Plugins::Factory.instance.plugin_class(name)
835
835
  result.push({
836
836
  plugin: name,
837
- detect: Formatter.tick(plugin_class.respond_to?(:detect)),
838
- wizard: Formatter.tick(plugin_class.method_defined?(:wizard)),
837
+ detect: TerminalFormatter.tick(plugin_class.respond_to?(:detect)),
838
+ wizard: TerminalFormatter.tick(plugin_class.method_defined?(:wizard)),
839
839
  path: Plugins::Factory.instance.plugin_source(name)
840
840
  })
841
841
  end
@@ -878,8 +878,8 @@ module Aspera
878
878
  when :sync
879
879
  case options.get_next_command(%i[spec admin translate])
880
880
  when :spec
881
- fields, data = Transfer::SpecDoc.man_table(Formatter, include_option: true, agent_columns: false, schema: Sync::Operations::CONF_SCHEMA)
882
- return Main.result_object_list(data, fields: fields.map(&:to_s))
881
+ builder = Schema::Documentation.new(TerminalFormatter, Sync::Operations::CONF_SCHEMA, include_option: true).build
882
+ return Main.result_object_list(builder.rows, fields: builder.columns)
883
883
  when :admin
884
884
  return execute_sync_admin
885
885
  when :translate
@@ -165,7 +165,7 @@ module Aspera
165
165
  def mailbox_filtered_entries(stop_at_id: nil)
166
166
  recipient_names = [options.get_option(:recipient) || options.get_option(:username, mandatory: true)]
167
167
  # some workgroup messages have no star in recipient name
168
- recipient_names.push(recipient_names.first[1..-1]) if recipient_names.first.start_with?('*')
168
+ recipient_names.push(recipient_names.first.delete_prefix('*')) if recipient_names.first.start_with?('*')
169
169
  # mailbox is in ATOM_MAILBOXES
170
170
  mailbox = options.get_option(:box, mandatory: true)
171
171
  # parameters
@@ -234,8 +234,8 @@ module Aspera
234
234
  result_transfer = []
235
235
  param_file_list = {}
236
236
  begin
237
- param_file_list['paths'] = transfer.source_list.map{ |source| {'path'=>source}}
238
- rescue Cli::BadArgument
237
+ param_file_list['paths'] = transfer.ts_source_paths
238
+ rescue Cli::MissingArgument
239
239
  # paths is optional
240
240
  end
241
241
  box = options.get_option(:box)
@@ -270,7 +270,7 @@ module Aspera
270
270
  end
271
271
 
272
272
  def package_send
273
- parameters = value_create_modify(command: :send)
273
+ parameters = value_create_modify(command: :send, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post'))
274
274
  # autofill recipient for public url
275
275
  if @api_v5.pub_link_context&.key?('recipient_type') && !parameters.key?('recipients')
276
276
  parameters['recipients'] = [{
@@ -494,8 +494,7 @@ module Aspera
494
494
  when :invite_external_collaborator
495
495
  # :shared_inboxes, :workgroups
496
496
  shared_inbox_id = options.instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
497
- creation_payload = value_create_modify(command: res_command, type: [Hash, String])
498
- creation_payload = {'email_address' => creation_payload} if creation_payload.is_a?(String)
497
+ creation_payload = value_create_modify(command: res_command)
499
498
  result = @api_v5.create("#{res_sym}/#{shared_inbox_id}/external_collaborator", creation_payload)
500
499
  formatter.display_status(result['message'])
501
500
  result = @api_v5.lookup_entity_by_field(
@@ -41,7 +41,7 @@ module Aspera
41
41
 
42
42
  def remove_in_object_list!(obj_list)
43
43
  obj_list.each do |item|
44
- item['path'] = item['path'][@root.length..-1] if item['path'].start_with?(@root)
44
+ item['path'] = item['path'].delete_prefix(@root) if item['path'].start_with?(@root)
45
45
  end
46
46
  end
47
47
  end
@@ -27,7 +27,7 @@ module Aspera
27
27
  # @return [Hash] sync info
28
28
  def async_info_from_args(direction: nil)
29
29
  path = options.get_next_argument('path')
30
- sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {})
30
+ sync_info = options.get_next_argument('sync info', mandatory: false, validation: Hash, default: {}, schema: Schema::Registry::SYNC_CONF)
31
31
  # is the positional path a remote path ?
32
32
  path_is_remote = direction.eql?(:pull)
33
33
  if sync_info.key?('sessions') || sync_info.key?('instance')
@@ -5,6 +5,7 @@ require 'aspera/transfer/spec'
5
5
  require 'aspera/cli/info'
6
6
  require 'aspera/log'
7
7
  require 'aspera/assert'
8
+ require 'aspera/schema/registry'
8
9
 
9
10
  module Aspera
10
11
  module Cli
@@ -12,9 +13,9 @@ module Aspera
12
13
  # one of the supported transfer agents.
13
14
  # Provide CLI options to select one of the transfer agents (FASP/ascp client)
14
15
  class TransferAgent
15
- # @args special value for --sources : read file list from arguments
16
+ # `@args` special value for --sources : read file list from arguments
16
17
  FILE_LIST_FROM_ARGS = '@args'
17
- # @ts special value for --sources : read file list from transfer spec (--ts)
18
+ # `@ts` special value for --sources : read file list from transfer spec (--ts)
18
19
  FILE_LIST_FROM_TRANSFER_SPEC = '@ts'
19
20
  FILE_LIST_OPTIONS = [FILE_LIST_FROM_ARGS, FILE_LIST_FROM_TRANSFER_SPEC, 'Array'].freeze
20
21
  DEFAULT_TRANSFER_NOTIFY_TEMPLATE = <<~END_OF_TEMPLATE
@@ -63,12 +64,12 @@ module Aspera
63
64
  @transfer_paths = nil
64
65
  # HTTPGW URL provided by webapp
65
66
  @httpgw_url_lambda = nil
66
- @opt_mgr.declare(:ts, 'Override transfer spec values', allowed: Hash, handler: {o: self, m: :user_transfer_spec})
67
+ @opt_mgr.declare(:ts, 'Override transfer spec values', allowed: Hash, handler: {o: self, m: :user_transfer_spec}, schema: Schema::Registry::TRANSFER_SPEC)
67
68
  @opt_mgr.declare(:to_folder, 'Destination folder for transferred files')
68
69
  @opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS)
69
70
  @opt_mgr.declare(:src_type, 'Type of file list', allowed: %i[list pair], default: :list)
70
71
  @opt_mgr.declare(:transfer, 'Type of transfer agent', allowed: Agent::Factory::ALL.keys, default: :direct)
71
- @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', allowed: Hash, handler: {o: self, m: :transfer_info})
72
+ @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', allowed: Hash, handler: {o: self, m: :transfer_info}, schema: Schema::Registry::TRANSFER_INFO)
72
73
  @opt_mgr.parse_options!
73
74
  @notification_cb = nil
74
75
  if !@opt_mgr.get_option(:notify_to).nil?
@@ -154,24 +155,25 @@ module Aspera
154
155
  end
155
156
 
156
157
  # Transform the list of paths to a list of hash with source/dest
157
- # @param file_list [Array]
158
+ # @param file_list [Array<Hash>]
158
159
  def list_to_paths(file_list)
159
160
  source_type = @opt_mgr.get_option(:src_type, mandatory: true)
160
- case source_type
161
- when :list
162
- # when providing a list, just specify source
163
- @transfer_paths = file_list.map{ |i| {'source' => i}}
164
- when :pair
165
- Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
166
- @transfer_paths = file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
167
- else Aspera.error_unexpected_value(source_type)
168
- end
161
+ @transfer_paths =
162
+ case source_type
163
+ when :list
164
+ # when providing a list, just specify source
165
+ file_list.map{ |i| {'source' => i}}
166
+ when :pair
167
+ Aspera.assert(file_list.length.even?, type: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
168
+ file_list.each_slice(2).map{ |s, d| {'source' => s, 'destination' => d}}
169
+ else Aspera.error_unexpected_value(source_type)
170
+ end
169
171
  end
170
172
 
171
173
  # This is how the list of files to be transferred is specified
172
174
  # get paths suitable for transfer spec from command line
173
175
  # computation is done only once, cache is kept in @transfer_paths
174
- # @param default [Array] of [String] if set, used as default file for --sources=@args
176
+ # @param default [nil, Array<String>] If set, used as default file for --sources=@args
175
177
  # @return [Array, nil] of Hash {source: (mandatory), destination: (optional)}
176
178
  def ts_source_paths(default: nil)
177
179
  # return cache if set
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # For beta add extension : .beta1
6
6
  # For dev version add extension : .pre
7
- VERSION = '4.26.0'
7
+ VERSION = '4.26.1'
8
8
  end
9
9
  end
@@ -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)}, type: :warn){name} 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
@@ -151,7 +151,7 @@ module Aspera
151
151
  # @param path [String] the file path
152
152
  # @param force [Boolean] if true, overwrite the file
153
153
  # @param mode [Integer] the file mode (permissions)
154
- # @block [Proc] return the content to write to the file
154
+ # @yieldreturn [String] The content to write to the file
155
155
  def write_file_restricted(path, force: false, mode: nil)
156
156
  Aspera.assert(block_given?, type: Aspera::InternalError)
157
157
  if force || !File.exist?(path)
@@ -280,7 +280,7 @@ module Aspera
280
280
  when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
281
281
  when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
282
282
  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'}
283
+ else Aspera.error_unexpected_value(os){'no graphical open method'}
284
284
  end
285
285
  end
286
286
 
@@ -334,7 +334,7 @@ module Aspera
334
334
  filename = filename.chop while filename.end_with?(' ', '.')
335
335
  if @file_illegal_characters&.size.to_i >= 2
336
336
  # replace all illegal characters with safe_char
337
- filename = filename.tr(@file_illegal_characters[1..-1], safe_char)
337
+ filename = filename.tr(@file_illegal_characters[1..], safe_char)
338
338
  end
339
339
  # ensure only one safe_char is used at a time
340
340
  return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
@@ -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)
data/lib/aspera/log.rb CHANGED
@@ -77,7 +77,7 @@ module Aspera
77
77
  # @param name [String, Symbol] Name of object dumped
78
78
  # @param object [Hash, nil] Data to dump
79
79
  # @param level [Symbol] Debug level
80
- # @param block [Proc, nil] Give computed object
80
+ # @yieldreturn [Object] Computed object to dump (alternative to object parameter)
81
81
  def dump(name, object = nil, level: :debug, &block)
82
82
  return unless instance.logger.send(:"#{level}?")
83
83
  Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
@@ -93,12 +93,13 @@ module Aspera
93
93
  JSON.pretty_generate(object) rescue PP.pp(object, +'')
94
94
  when :ruby
95
95
  PP.pp(object, +'')
96
- else error_unexpected_value(instance.dump_format){'dump format'}
96
+ else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
97
97
  end
98
98
  "#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
99
99
  end
100
100
 
101
101
  # Capture the output of $stderr and log it at debug level
102
+ # @yieldreturn [void] Code block whose stderr output will be captured
102
103
  def capture_stderr
103
104
  real_stderr = $stderr
104
105
  $stderr = StringIO.new
@@ -192,7 +193,7 @@ module Aspera
192
193
  end
193
194
  # Use `local2` facility, like other Aspera components
194
195
  @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
195
- else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
196
+ else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
196
197
  end
197
198
  @logger.level = current_severity_integer
198
199
  @logger_type = new_log_type