aspera-cli 4.24.2 → 4.25.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 (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1070 -758
  4. data/CONTRIBUTING.md +130 -115
  5. data/README.md +961 -623
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/factory.rb +9 -6
  8. data/lib/aspera/agent/transferd.rb +8 -8
  9. data/lib/aspera/api/aoc.rb +104 -67
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/cos_node.rb +3 -2
  12. data/lib/aspera/api/faspex.rb +17 -10
  13. data/lib/aspera/api/node.rb +10 -12
  14. data/lib/aspera/ascmd.rb +2 -3
  15. data/lib/aspera/ascp/installation.rb +60 -46
  16. data/lib/aspera/ascp/management.rb +9 -5
  17. data/lib/aspera/assert.rb +28 -6
  18. data/lib/aspera/cli/error.rb +4 -2
  19. data/lib/aspera/cli/extended_value.rb +94 -62
  20. data/lib/aspera/cli/formatter.rb +44 -58
  21. data/lib/aspera/cli/main.rb +21 -14
  22. data/lib/aspera/cli/manager.rb +317 -250
  23. data/lib/aspera/cli/plugins/alee.rb +3 -3
  24. data/lib/aspera/cli/plugins/aoc.rb +139 -78
  25. data/lib/aspera/cli/plugins/ats.rb +30 -36
  26. data/lib/aspera/cli/plugins/base.rb +68 -55
  27. data/lib/aspera/cli/plugins/config.rb +90 -100
  28. data/lib/aspera/cli/plugins/console.rb +15 -9
  29. data/lib/aspera/cli/plugins/cos.rb +1 -1
  30. data/lib/aspera/cli/plugins/faspex.rb +39 -30
  31. data/lib/aspera/cli/plugins/faspex5.rb +57 -52
  32. data/lib/aspera/cli/plugins/faspio.rb +10 -7
  33. data/lib/aspera/cli/plugins/httpgw.rb +3 -2
  34. data/lib/aspera/cli/plugins/node.rb +140 -125
  35. data/lib/aspera/cli/plugins/oauth.rb +13 -12
  36. data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
  37. data/lib/aspera/cli/plugins/preview.rb +28 -48
  38. data/lib/aspera/cli/plugins/server.rb +9 -10
  39. data/lib/aspera/cli/plugins/shares.rb +77 -43
  40. data/lib/aspera/cli/sync_actions.rb +49 -38
  41. data/lib/aspera/cli/transfer_agent.rb +16 -35
  42. data/lib/aspera/cli/version.rb +1 -1
  43. data/lib/aspera/cli/wizard.rb +8 -5
  44. data/lib/aspera/command_line_builder.rb +24 -21
  45. data/lib/aspera/coverage.rb +6 -2
  46. data/lib/aspera/dot_container.rb +108 -0
  47. data/lib/aspera/environment.rb +71 -84
  48. data/lib/aspera/faspex_gw.rb +1 -1
  49. data/lib/aspera/faspex_postproc.rb +1 -1
  50. data/lib/aspera/id_generator.rb +7 -10
  51. data/lib/aspera/keychain/factory.rb +1 -2
  52. data/lib/aspera/keychain/macos_security.rb +2 -2
  53. data/lib/aspera/log.rb +2 -1
  54. data/lib/aspera/markdown.rb +31 -0
  55. data/lib/aspera/nagios.rb +6 -5
  56. data/lib/aspera/oauth/base.rb +41 -64
  57. data/lib/aspera/oauth/factory.rb +6 -7
  58. data/lib/aspera/oauth/generic.rb +1 -1
  59. data/lib/aspera/oauth/jwt.rb +1 -1
  60. data/lib/aspera/oauth/url_json.rb +6 -4
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/preview/file_types.rb +24 -38
  63. data/lib/aspera/preview/terminal.rb +95 -29
  64. data/lib/aspera/preview/utils.rb +6 -5
  65. data/lib/aspera/products/connect.rb +3 -3
  66. data/lib/aspera/rest.rb +54 -39
  67. data/lib/aspera/rest_error_analyzer.rb +4 -4
  68. data/lib/aspera/ssh.rb +10 -6
  69. data/lib/aspera/ssl.rb +41 -0
  70. data/lib/aspera/sync/conf.schema.yaml +184 -36
  71. data/lib/aspera/sync/database.rb +2 -1
  72. data/lib/aspera/sync/operations.rb +128 -72
  73. data/lib/aspera/transfer/parameters.rb +9 -10
  74. data/lib/aspera/transfer/spec.rb +2 -3
  75. data/lib/aspera/transfer/spec.schema.yaml +52 -22
  76. data/lib/aspera/transfer/spec_doc.rb +20 -30
  77. data/lib/aspera/uri_reader.rb +18 -4
  78. data/lib/transferd_pb.rb +2 -2
  79. data.tar.gz.sig +0 -0
  80. metadata +34 -6
  81. metadata.gz.sig +0 -0
@@ -18,15 +18,22 @@ module Aspera
18
18
  MAX_ITEMS = 'max'
19
19
  # Special query parameter: `pmax`: max number of pages for list command
20
20
  MAX_PAGES = 'pmax'
21
- # Special identifier format: look for this name to find where supported
22
- REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
23
21
 
24
22
  class << self
25
23
  def declare_options(options)
26
- options.declare(:query, 'Additional filter for for some commands (list/delete)', types: [Hash, Array])
24
+ options.declare(:query, 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass])
27
25
  options.declare(:property, 'Name of property to set (modify operation)')
28
- options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
29
- options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
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
30
37
  end
31
38
  end
32
39
 
@@ -41,10 +48,15 @@ module Aspera
41
48
  # Global objects
42
49
  attr_reader :context
43
50
 
51
+ # @return [Manager]
44
52
  def options; @context.options; end
53
+ # @return [TransferAgent]
45
54
  def transfer; @context.transfer; end
55
+ # @return [Config]
46
56
  def config; @context.config; end
57
+ # @return [Formatter]
47
58
  def formatter; @context.formatter; end
59
+ # @return [PersistencyFolder]
48
60
  def persistency; @context.persistency; end
49
61
 
50
62
  def add_manual_header(has_options = true)
@@ -55,26 +67,17 @@ module Aspera
55
67
  options.parser.separator('OPTIONS:') if has_options
56
68
  end
57
69
 
58
- # Must be called AFTER the instance action:
59
- # ... folder browse _call_instance_identifier
70
+ # Resource identifier as positional parameter
60
71
  #
61
72
  # @param description [String] description of the identifier
62
- # @param as_option [Symbol] option name to use if identifier is an option
63
73
  # @param block [Proc] block to search for identifier based on attribute value
64
74
  # @return [String, Array] identifier or list of ids
65
- def instance_identifier(description: 'identifier', as_option: nil, &block)
66
- if as_option.nil?
67
- res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
68
- else
69
- res_id = options.get_option(as_option)
70
- end
75
+ def instance_identifier(description: 'identifier', &block)
76
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
71
77
  # Can be an Array
72
- if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
73
- if block
74
- res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
75
- else
76
- raise Cli::BadArgument, "Percent syntax for #{description} not supported in this context"
77
- end
78
+ if res_id.is_a?(String) && (m = Base.percent_selector(res_id))
79
+ Aspera.assert(block, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
80
+ res_id = yield(m[:field], m[:value])
78
81
  end
79
82
  return res_id
80
83
  end
@@ -133,12 +136,12 @@ module Aspera
133
136
  # @param command [Symbol] command to execute: create show list modify delete
134
137
  # @param display_fields [Array] fields to display by default
135
138
  # @param items_key [String] result is in a sub key of the json
136
- # @param delete_style [String] if set, the delete operation by array in payload
137
- # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
138
- # @param is_singleton [Boolean] if true, entity is the full path to the resource
139
- # @param tclo [Bool] if set, :list use paging with total_count, limit, offset
140
- # @param block [Proc] block to search for identifier based on attribute value
141
- # @return result suitable for CLI result
139
+ # @param delete_style [String] If set, the delete operation by array in payload
140
+ # @param id_as_arg [String] If set, the id is provided as url argument ?<id_as_arg>=<id>
141
+ # @param is_singleton [Boolean] If `true`, entity is the full path to the resource
142
+ # @param tclo [Boolean] If `true`, :list use paging with total_count, limit, offset
143
+ # @param block [Proc] Block to search for identifier based on attribute value
144
+ # @return [Hash] Result suitable for CLI result
142
145
  def entity_execute(
143
146
  api:,
144
147
  entity:,
@@ -172,12 +175,11 @@ module Aspera
172
175
  if !delete_style.nil?
173
176
  one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
174
177
  Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
175
- api.call(
176
- operation: 'DELETE',
177
- subpath: entity,
178
+ api.delete(
179
+ entity,
180
+ nil,
178
181
  content_type: Rest::MIME_JSON,
179
- body: {delete_style => one_res_id},
180
- headers: {'Accept' => Rest::MIME_JSON}
182
+ body: {delete_style => one_res_id}
181
183
  )
182
184
  return Main.result_status('deleted')
183
185
  end
@@ -192,11 +194,10 @@ module Aspera
192
194
  data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
193
195
  return Main.result_object_list(data, total: total, fields: display_fields)
194
196
  end
195
- resp = api.call(operation: 'GET', subpath: entity, headers: {'Accept' => Rest::MIME_JSON}, query: query_read_delete)
196
- return Main.result_empty if resp[:http].code == '204'
197
- data = resp[:data]
197
+ data, http = api.read(entity, query_read_delete, ret: :both)
198
+ return Main.result_empty if http.code == '204'
198
199
  # TODO: not generic : which application is this for ?
199
- if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
200
+ if http['Content-Type'].start_with?('application/vnd.api+json')
200
201
  Log.log.debug('is vnd.api')
201
202
  data = data[entity]
202
203
  end
@@ -223,9 +224,8 @@ module Aspera
223
224
 
224
225
  # Query parameters in URL suitable for REST: list/GET and delete/DELETE
225
226
  def query_read_delete(default: nil)
226
- query = options.get_option(:query)
227
227
  # Dup default, as it could be frozen
228
- query = default.dup if query.nil?
228
+ query = options.get_option(:query) || default.dup
229
229
  Log.log.debug{"query_read_delete=#{query}".bg_red}
230
230
  begin
231
231
  # Check it is suitable
@@ -249,7 +249,7 @@ module Aspera
249
249
  value = default if value.nil?
250
250
  unless type.nil?
251
251
  type = [type] unless type.is_a?(Array)
252
- Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
252
+ Aspera.assert_array_all(type, Class){'check types'}
253
253
  if bulk
254
254
  Aspera.assert_type(value, Array, type: Cli::BadArgument)
255
255
  value.each do |v|
@@ -263,10 +263,10 @@ module Aspera
263
263
  end
264
264
 
265
265
  # Get a (full or partial) list of all entities of a given type with query: offset/limit
266
- # @param `api` [Rest] the API object
267
- # @param `entity` [String,Symbol] the API endpoint of entity to list
268
- # @param `items_key` [String] key in the result to get the list of items
269
- # @param `query` [Hash,nil] additional query parameters
266
+ # @param api [Rest] API object
267
+ # @param entity [String,Symbol] API endpoint of entity to list
268
+ # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
269
+ # @param query [Hash,nil] Additional query parameters
270
270
  # @return [Array] items, total_count
271
271
  def list_entities_limit_offset_total_count(
272
272
  api:,
@@ -309,26 +309,39 @@ module Aspera
309
309
  return result, total_count
310
310
  end
311
311
 
312
- # Lookup an entity id from its name
313
- # @param entity [String] the type of entity to lookup, by default it is the path, and it is also the field name in result
314
- # @param value [String] the value to lookup
315
- # @param field [String] the field to match, by default it is 'name'
316
- # @param items_key [String] key in the result to get the list of items (override entity)
317
- # @param query [Hash] additional query parameters
312
+ # Lookup an entity id from its name.
313
+ # Uses query `q` if `query` is `:default` and `field` is `name`.
314
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
315
+ # @param value [String] Value to lookup
316
+ # @param field [String] Field to match, by default it is `'name'`
317
+ # @param items_key [String] Key in the result to get the list of items (override entity)
318
+ # @param query [Hash] Additional query parameters (Default: `:default`)
318
319
  def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
319
320
  if query.eql?(:default)
320
321
  Aspera.assert(field.eql?('name')){'Default query is on name only'}
321
322
  query = {'q'=> value}
322
323
  end
323
- found = list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first.select{ |i| i[field].eql?(value)}
324
- case found.length
325
- when 0 then raise "No #{entity} with #{field} = #{value}"
326
- when 1 then return found.first
327
- else raise "Found #{found.length} #{entity} with #{field} = #{value}"
328
- end
324
+ 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}
325
+ end
326
+
327
+ # Lookup entity by field and value. Extract single result from list of result returned by block.
328
+ # @param entity [String] Type of entity to lookup, by default it is the path, and it is also the field name in result
329
+ # @param value [String] Value to lookup
330
+ # @param field [String] Field to match, by default it is `'name'`
331
+ # @param block [Proc] Get list of entity matching query.
332
+ def lookup_entity_generic(entity:, value:, field: 'name', &block)
333
+ Aspera.assert(block_given?)
334
+ found = yield
335
+ Aspera.assert_array_all(found, Hash)
336
+ found = found.select{ |i| i[field].eql?(value)}
337
+ return found.first if found.length.eql?(1)
338
+ raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
329
339
  end
340
+
330
341
  PER_PAGE_DEFAULT = 1000
331
- private_constant :PER_PAGE_DEFAULT
342
+ # Percent selector: select by this field for this value
343
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
344
+ private_constant :PER_PAGE_DEFAULT, :REGEX_LOOKUP_ID_BY_FIELD
332
345
  end
333
346
  end
334
347
  end
@@ -10,6 +10,7 @@ require 'aspera/cli/formatter'
10
10
  require 'aspera/cli/info'
11
11
  require 'aspera/cli/transfer_progress'
12
12
  require 'aspera/cli/wizard'
13
+ require 'aspera/cli/sync_actions'
13
14
  require 'aspera/ascp/installation'
14
15
  require 'aspera/sync/operations'
15
16
  require 'aspera/products/transferd'
@@ -29,7 +30,9 @@ require 'aspera/oauth/jwt'
29
30
  require 'aspera/log'
30
31
  require 'aspera/assert'
31
32
  require 'aspera/oauth'
33
+ require 'aspera/ssl'
32
34
  require 'openssl'
35
+ require 'digest'
33
36
  require 'open3'
34
37
  require 'date'
35
38
  require 'erb'
@@ -39,6 +42,8 @@ module Aspera
39
42
  module Plugins
40
43
  # Manage the CLI config file
41
44
  class Config < Base
45
+ include SyncActions
46
+
42
47
  class << self
43
48
  # Folder containing plugins in the gem's main folder
44
49
  def gem_plugins_folder
@@ -76,7 +81,7 @@ module Aspera
76
81
  # We need to defer parsing of options until we have the config file, so we can use @extend with @preset
77
82
  super
78
83
  @use_plugin_defaults = true
79
- @config_presets = nil
84
+ @config_presets = {}
80
85
  @config_checksum_on_disk = nil
81
86
  @vault_instance = nil
82
87
  @pac_exec = nil
@@ -89,7 +94,7 @@ module Aspera
89
94
  @option_cache_tokens = true
90
95
  @main_folder = nil
91
96
  @option_config_file = nil
92
- # Store is used for ruby https
97
+ # Store is used for ruby https (OpenSSL::X509::Store)
93
98
  @certificate_store = nil
94
99
  # Paths are used for ascp
95
100
  @certificate_paths = nil
@@ -98,7 +103,6 @@ module Aspera
98
103
  options.declare(
99
104
  :home, 'Home folder for tool',
100
105
  handler: {o: self, m: :main_folder},
101
- types: String,
102
106
  default: self.class.default_app_main_folder(app_name: Info::CMD_NAME)
103
107
  )
104
108
  options.parse_options!
@@ -118,65 +122,49 @@ module Aspera
118
122
  # Read config file (set @config_presets)
119
123
  read_config_file
120
124
  # Add preset handler (needed for smtp)
121
- ExtendedValue.instance.set_handler(EXTEND_PRESET, lambda{ |v| preset_by_name(v)})
122
- ExtendedValue.instance.set_handler(EXTEND_VAULT, lambda{ |v| vault_value(v)})
125
+ ExtendedValue.instance.on(EXTEND_PRESET){ |v| preset_by_name(v)}
126
+ ExtendedValue.instance.on(EXTEND_VAULT){ |v| vault_value(v)}
127
+ ExtendedValue.instance.on(EXTEND_ARGS){ |v| options.args_as_extended(v)}
123
128
  # Load defaults before it can be overridden
124
129
  add_plugin_default_preset(CONF_GLOBAL_SYM)
125
130
  # Vault options
126
131
  options.declare(:secret, 'Secret for access keys')
127
- options.declare(:vault, 'Vault for secrets', types: Hash, default: {})
132
+ options.declare(:vault, 'Vault for secrets', allowed: Hash)
128
133
  options.declare(:vault_password, 'Vault password')
129
134
  options.parse_options!
130
135
  # Declare generic plugin options only after handlers are declared
131
136
  Base.declare_options(options)
132
137
  # Configuration options
133
- options.declare(:no_default, 'Do not load default configuration for plugin', values: :none, short: 'N'){@use_plugin_defaults = false}
138
+ options.declare(:no_default, 'Do not load default configuration for plugin', allowed: Allowed::TYPES_NONE, short: 'N'){@use_plugin_defaults = false}
134
139
  options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
135
- options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', coerce: Integer, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
140
+ options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', allowed: Allowed::TYPES_INTEGER, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
136
141
  options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
137
142
  # Declare wizard options
138
143
  @wizard = Wizard.new(self, @main_folder)
139
144
  # Transfer SDK options
140
- options.declare(:ascp_path, 'Ascp: Path to ascp', handler: {o: Ascp::Installation.instance, m: :ascp_path})
141
- options.declare(:use_product, 'Ascp: Use ascp from specified product', handler: {o: self, m: :option_use_product})
142
145
  options.declare(:sdk_url, 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF)
143
- options.declare(:locations_url, 'Ascp: URL to get locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
144
- options.declare(:sdk_folder, 'Ascp: SDK folder path', handler: {o: Products::Transferd, m: :sdk_directory})
145
- options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
146
+ options.parse_options!
147
+ set_sdk_dir
148
+ options.declare(:ascp_path, 'Ascp: Path to ascp (or product with "product:")', handler: {o: Ascp::Installation.instance, m: :ascp_path}, default: "#{Ascp::Installation::USE_PRODUCT_PREFIX}#{Ascp::Installation::FIRST_FOUND}")
149
+ options.declare(:locations_url, 'Ascp: URL to get download locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
150
+ options.declare(:sdk_folder, 'Ascp: SDK installation folder path', handler: {o: Products::Transferd, m: :sdk_directory})
151
+ options.declare(:progress_bar, 'Display progress bar', allowed: Allowed::TYPES_BOOLEAN, default: Environment.terminal?)
146
152
  # Email options
147
- options.declare(:smtp, 'Email: SMTP configuration', types: Hash)
153
+ options.declare(:smtp, 'Email: SMTP configuration', allowed: Hash)
148
154
  options.declare(:notify_to, 'Email: Recipient for notification of transfers')
149
155
  options.declare(:notify_template, 'Email: ERB template for notification of transfers')
150
156
  # HTTP options
151
- options.declare(:insecure, 'HTTP/S: Do not validate any certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
152
- options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
153
- options.declare(:warn_insecure, 'HTTP/S: Issue a warning if certificate is ignored', values: :bool, handler: {o: self, m: :option_warn_insecure_cert}, default: :yes)
154
- options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
155
- options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
156
- options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', types: String, handler: {o: self, m: :option_http_proxy})
157
- options.declare(:cache_tokens, 'Save and reuse OAuth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
157
+ options.declare(:insecure, 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_insecure}, default: false)
158
+ options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass], handler: {o: self, m: :option_ignore_cert_host_port})
159
+ options.declare(:warn_insecure, 'HTTP/S: Issue a warning if certificate is ignored', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_warn_insecure_cert}, default: true)
160
+ options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY, handler: {o: self, m: :trusted_cert_locations})
161
+ options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', allowed: Hash, handler: {o: self, m: :option_http_options}, default: {})
162
+ options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', handler: {o: self, m: :option_http_proxy})
163
+ options.declare(:cache_tokens, 'Save and reuse OAuth tokens', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_cache_tokens})
158
164
  options.declare(:fpac, 'Proxy auto configuration script')
159
- options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', types: Array)
165
+ options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', allowed: [Array, NilClass])
160
166
  options.parse_options!
161
167
  @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
162
- # Check SDK folder is set or not, for compatibility, we check in two places
163
- sdk_dir = Products::Transferd.sdk_directory rescue nil
164
- if sdk_dir.nil?
165
- @sdk_default_location = true
166
- Log.log.debug('SDK folder is not set, checking default')
167
- # New location
168
- sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
169
- Log.log.debug{"Checking: #{sdk_dir}"}
170
- if !Dir.exist?(sdk_dir)
171
- Log.log.debug{"No such folder: #{sdk_dir}"}
172
- # Former location
173
- former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
174
- Log.log.debug{"Checking: #{former_sdk_folder}"}
175
- sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
176
- end
177
- Log.log.debug{"Using: #{sdk_dir}"}
178
- Products::Transferd.sdk_directory = sdk_dir
179
- end
180
168
  pac_script = options.get_option(:fpac)
181
169
  # Create PAC executor
182
170
  if !pac_script.nil?
@@ -200,27 +188,7 @@ module Aspera
200
188
  RestParameters.instance.send(method, v)
201
189
  elsif k.eql?('ssl_options')
202
190
  keys_to_delete.push(k)
203
- # NOTE: here is a hack that allows setting SSLContext options
204
- Aspera.assert_type(v, Array){'ssl_options'}
205
- # Start with default options
206
- ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
207
- v.each do |opt|
208
- case opt
209
- when Integer
210
- ssl_options = opt
211
- when String
212
- name = "OP_#{opt.start_with?('-') ? opt[1..] : opt}".upcase
213
- raise Cli::BadArgument, "Unknown ssl_option: #{name}, use one of: #{OpenSSL::SSL.constants.grep(/^OP_/).map{ |c| c.to_s.sub(/^OP_/, '')}.join(', ')}" if !OpenSSL::SSL.const_defined?(name)
214
- if opt.start_with?('-')
215
- ssl_options &= ~OpenSSL::SSL.const_get(name)
216
- else
217
- ssl_options |= OpenSSL::SSL.const_get(name)
218
- end
219
- else
220
- Aspera.error_unexpected_value(opt.class.name){'Expected String or Integer in ssl_options'}
221
- end
222
- end
223
- OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] = ssl_options
191
+ Aspera::SSL.option_list = v
224
192
  elsif OAuth::Factory.instance.parameters.key?(k.to_sym)
225
193
  keys_to_delete.push(k)
226
194
  OAuth::Factory.instance.parameters[k.to_sym] = v
@@ -238,11 +206,31 @@ module Aspera
238
206
  attr_accessor :main_folder, :option_cache_tokens, :option_insecure, :option_warn_insecure_cert, :option_http_options
239
207
  attr_reader :option_ignore_cert_host_port, :progress_bar
240
208
 
209
+ def set_sdk_dir
210
+ # Check SDK folder is set or not, for compatibility, we check in two places
211
+ sdk_dir = Products::Transferd.sdk_directory rescue nil
212
+ if sdk_dir.nil?
213
+ @sdk_default_location = true
214
+ Log.log.debug('SDK folder is not set, checking default')
215
+ # New location
216
+ sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
217
+ Log.log.debug{"Checking: #{sdk_dir}"}
218
+ if !Dir.exist?(sdk_dir)
219
+ Log.log.debug{"No such folder: #{sdk_dir}"}
220
+ # Former location
221
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
222
+ Log.log.debug{"Checking: #{former_sdk_folder}"}
223
+ sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
224
+ end
225
+ Log.log.debug{"Using: #{sdk_dir}"}
226
+ Products::Transferd.sdk_directory = sdk_dir
227
+ end
228
+ end
229
+
241
230
  # Add files, folders or default locations to the certificate store
242
- # @param path_list [Array<String>] list of paths to add
231
+ # @param path_list [Array<String>] List of paths to add
243
232
  # @return the list of paths
244
233
  def trusted_cert_locations=(path_list)
245
- path_list = [path_list] unless path_list.is_a?(Array)
246
234
  Aspera.assert_type(path_list, Array){'cert locations'}
247
235
  if @certificate_store.nil?
248
236
  Log.log.debug('Creating SSL Cert store')
@@ -273,6 +261,7 @@ module Aspera
273
261
  pp = Dir.entries(p)
274
262
  .map{ |e| File.realpath(File.join(p, e))}
275
263
  .select{ |entry| File.file?(entry)}
264
+ .select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
276
265
  end
277
266
  @certificate_paths.concat(pp)
278
267
  end
@@ -285,7 +274,7 @@ module Aspera
285
274
  locations = @certificate_paths
286
275
  if locations.nil?
287
276
  # Compute default locations
288
- self.trusted_cert_locations = SpecialValues::DEF
277
+ self.trusted_cert_locations = [SpecialValues::DEF]
289
278
  locations = @certificate_paths
290
279
  # Restore defaults
291
280
  @certificate_paths = @certificate_store = nil
@@ -447,7 +436,7 @@ module Aspera
447
436
  Log.log.warn{"keeping same value for #{preset}: #{param_name}: #{param_value}"}
448
437
  return
449
438
  end
450
- Log.log.warn{"overwriting value: #{selected_preset[param_name]}"}
439
+ Log.log.warn{"overwriting value for #{param_name}: #{selected_preset[param_name]}"}
451
440
  end
452
441
  selected_preset[param_name] = param_value
453
442
  formatter.display_status("Updated: #{preset}: #{param_name} <- #{param_value}")
@@ -479,21 +468,12 @@ module Aspera
479
468
  raise Cli::Error, "Unknown config preset: #{include_path}" if current.nil?
480
469
  end
481
470
  current = self.class.deep_clone(current) unless current.is_a?(String)
482
- return ExtendedValue.instance.evaluate(current)
483
- end
484
-
485
- def option_use_product=(value)
486
- Ascp::Installation.instance.use_ascp_from_product(value)
487
- end
488
-
489
- def option_use_product
490
- 'write-only option, see value of ascp_path'
471
+ return ExtendedValue.instance.evaluate(current, context: 'preset')
491
472
  end
492
473
 
493
474
  def option_plugin_folder=(value)
494
- Aspera.assert_values(value.class, [String, Array]){'plugin folder'}
495
- value = [value] if value.is_a?(String)
496
- Aspera.assert(value.all?(String)){'plugin folder'}
475
+ value = [value] unless value.is_a?(Array)
476
+ Aspera.assert_array_all(value, String){'plugin folder(s)'}
497
477
  value.each{ |f| Plugins::Factory.instance.add_lookup_folder(f)}
498
478
  end
499
479
 
@@ -514,8 +494,9 @@ module Aspera
514
494
  end
515
495
  end
516
496
 
497
+ # @return [Integer]
517
498
  def config_checksum
518
- JSON.generate(@config_presets).hash
499
+ Digest::SHA1.hexdigest(JSON.generate(@config_presets))
519
500
  end
520
501
 
521
502
  # Read config file and validate format
@@ -556,6 +537,7 @@ module Aspera
556
537
  Log.log.warn{"#{file} -> #{@main_folder}"}
557
538
  end
558
539
  end
540
+ return
559
541
  rescue Psych::SyntaxError => e
560
542
  Log.log.error('YAML error in config file')
561
543
  raise e
@@ -605,6 +587,15 @@ module Aspera
605
587
  return Main.result_status("Opened: #{one_link['href']}")
606
588
  end
607
589
  end
590
+ Aspera.error_unreachable_line
591
+ end
592
+
593
+ def install_transfer_sdk
594
+ # Reset to default location, if older default was used
595
+ Products::Transferd.sdk_directory = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME) if @sdk_default_location
596
+ asked_version = options.get_next_argument('transferd version', mandatory: false)
597
+ name, version, folder = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: asked_version)
598
+ return Main.result_status("Installed #{name} version #{version} in #{folder}")
608
599
  end
609
600
 
610
601
  def execute_action_ascp
@@ -624,7 +615,7 @@ module Aspera
624
615
  # Collect info from ascp executable
625
616
  data = Ascp::Installation.instance.ascp_info
626
617
  # Add command line transfer spec
627
- data['ts'] = transfer.option_transfer_spec
618
+ data['ts'] = transfer.user_transfer_spec
628
619
  # Add keys
629
620
  DataRepository::ELEMENTS.each_with_object(data){ |i, h| h[i.to_s] = DataRepository.instance.item(i)}
630
621
  # Declare those as secrets
@@ -638,15 +629,11 @@ module Aspera
638
629
  when :use
639
630
  default_product = options.get_next_argument('product name')
640
631
  Ascp::Installation.instance.use_ascp_from_product(default_product)
641
- set_global_default(:ascp_path, Ascp::Installation.instance.path(:ascp))
632
+ set_global_default(:ascp_path, "#{Ascp::Installation::USE_PRODUCT_PREFIX}#{default_product}")
642
633
  return Main.result_nothing
643
634
  end
644
635
  when :install
645
- # Reset to default location, if older default was used
646
- Products::Transferd.sdk_directory = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME) if @sdk_default_location
647
- version = options.get_next_argument('transferd version', mandatory: false)
648
- n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
649
- return Main.result_status("Installed #{n} version #{v}")
636
+ return install_transfer_sdk
650
637
  when :spec
651
638
  fields, data = Transfer::SpecDoc.man_table(Formatter, include_option: true)
652
639
  return Main.result_object_list(data, fields: fields.map(&:to_s))
@@ -671,11 +658,7 @@ module Aspera
671
658
  command = options.get_next_command(%i[list install])
672
659
  case command
673
660
  when :install
674
- # Reset to default location, if older default was used
675
- Products::Transferd.sdk_directory = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME) if @sdk_default_location
676
- version = options.get_next_argument('transferd version', mandatory: false)
677
- n, v = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: version)
678
- return Main.result_status("Installed #{n} version #{v}")
661
+ return install_transfer_sdk
679
662
  when :list
680
663
  sdk_list = Ascp::Installation.instance.sdk_locations
681
664
  return Main.result_object_list(
@@ -725,7 +708,7 @@ module Aspera
725
708
  value = @config_presets[name][param_name]
726
709
  raise "no such option in preset #{name} : #{param_name}" if value.nil?
727
710
  case value
728
- when Numeric, String then return Main.result_text(ExtendedValue.instance.evaluate(value.to_s))
711
+ when Numeric, String then return Main.result_text(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
729
712
  end
730
713
  return Main.result_single_object(value)
731
714
  when :unset
@@ -754,7 +737,7 @@ module Aspera
754
737
  options.ask_missing_mandatory = true
755
738
  @config_presets[name] ||= {}
756
739
  options.get_next_argument('option names', multiple: true).each do |option_name|
757
- option_value = options.get_interactive(option_name, option: true)
740
+ option_value = options.get_interactive(option_name, check_option: true)
758
741
  @config_presets[name][option_name] = option_value
759
742
  end
760
743
  return Main.result_status("Updated: #{name}")
@@ -889,7 +872,7 @@ module Aspera
889
872
  result.push({
890
873
  plugin: name,
891
874
  detect: Formatter.tick(plugin_class.respond_to?(:detect)),
892
- wizard: Formatter.tick(plugin_class.instance_methods.include?(:wizard)),
875
+ wizard: Formatter.tick(plugin_class.method_defined?(:wizard)),
893
876
  path: Plugins::Factory.instance.plugin_source(name)
894
877
  })
895
878
  end
@@ -930,10 +913,14 @@ module Aspera
930
913
  when :ascp
931
914
  execute_action_ascp
932
915
  when :sync
933
- case options.get_next_command(%i[spec])
916
+ case options.get_next_command(%i[spec admin translate])
934
917
  when :spec
935
918
  fields, data = Transfer::SpecDoc.man_table(Formatter, include_option: true, agent_columns: false, schema: Sync::Operations::CONF_SCHEMA)
936
919
  return Main.result_object_list(data, fields: fields.map(&:to_s))
920
+ when :admin
921
+ return execute_sync_admin
922
+ when :translate
923
+ return Main.result_single_object(Sync::Operations.args_to_conf(options.get_next_argument('async arguments', multiple: true)))
937
924
  else Aspera.error_unreachable_line
938
925
  end
939
926
  when :transferd
@@ -1183,16 +1170,17 @@ module Aspera
1183
1170
  end
1184
1171
 
1185
1172
  # Lookup the corresponding secret for the given URL and usernames
1186
- # @raise Exception if mandatory and not found
1187
- def lookup_secret(url:, username:, mandatory: false)
1173
+ # @param url [String] Server URL
1174
+ # @param username [String] Username
1175
+ # @return [String, nil] Secret if found
1176
+ def lookup_secret(url:, username:)
1188
1177
  secret = options.get_option(:secret)
1189
- if secret.nil?
1178
+ if secret.eql?('PRESET')
1190
1179
  conf = lookup_preset(url: url, username: username)
1191
1180
  if conf.is_a?(Hash)
1192
1181
  Log.log.debug{"Found preset #{conf} with URL and username"}
1193
1182
  secret = conf['password']
1194
1183
  end
1195
- raise "Please provide secret for #{username} using option: secret or by setting a preset for #{username}@#{url}." if secret.nil? && mandatory
1196
1184
  end
1197
1185
  return secret
1198
1186
  end
@@ -1227,6 +1215,7 @@ module Aspera
1227
1215
  # Special extended values
1228
1216
  EXTEND_PRESET = :preset
1229
1217
  EXTEND_VAULT = :vault
1218
+ EXTEND_ARGS = :''
1230
1219
  PRESET_DIG_SEPARATOR = '.'
1231
1220
  DEFAULT_CHECK_NEW_VERSION_DAYS = 7
1232
1221
  COFFEE_IMAGE_URL = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
@@ -1235,7 +1224,7 @@ module Aspera
1235
1224
  SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
1236
1225
  CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
1237
1226
  SMTP_CONF_PARAMS = %i[server tls ssl port domain username password from_name from_email].freeze
1238
-
1227
+ CERT_EXT = %w[crt cer pem der].freeze
1239
1228
  private_constant :ASPERA_HOME_FOLDER_NAME,
1240
1229
  :DEFAULT_CONFIG_FILENAME,
1241
1230
  :CONF_PRESET_CONFIG,
@@ -1260,7 +1249,8 @@ module Aspera
1260
1249
  :TRANSFERD_APP_NAME,
1261
1250
  :GLOBAL_DEFAULT_KEYWORD,
1262
1251
  :CONF_GLOBAL_SYM,
1263
- :GEM_CHECK_DATE_FMT
1252
+ :GEM_CHECK_DATE_FMT,
1253
+ :CERT_EXT
1264
1254
  end
1265
1255
  end
1266
1256
  end