aspera-cli 4.26.1 → 4.27.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 (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +300 -22
  4. data/CONTRIBUTING.md +100 -12
  5. data/bin/ascli +5 -5
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +27 -25
  12. data/lib/aspera/agent/factory.rb +4 -0
  13. data/lib/aspera/agent/httpgw.rb +2 -2
  14. data/lib/aspera/agent/node.rb +34 -2
  15. data/lib/aspera/agent/transferd.rb +39 -3
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +56 -51
  18. data/lib/aspera/api/cos_node.rb +2 -2
  19. data/lib/aspera/api/faspex.rb +22 -20
  20. data/lib/aspera/api/httpgw.rb +4 -4
  21. data/lib/aspera/api/node.rb +33 -30
  22. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  23. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  24. data/lib/aspera/ascmd.rb +25 -23
  25. data/lib/aspera/ascp/installation.rb +43 -40
  26. data/lib/aspera/ascp/management.rb +2 -3
  27. data/lib/aspera/cli/ascp_actions.rb +155 -0
  28. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  29. data/lib/aspera/cli/bootstrapper.rb +197 -0
  30. data/lib/aspera/cli/command_registry.rb +145 -0
  31. data/lib/aspera/cli/command_spec.rb +138 -0
  32. data/lib/aspera/cli/context.rb +71 -0
  33. data/lib/aspera/cli/error.rb +16 -0
  34. data/lib/aspera/cli/extended_value.rb +5 -3
  35. data/lib/aspera/cli/formatter.rb +122 -209
  36. data/lib/aspera/cli/gem_checker.rb +65 -0
  37. data/lib/aspera/cli/hints.rb +2 -2
  38. data/lib/aspera/cli/http.rb +218 -0
  39. data/lib/aspera/cli/info.rb +2 -0
  40. data/lib/aspera/cli/mailer.rb +97 -0
  41. data/lib/aspera/cli/mcp_tool.rb +198 -0
  42. data/lib/aspera/cli/options.schema.yaml +364 -7
  43. data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
  44. data/lib/aspera/cli/plugins/alee.rb +20 -22
  45. data/lib/aspera/cli/plugins/aoc.rb +1173 -849
  46. data/lib/aspera/cli/plugins/ats.rb +200 -161
  47. data/lib/aspera/cli/plugins/base.rb +470 -107
  48. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  49. data/lib/aspera/cli/plugins/config.rb +434 -1032
  50. data/lib/aspera/cli/plugins/console.rb +106 -64
  51. data/lib/aspera/cli/plugins/cos.rb +44 -32
  52. data/lib/aspera/cli/plugins/factory.rb +7 -4
  53. data/lib/aspera/cli/plugins/faspex.rb +296 -259
  54. data/lib/aspera/cli/plugins/faspex5.rb +592 -344
  55. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  56. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  57. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  58. data/lib/aspera/cli/plugins/node.rb +1001 -797
  59. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  60. data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
  61. data/lib/aspera/cli/plugins/preview.rb +183 -142
  62. data/lib/aspera/cli/plugins/server.rb +125 -80
  63. data/lib/aspera/cli/plugins/shares.rb +301 -107
  64. data/lib/aspera/cli/preset_actions.rb +139 -0
  65. data/lib/aspera/cli/preset_manager.rb +236 -0
  66. data/lib/aspera/cli/result.rb +360 -0
  67. data/lib/aspera/cli/runner.rb +473 -0
  68. data/lib/aspera/cli/secret_finder.rb +40 -0
  69. data/lib/aspera/cli/special_values.rb +1 -0
  70. data/lib/aspera/cli/sync_actions.rb +84 -56
  71. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  72. data/lib/aspera/cli/transfer_actions.rb +83 -0
  73. data/lib/aspera/cli/transfer_agent.rb +116 -51
  74. data/lib/aspera/cli/transfer_progress.rb +9 -9
  75. data/lib/aspera/cli/vault_manager.rb +57 -0
  76. data/lib/aspera/cli/version.rb +1 -1
  77. data/lib/aspera/cli/wizard.rb +26 -26
  78. data/lib/aspera/command_line_builder.rb +23 -23
  79. data/lib/aspera/data_repository.rb +6 -1
  80. data/lib/aspera/dot_container.rb +5 -5
  81. data/lib/aspera/environment.rb +39 -13
  82. data/lib/aspera/exec_spec.rb +13 -0
  83. data/lib/aspera/faspex_gw.rb +2 -2
  84. data/lib/aspera/faspex_postproc.rb +4 -3
  85. data/lib/aspera/graphql.rb +35 -0
  86. data/lib/aspera/hash_ext.rb +6 -0
  87. data/lib/aspera/json_rpc/client.rb +62 -0
  88. data/lib/aspera/json_rpc/version.rb +7 -0
  89. data/lib/aspera/keychain/base.rb +1 -1
  90. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  91. data/lib/aspera/keychain/factory.rb +4 -4
  92. data/lib/aspera/keychain/macos_security.rb +4 -4
  93. data/lib/aspera/link_header.rb +82 -0
  94. data/lib/aspera/log.rb +23 -5
  95. data/lib/aspera/markdown.rb +85 -2
  96. data/lib/aspera/nagios.rb +2 -2
  97. data/lib/aspera/node_simulator.rb +3 -1
  98. data/lib/aspera/oauth/base.rb +10 -10
  99. data/lib/aspera/oauth/boot.rb +43 -0
  100. data/lib/aspera/oauth/factory.rb +41 -15
  101. data/lib/aspera/oauth/jwt.rb +4 -4
  102. data/lib/aspera/oauth/web.rb +4 -4
  103. data/lib/aspera/oauth.rb +1 -0
  104. data/lib/aspera/persistency_action_once.rb +10 -10
  105. data/lib/aspera/persistency_folder.rb +10 -3
  106. data/lib/aspera/preview/file_types.rb +7 -3
  107. data/lib/aspera/preview/generator.rb +3 -3
  108. data/lib/aspera/preview/utils.rb +1 -1
  109. data/lib/aspera/products/connect.rb +4 -1
  110. data/lib/aspera/products/other.rb +1 -1
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -9
  113. data/lib/aspera/rest.rb +18 -40
  114. data/lib/aspera/rest_error_analyzer.rb +7 -3
  115. data/lib/aspera/rest_errors_aspera.rb +0 -10
  116. data/lib/aspera/rest_list.rb +13 -6
  117. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
  119. data/lib/aspera/schema/async_tables.yaml +361 -0
  120. data/lib/aspera/schema/documentation.rb +57 -26
  121. data/lib/aspera/schema/reader.rb +60 -11
  122. data/lib/aspera/schema/registry.rb +50 -10
  123. data/lib/aspera/secret_hider.rb +11 -2
  124. data/lib/aspera/ssl.rb +4 -4
  125. data/lib/aspera/sync/conf.schema.yaml +2 -2
  126. data/lib/aspera/sync/database.rb +14 -10
  127. data/lib/aspera/sync/operations.rb +15 -18
  128. data/lib/aspera/temp_file_manager.rb +5 -1
  129. data/lib/aspera/timer_limiter.rb +1 -1
  130. data/lib/aspera/transfer/faux_file.rb +24 -11
  131. data/lib/aspera/transfer/parameters.rb +27 -25
  132. data/lib/aspera/transfer/result.rb +74 -0
  133. data/lib/aspera/transfer/resumer.rb +7 -7
  134. data/lib/aspera/transfer/spec.rb +18 -1
  135. data/lib/aspera/transfer/spec.schema.yaml +26 -8
  136. data/lib/aspera/transfer/uri.rb +1 -1
  137. data/lib/aspera/uri_reader.rb +2 -2
  138. data/lib/aspera/web_auth.rb +2 -2
  139. data/lib/aspera/web_server_simple.rb +8 -5
  140. data/lib/aspera/yaml.rb +2 -1
  141. data.tar.gz.sig +0 -0
  142. metadata +45 -16
  143. metadata.gz.sig +0 -0
  144. data/lib/aspera/cli/main.rb +0 -480
  145. data/lib/aspera/json_rpc.rb +0 -51
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore initdemo genkey pubkey asperasoft filelists
4
+ require 'aspera/cli/bootstrapper'
4
5
  require 'aspera/cli/plugins/basic_auth'
5
6
  require 'aspera/cli/plugins/factory'
6
7
  require 'aspera/cli/extended_value'
@@ -8,34 +9,30 @@ require 'aspera/cli/special_values'
8
9
  require 'aspera/cli/version'
9
10
  require 'aspera/cli/formatter'
10
11
  require 'aspera/cli/info'
11
- require 'aspera/cli/transfer_progress'
12
12
  require 'aspera/cli/wizard'
13
13
  require 'aspera/cli/sync_actions'
14
+ require 'aspera/cli/ascp_actions'
15
+ require 'aspera/cli/preset_actions'
16
+ require 'aspera/cli/vault_manager'
17
+ require 'aspera/cli/transfer_actions'
18
+ require 'aspera/cli/gem_checker'
14
19
  require 'aspera/ascp/installation'
15
20
  require 'aspera/sync/operations'
16
21
  require 'aspera/products/transferd'
17
- require 'aspera/transfer/parameters'
18
22
  require 'aspera/transfer/spec'
19
23
  require 'aspera/schema/documentation'
20
24
  require 'aspera/keychain/macos_security'
21
- require 'aspera/proxy_auto_config'
22
25
  require 'aspera/environment'
23
26
  require 'aspera/persistency_action_once'
24
27
  require 'aspera/id_generator'
25
- require 'aspera/persistency_folder'
26
28
  require 'aspera/data_repository'
27
- require 'aspera/line_logger'
28
29
  require 'aspera/rest'
29
30
  require 'aspera/oauth/jwt'
30
31
  require 'aspera/log'
31
32
  require 'aspera/assert'
32
- require 'aspera/oauth'
33
- require 'aspera/ssl'
34
33
  require 'openssl'
35
34
  require 'digest'
36
35
  require 'open3'
37
- require 'date'
38
- require 'erb'
39
36
  require 'net/http'
40
37
 
41
38
  module Aspera
@@ -44,6 +41,11 @@ module Aspera
44
41
  # Manage the CLI config file
45
42
  class Config < Base
46
43
  include SyncActions
44
+ include VaultManager
45
+ include GemChecker
46
+ include AscpActions
47
+ include PresetActions
48
+ include TransferActions
47
49
 
48
50
  class << self
49
51
  # Folder containing plugins in the gem's main folder
@@ -51,7 +53,7 @@ module Aspera
51
53
  File.dirname(File.expand_path(__FILE__))
52
54
  end
53
55
 
54
- # @return main folder where code is, i.e. .../lib
56
+ # @return [String] main folder where code is, i.e. .../lib
55
57
  # Go up as many times as englobing modules (not counting class, as it is a file)
56
58
  def gem_src_root
57
59
  # Module.nesting[2] is Cli::Plugins
@@ -63,7 +65,7 @@ module Aspera
63
65
  return Marshal.load(Marshal.dump(val))
64
66
  end
65
67
 
66
- # @return product family folder (~/.aspera)
68
+ # @return [String] product family folder (~/.aspera)
67
69
  def module_family_folder
68
70
  user_home_folder = Dir.home
69
71
  Aspera.assert(Dir.exist?(user_home_folder), type: Cli::Error){"Home folder does not exist: #{user_home_folder}. Check your user environment."}
@@ -73,416 +75,280 @@ module Aspera
73
75
  # @return [String] Product config folder (~/.aspera/<name>)
74
76
  def default_app_main_folder(app_name:)
75
77
  Aspera.assert_type(app_name, String)
76
- Aspera.assert(!app_name.empty?)
78
+ Aspera.assert(!app_name.empty?, 'app_name must not be empty')
77
79
  return File.join(module_family_folder, app_name)
78
80
  end
79
81
  end
80
82
 
83
+ DEFAULT_CHECK_NEW_VERSION_DAYS = 7
84
+ private_constant :DEFAULT_CHECK_NEW_VERSION_DAYS
85
+
86
+ option :preset, description: 'Load the named option preset from current config file', short: 'P', handler: :option_preset
87
+ option :version_check_days, description: 'Period in days to check new version (zero to disable)', allowed: Allowed::TYPES_INTEGER, default: DEFAULT_CHECK_NEW_VERSION_DAYS
88
+ option :plugin_folder, description: 'Folder where to find additional plugins', handler: :option_plugin_folder
89
+ option :sdk_url, description: 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF
90
+ option :locations_url, description: 'Ascp: URL to get download locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls}
91
+ option :sdk_folder, description: 'Ascp: Path to folder with ascp (or product with "product:")', handler: {o: Products::Transferd, m: :sdk_directory}
92
+ option :smtp, schema: Schema::Registry::SMTP_OPTIONS
93
+ option :notify_to, description: 'Email: Recipient for notification of transfers'
94
+ option :notify_template, description: 'Email: ERB template for notification of transfers'
95
+ option :cache_tokens, description: 'Save and reuse OAuth tokens', allowed: Allowed::TYPES_BOOLEAN, default: true, handler: :option_cache_tokens
96
+
81
97
  def initialize(**_)
82
- # We need to defer parsing of options until we have the config file, so we can use @extend with @preset
83
98
  super
84
- @use_plugin_defaults = true
85
- @config_presets = {}
86
- @config_checksum_on_disk = nil
87
99
  @vault_instance = nil
88
- @pac_exec = nil
89
100
  @sdk_default_location = false
90
- @option_insecure = false
91
- @option_warn_insecure_cert = true
92
- @option_ignore_cert_host_port = []
93
- @option_http_options = {}
94
- @ssl_warned_urls = []
95
101
  @option_cache_tokens = true
96
- @main_folder = nil
97
- @option_config_file = nil
98
- # Store is used for ruby https (OpenSSL::X509::Store)
99
- @certificate_store = nil
100
- # Paths are used for ascp
101
- @certificate_paths = nil
102
- @progress_bar = nil
103
- # Option to set main folder
104
- options.declare(
105
- :home, 'Home folder for tool',
106
- handler: {o: self, m: :main_folder},
107
- default: self.class.default_app_main_folder(app_name: Info::CMD_NAME)
108
- )
109
- options.parse_options!
110
- Log.log.debug{"#{Info::CMD_NAME} folder: #{@main_folder}"}
111
- setup_persistency_and_plugin_folders
112
- # Option to set config file
113
- options.declare(
114
- :config_file, 'Path to YAML file with preset configuration',
115
- handler: {o: self, m: :option_config_file},
116
- default: File.join(@main_folder, DEFAULT_CONFIG_FILENAME)
117
- )
118
- options.parse_options!
119
- # Read config file (set @config_presets)
120
- read_config_file
121
- setup_extended_value_handlers
122
- # Vault options
123
- options.declare(:secret, 'Secret for access keys')
124
- options.declare(:vault, 'Vault for secrets', allowed: Hash)
125
- options.declare(:vault_password, 'Vault password')
126
- options.parse_options!
127
- # Declare generic plugin options only after handlers are declared
128
- Base.declare_options(options)
129
- # Configuration options
130
- options.declare(:no_default, 'Do not load default configuration for plugin', allowed: Allowed::TYPES_NONE, short: 'N'){@use_plugin_defaults = false}
131
- options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
132
- 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)
133
- options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
134
- # Declare wizard options
135
- @wizard = Wizard.new(self, @main_folder)
136
- # Transfer SDK options
137
- options.declare(:sdk_url, 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF)
102
+ # :no_default uses a &block callback - must stay imperative
103
+ options.declare(:no_default, description: 'Do not load default configuration for plugin', allowed: Allowed::TYPES_NONE, short: 'N'){presets.use_plugin_defaults = false}
104
+ # Declare wizard options (Wizard#initialize calls options.declare internally)
105
+ @wizard = Wizard.new(self, context.main_folder)
138
106
  options.parse_options!
139
107
  set_sdk_dir
140
- options.declare(:locations_url, 'Ascp: URL to get download locations of Aspera Transfer Daemon', handler: {o: Ascp::Installation.instance, m: :transferd_urls})
141
- options.declare(:sdk_folder, 'Ascp: Path to folder with ascp (or product with "product:")', handler: {o: Products::Transferd, m: :sdk_directory})
142
- options.declare(:progress_bar, 'Display progress bar', allowed: Allowed::TYPES_BOOLEAN, default: Environment.terminal?)
143
- # Email options
144
- options.declare(:smtp, 'Email: SMTP configuration', allowed: Hash)
145
- options.declare(:notify_to, 'Email: Recipient for notification of transfers')
146
- options.declare(:notify_template, 'Email: ERB template for notification of transfers')
147
- # HTTP options
148
- options.declare(:insecure, 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_insecure}, default: false)
149
- 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})
150
- 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)
151
- options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY, handler: {o: self, m: :trusted_cert_locations})
152
- options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', allowed: Hash, handler: {o: self, m: :option_http_options}, default: {})
153
- options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', handler: {o: self, m: :option_http_proxy})
154
- options.declare(:cache_tokens, 'Save and reuse OAuth tokens', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_cache_tokens})
155
- options.declare(:fpac, 'Proxy auto configuration script')
156
- options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', allowed: [Array, NilClass])
108
+ # HTTP options: declare metadata (class method), then bind to the instance
109
+ Http.declare_options(options)
110
+ context.http_config.bind_options(options)
157
111
  options.parse_options!
158
- @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
159
- setup_pac_executor
160
- setup_rest_and_transfer_runtime
161
- end
162
-
163
- private
164
-
165
- def setup_persistency_and_plugin_folders
166
- context.persistency = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
167
- Plugins::Factory.instance.add_lookup_folder(self.class.gem_plugins_folder)
168
- Plugins::Factory.instance.add_lookup_folder(File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME))
169
- end
170
-
171
- def setup_extended_value_handlers
172
- ExtendedValue.instance.on(EXTEND_PRESET){ |v| preset_by_name(v)}
173
- ExtendedValue.instance.on(EXTEND_VAULT){ |v| vault_value(v)}
174
- ExtendedValue.instance.on(EXTEND_ARGS){ |v| options.args_as_extended(v)}
175
- add_plugin_default_preset(CONF_GLOBAL_SYM)
176
- end
177
-
178
- def setup_pac_executor
179
- pac_script = options.get_option(:fpac)
180
- return unless pac_script
181
-
182
- @pac_exec = ProxyAutoConfig.new(pac_script).register_uri_generic
183
- proxy_user_pass = options.get_option(:proxy_credentials)
184
- if proxy_user_pass
185
- Aspera.assert(proxy_user_pass.length.eql?(2), type: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
186
- @pac_exec.proxy_user = proxy_user_pass[0]
187
- @pac_exec.proxy_pass = proxy_user_pass[1]
188
- end
189
- end
190
-
191
- def setup_rest_and_transfer_runtime
192
- RestParameters.instance.user_agent = Info::CMD_NAME
193
- RestParameters.instance.progress_bar = @progress_bar
194
- RestParameters.instance.session_cb = ->(http_session){update_http_session(http_session)}
195
- RestParameters.instance.spinner_cb = ->(title = nil, action: :spin){formatter.long_operation(title, action: action)}
196
- # Check http options that are global
197
- keys_to_delete = []
198
- @option_http_options.each do |k, v|
199
- method = "#{k}=".to_sym
200
- if RestParameters.instance.respond_to?(method)
201
- keys_to_delete.push(k)
202
- RestParameters.instance.send(method, v)
203
- elsif k.eql?('ssl_options')
204
- keys_to_delete.push(k)
205
- Aspera::SSL.option_list = v
206
- elsif OAuth::Factory.instance.parameters.key?(k.to_sym)
207
- keys_to_delete.push(k)
208
- OAuth::Factory.instance.parameters[k.to_sym] = v
209
- end
210
- end
211
- keys_to_delete.each{ |k| @option_http_options.delete(k)}
212
- OAuth::Factory.instance.persist_mgr = persistency if @option_cache_tokens
213
- OAuth::Web.additional_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
214
- Transfer::Parameters.file_list_folder = File.join(@main_folder, FILE_LIST_FOLDER_NAME)
215
- RestErrorAnalyzer.instance.log_file = File.join(@main_folder, REST_EXCEPTIONS_LOG_FILENAME)
216
- # Register aspera REST call error handlers
217
- RestErrorsAspera.register_handlers
218
- end
219
-
220
- public
221
-
222
- attr_accessor :main_folder, :option_cache_tokens, :option_insecure, :option_warn_insecure_cert, :option_http_options
223
- attr_reader :option_ignore_cert_host_port, :progress_bar
224
-
225
- def set_sdk_dir
226
- # Check SDK folder is set or not, for compatibility, we check in two places
227
- sdk_dir = Products::Transferd.sdk_directory rescue nil
228
- if sdk_dir.nil?
229
- @sdk_default_location = true
230
- Log.log.debug('SDK folder is not set, checking default')
231
- # New location
232
- sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
233
- Log.log.debug{"Checking: #{sdk_dir}"}
234
- if !Dir.exist?(sdk_dir)
235
- Log.log.debug{"No such folder: #{sdk_dir}"}
236
- # Former location
237
- former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
238
- Log.log.debug{"Checking: #{former_sdk_folder}"}
239
- sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
240
- end
241
- Log.log.debug{"Using: #{sdk_dir}"}
242
- Products::Transferd.sdk_directory = sdk_dir
243
- end
244
- end
245
-
246
- # Add files, folders or default locations to the certificate store
247
- # @param path_list [Array<String>] List of paths to add
248
- # @return the list of paths
249
- def trusted_cert_locations=(path_list)
250
- Aspera.assert_type(path_list, Array){'cert locations'}
251
- if @certificate_store.nil?
252
- Log.log.debug('Creating SSL Cert store')
253
- @certificate_store = OpenSSL::X509::Store.new
254
- @certificate_paths = []
255
- end
256
-
257
- path_list.each do |path|
258
- Aspera.assert_type(path, String){'Expecting a String for certificate location'}
259
- paths_to_add = [path]
260
- Log.log.debug{"Adding cert location: #{path}"}
261
- if path.eql?(SpecialValues::DEF)
262
- @certificate_store.set_default_paths
263
- paths_to_add = [OpenSSL::X509::DEFAULT_CERT_DIR]
264
- # JRuby cert file seems not to be PEM
265
- paths_to_add.push(OpenSSL::X509::DEFAULT_CERT_FILE) unless defined?(JRUBY_VERSION)
266
- paths_to_add.select!{ |f| File.exist?(f)}
267
- elsif File.file?(path)
268
- @certificate_store.add_file(path)
269
- elsif File.directory?(path)
270
- @certificate_store.add_path(path)
271
- else
272
- raise "No such file or folder: #{path}"
273
- end
274
- paths_to_add.each do |p|
275
- pp = [File.realpath(p)]
276
- if File.directory?(p)
277
- pp = Dir.entries(p)
278
- .map{ |e| File.realpath(File.join(p, e))}
279
- .select{ |entry| File.file?(entry)}
280
- .select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
281
- end
282
- @certificate_paths.concat(pp)
283
- end
284
- end
285
- @certificate_paths.uniq!
286
- end
287
-
288
- # @return only files
289
- def trusted_cert_locations
290
- locations = @certificate_paths
291
- if locations.nil?
292
- # Compute default locations
293
- self.trusted_cert_locations = [SpecialValues::DEF]
294
- locations = @certificate_paths
295
- # Restore defaults
296
- @certificate_paths = @certificate_store = nil
297
- end
298
- return locations
299
112
  end
300
113
 
301
- def option_http_proxy
302
- return ENV['http_proxy']
303
- end
304
-
305
- def option_http_proxy=(value)
306
- URI.parse(value)
307
- ENV['http_proxy'] = value
308
- end
309
-
310
- def option_ignore_cert_host_port=(url_list)
311
- url_list.each do |url|
312
- uri = URI.parse(url)
313
- raise "Expecting https scheme: #{url}" unless uri.scheme.eql?('https')
314
- @option_ignore_cert_host_port.push([uri.host, uri.port].freeze)
315
- end
316
- end
317
-
318
- def ignore_cert?(address, port)
319
- endpoint = [address, port].freeze
320
- ignore_cert = false
321
- if @option_insecure || @option_ignore_cert_host_port.any?(endpoint)
322
- ignore_cert = true
323
- if @option_warn_insecure_cert
324
- base_url = "https://#{address}:#{port}"
325
- if !@ssl_warned_urls.include?(base_url)
326
- Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
327
- @ssl_warned_urls.push(base_url)
328
- end
329
- end
330
- end
331
- Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
332
- return ignore_cert
333
- end
334
-
335
- # Called every time a new REST HTTP session is opened to set user-provided options
336
- # @param http_session [Net::HTTP] the newly created HTTP/S session object
337
- def update_http_session(http_session)
338
- http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
339
- # Rest.io_http_session(http_session).debug_output = Log.log
340
- http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
341
- http_session.cert_store = @certificate_store if @certificate_store
342
- Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
343
- @option_http_options.each do |k, v|
344
- method = "#{k}=".to_sym
345
- # Check if accessor is a method of Net::HTTP
346
- # continue_timeout= read_timeout= write_timeout=
347
- if http_session.respond_to?(method)
348
- http_session.send(method, v)
349
- else
350
- Log.log.error{"Unknown HTTP session attribute: #{k}"}
114
+ # DSL command declarations - replaces ACTIONS + execute_action
115
+ command :preset, description: 'Manage configuration presets'
116
+ commands_under(:preset) do
117
+ command :list, description: 'List all presets'
118
+ command :overview, description: 'Display all options from all presets'
119
+ command :lookup, description: 'Find preset matching URL and username'
120
+ command :secure, description: 'Move secrets to vault',
121
+ arguments: [{name: :config_name, type: String, mandatory: false}]
122
+ command :show, description: 'Display a preset',
123
+ arguments: [{name: :name, type: :identifier}]
124
+ command :delete, description: 'Delete a preset',
125
+ arguments: [{name: :name, type: :identifier}]
126
+ command :get, description: 'Get a single parameter from a preset',
127
+ arguments: [{name: :name, type: :identifier}, {name: :param_name, type: String}]
128
+ command :unset, description: 'Remove a parameter from a preset',
129
+ arguments: [{name: :name, type: :identifier}, {name: :param_name, type: String}]
130
+ command(
131
+ :set, description: 'Set a parameter in a preset',
132
+ arguments: [{name: :name, type: :identifier}, {name: :param_name, type: String},
133
+ {name: :param_value, type: nil}]
134
+ )
135
+ command :initialize, description: 'Initialize a preset with a value',
136
+ arguments: [{name: :name, type: :identifier}, {name: :config_value, type: Hash}]
137
+ command :update, description: 'Update a preset with current option values',
138
+ arguments: [{name: :name, type: :identifier}]
139
+ command :ask, description: 'Interactively ask for option values',
140
+ arguments: [{name: :name, type: :identifier},
141
+ {name: :option_names, type: String, multiple: true, interactive: true}]
142
+ end
143
+ command(
144
+ :open, description: 'Open the configuration file in the default editor',
145
+ action: lambda do
146
+ Environment.instance.open_editor(context.presets.config_file.to_s)
147
+ Result::Nothing.new
148
+ end
149
+ )
150
+ command :documentation, description: 'Open the documentation in the default browser',
151
+ arguments: [
152
+ {name: :location, type: Symbol, mandatory: false, default: :github, allowed: %i[github local toc]},
153
+ {name: :section, type: String, mandatory: false}
154
+ ]
155
+ command :genkey, description: 'Generate a new RSA private key',
156
+ arguments: [
157
+ {name: :private_key_path, type: String},
158
+ {name: :private_key_length, type: Integer, mandatory: false, default: OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH}
159
+ ]
160
+ command :pubkey, description: 'Display the public key of an RSA private key',
161
+ arguments: [{name: :private_key_pem, type: String}],
162
+ action: ->(private_key_pem:, **){Result::Text.new(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)}
163
+ command :remote_certificate, description: 'Retrieve the certificate chain of a remote HTTPS server'
164
+ command :echo, description: 'Display the value of a given argument',
165
+ arguments: [{name: :value, type: nil}],
166
+ action: ->(value:, **){Result.auto(value)}
167
+ command :download, description: 'Download a file from a URL',
168
+ arguments: [
169
+ {name: :file_url, type: String},
170
+ {name: :file_dest, type: String, mandatory: false}
171
+ ]
172
+ command :tokens, description: 'Manage OAuth tokens'
173
+ command :plugins, description: 'Manage CLI plugins'
174
+ command :detect, description: 'Detect the Aspera product from a URL (interactive)',
175
+ arguments: [{name: :url, type: String}, {name: :plugin_name, mandatory: false, default: nil}]
176
+ command :wizard, description: 'Run the setup wizard for an Aspera product (interactive)',
177
+ arguments: [{name: :url, type: String}, {name: :plugin_name, mandatory: false, default: nil},
178
+ {name: :preset_name, mandatory: false, default: ''}]
179
+ command :coffee, description: 'Display a coffee image', action: ->{Result::Image.new(COFFEE_IMAGE_URL)}
180
+ command :image, description: 'Display an image',
181
+ arguments: [{name: :image_uri, type: nil}],
182
+ action: ->(image_uri:, **){Result::Image.new(image_uri)}
183
+ command :sync, description: 'Manage Aspera Sync operations'
184
+ command :gem, description: 'Display gem information'
185
+ command :folder, description: 'Display the configuration folder path', action: ->{Result::Text.new(context.main_folder)}
186
+ command :file, description: 'Display the configuration file path', action: ->{Result::Text.new(context.presets.config_file)}
187
+ command(
188
+ :email_test, description: 'Send a test email',
189
+ action: lambda do
190
+ context.mailer.send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
191
+ Result::Nothing.new
192
+ end
193
+ )
194
+ command :smtp_settings, description: 'Display the current SMTP settings', action: ->{Result::SingleObject.new(context.mailer.email_settings)}
195
+ command(
196
+ :proxy_check, description: 'Check the proxy returned by the PAC script for a given URL',
197
+ arguments: [{name: :server_url, type: String}],
198
+ action: lambda do |server_url:, **|
199
+ Aspera.assert(!context.pac_executor.nil?, type: Cli::BadArgument){'No PAC script configured, use --fpac'}
200
+ Result::ValueList.new(context.pac_executor.get_proxies(server_url), name: 'proxy')
201
+ end
202
+ )
203
+ command :check_update, description: 'Check if a newer version of the gem is available', action: ->{Result::SingleObject.new(check_gem_version)}
204
+ command :initdemo, description: 'Initialize the demo server preset'
205
+ command :vault, description: 'Manage secrets in the vault'
206
+ commands_under(:vault) do
207
+ command :info, description: 'Display vault information',
208
+ action: ->(**){Result::SingleObject.new(vault.info)}
209
+ command :list, description: 'List secrets in the vault',
210
+ action: ->(**){Result::ObjectList.new(vault.list)}
211
+ command :show, description: 'Show a secret by label',
212
+ arguments: [{name: :label, type: String}]
213
+ command :create, description: 'Add a new secret to the vault',
214
+ arguments: [{name: :info, type: Hash}]
215
+ command :delete, description: 'Delete a secret by label',
216
+ arguments: [{name: :label, type: String}]
217
+ command :password, description: 'Change the vault password',
218
+ arguments: [{name: :new_password, type: String}]
219
+ end
220
+ command :commands, description: 'List all available commands across all plugins'
221
+ command :options, description: 'List all options available for a plugin',
222
+ arguments: [{name: :plugin_name, type: String}]
223
+ command :test, description: 'Internal test commands'
224
+ command :platform, description: 'Display the current platform/architecture', action: ->{Result::Text.new(Environment.instance.architecture)}
225
+ command :completion, description: 'Generate shell completion scripts'
226
+
227
+ # remote_certificate sub-commands
228
+ commands_under(:remote_certificate) do
229
+ command :chain, description: 'Display the full certificate chain as PEM',
230
+ arguments: [{name: :remote_url, type: String}]
231
+ command :only, description: 'Display only the server certificate as PEM',
232
+ arguments: [{name: :remote_url, type: String}]
233
+ command :name, description: 'Display the CN of the server certificate',
234
+ arguments: [{name: :remote_url, type: String}]
235
+ end
236
+
237
+ # tokens sub-commands
238
+ commands_under(:tokens) do
239
+ command(
240
+ :flush, description: 'Delete all cached OAuth tokens',
241
+ action: lambda do
242
+ require 'aspera/api/node'
243
+ Result::ValueList.new(OAuth::Factory.instance.flush_tokens, name: 'file')
351
244
  end
352
- end
353
- end
354
-
355
- def check_gem_version
356
- latest_version =
357
- begin
358
- Rest.new(base_url: 'https://rubygems.org/api/v1').read("versions/#{Info::GEM_NAME}/latest.json")['version']
359
- rescue StandardError
360
- Log.log.warn('Could not retrieve latest gem version on rubygems.')
361
- '0'
245
+ )
246
+ command(
247
+ :list, description: 'List all cached OAuth tokens',
248
+ action: lambda do
249
+ require 'aspera/api/node'
250
+ Result::ObjectList.new(OAuth::Factory.instance.persisted_tokens)
362
251
  end
363
- if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(Info::RUBY_FUTURE_MINIMUM_VERSION)
364
- Log.log.warn do
365
- "Note that a future version will require Ruby version #{Info::RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
366
- "you are using #{Environment.ruby_version}"
252
+ )
253
+ command :show, description: 'Show details of a cached OAuth token',
254
+ arguments: [{name: :token_id, type: :identifier}]
255
+ end
256
+
257
+ # plugins sub-commands
258
+ commands_under(:plugins) do
259
+ command :list, description: 'List all available plugins'
260
+ command :create, description: 'Create a new plugin skeleton file',
261
+ arguments: [
262
+ {name: :name, type: String},
263
+ {name: :folder, type: String, mandatory: false}
264
+ ]
265
+ end
266
+
267
+ # ascp sub-commands
268
+ command :ascp, description: 'Manage FASP/ascp transfer engine'
269
+ commands_under(:ascp) do
270
+ command :show, description: 'Display ascp binary path'
271
+ command :info, description: 'Display ascp and transfer spec information'
272
+ command :install, description: 'Install the transfer SDK',
273
+ arguments: [{name: :version, mandatory: false, default: nil}]
274
+ command :spec, description: 'Display the transfer spec schema'
275
+ command :schema, description: 'Display the transfer spec JSON schema',
276
+ arguments: [{name: :agent_name, mandatory: false, default: nil}]
277
+ command :errors, description: 'Display FASP error codes'
278
+ command :products, description: 'Manage installed Aspera products'
279
+ commands_under(%i[ascp products]) do
280
+ command :list, description: 'List installed Aspera products'
281
+ end
282
+ end
283
+
284
+ # agents sub-commands
285
+ command :agents, description: 'Manage transfer agents'
286
+ commands_under(:agents) do
287
+ command :list, description: 'List all transfer agents'
288
+ command :show, description: 'Show details for a transfer agent',
289
+ arguments: [{name: :agent_name, allowed: Agent::Factory::ALL.keys}]
290
+ command :parameters, description: 'Show configurable parameters for a transfer agent',
291
+ arguments: [{name: :agent_name, allowed: Agent::Factory::ALL.keys}]
292
+ end
293
+
294
+ # transfer async management sub-commands
295
+ command :transfer, description: 'Manage asynchronous transfers'
296
+ commands_under(:transfer) do
297
+ command :list, description: 'List all async transfer jobs'
298
+ command :status, description: 'Show status of an async transfer job',
299
+ arguments: [{name: :job_id, type: String}]
300
+ command :cleanup, description: 'Remove completed/failed/cancelled transfer entries'
301
+ end
302
+
303
+ # transferd sub-commands
304
+ command :transferd, description: 'Manage the transfer daemon (transferd)'
305
+ commands_under(:transferd) do
306
+ command :install, description: 'Install the transfer daemon'
307
+ command :list, description: 'List available SDK locations'
308
+ end
309
+
310
+ # sync sub-commands
311
+ commands_under(:sync) do
312
+ command(
313
+ :spec, description: 'Display the sync configuration schema',
314
+ action: lambda do
315
+ builder = Schema::Documentation.new(TerminalFormatter, Sync::Operations::CONF_SCHEMA, include_option: true).build
316
+ Result::ObjectList.new(builder.rows, fields: builder.columns)
367
317
  end
368
- end
369
- return {
370
- name: Info::GEM_NAME,
371
- current: Cli::VERSION,
372
- latest: latest_version,
373
- need_update: Gem::Version.new(Cli::VERSION) < Gem::Version.new(latest_version)
374
- }
375
- end
376
-
377
- def periodic_check_newer_gem_version
378
- # Get verification period
379
- delay_days = options.get_option(:version_check_days, mandatory: true).to_i
380
- # Check only if not zero day
381
- return if delay_days.eql?(0)
382
- # Get last date from persistency
383
- last_check_array = []
384
- check_date_persist = PersistencyActionOnce.new(
385
- manager: persistency,
386
- data: last_check_array,
387
- id: 'version_last_check'
388
318
  )
389
- # Get persisted date or nil
390
- current_date = Date.today
391
- last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
392
- Log.log.debug{"gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}"}
393
- return if !last_check_days.nil? && last_check_days < delay_days
394
- # Generate timestamp
395
- last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
396
- check_date_persist.save
397
- # Compare this version and the one on internet
398
- check_data = check_gem_version
399
- Log.log.warn do
400
- "A new version is available: #{check_data[:latest]}. You have #{check_data[:current]}. Upgrade with: gem update #{check_data[:name]}"
401
- end if check_data[:need_update]
319
+ command :admin, description: 'Manage sync database (admin operations)'
320
+ SyncActions.register_sync_admin_commands(self, %i[sync admin])
321
+ command :translate, description: 'Translate async-style arguments to sync config format',
322
+ arguments: [{name: :async_arguments, type: String, multiple: true}],
323
+ action: ->(async_arguments:, **){Result::SingleObject.new(Sync::Operations.args_to_conf(async_arguments))}
402
324
  end
403
325
 
404
- # Loads default parameters of plugin if no -P parameter
405
- # and if there is a section defined for the plugin in the "default" section
406
- # Try to find: conf[conf["default"][plugin_str]]
407
- # @param plugin_name_sym : symbol for plugin name
408
- def add_plugin_default_preset(plugin_name_sym)
409
- default_config_name = get_plugin_default_config_name(plugin_name_sym)
410
- Log.log.debug{"add_plugin_default_preset:#{plugin_name_sym}:#{default_config_name}"}
411
- options.add_option_preset(preset_by_name(default_config_name), 'default_plugin', override: false) unless default_config_name.nil?
412
- return
326
+ # gem sub-commands
327
+ commands_under(:gem) do
328
+ command :path, description: 'Display the gem source root path', action: ->{Result::Text.new(self.class.gem_src_root)}
329
+ command :version, description: 'Display the gem version', action: ->{Result::Text.new(Cli::VERSION)}
330
+ command :name, description: 'Display the gem name', action: ->{Result::Text.new(Info::GEM_NAME)}
413
331
  end
414
332
 
415
- # Get the default global preset, or set default one
416
- def global_default_preset
417
- result = get_plugin_default_config_name(CONF_GLOBAL_SYM)
418
- if result.nil?
419
- result = CONF_PRESET_GLOBAL
420
- set_preset_key(CONF_PRESET_DEFAULTS, CONF_GLOBAL_SYM, result)
421
- end
422
- return result
423
- end
424
-
425
- def defaults_set(plugin_name, preset_name, preset_values, option_default, option_override)
426
- @config_presets[CONF_PRESET_DEFAULTS] ||= {}
427
- raise Cli::Error, "A default configuration already exists for plugin '#{plugin_name}' (use --override=yes or --default=no)" \
428
- if !option_override && option_default && @config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name)
429
- raise Cli::Error, "Preset already exists: #{preset_name} (use --override=yes or provide alternate name on command line)" \
430
- if !option_override && @config_presets.key?(preset_name)
431
- if option_default
432
- Log.log.info("Setting config preset as default for #{plugin_name}")
433
- @config_presets[CONF_PRESET_DEFAULTS][plugin_name.to_s] = preset_name
434
- end
435
- @config_presets[preset_name] = preset_values
333
+ # test sub-commands
334
+ commands_under(:test) do
335
+ command :throw, description: 'Raise an exception (for testing)',
336
+ arguments: [
337
+ {name: :exception_class_name, type: String},
338
+ {name: :exception_text, type: String}
339
+ ]
340
+ command :web, description: 'Test web browser interaction', action: -> {}
436
341
  end
437
342
 
438
- def set_preset_key(preset, param_name, param_value)
439
- Aspera.assert_type(param_name, String, Symbol){'parameter'}
440
- param_name = param_name.to_s
441
- selected_preset = @config_presets[preset]
442
- if selected_preset.nil?
443
- Log.log.debug{"Unknown preset name: #{preset}, initializing"}
444
- selected_preset = @config_presets[preset] = {}
445
- end
446
- Aspera.assert_type(selected_preset, Hash){"#{preset}.#{param_name}"}
447
- if selected_preset.key?(param_name)
448
- if selected_preset[param_name].eql?(param_value)
449
- Log.log.warn{"keeping same value for #{preset}: #{param_name}: #{param_value}"}
450
- return
451
- end
452
- Log.log.warn{"overwriting value for #{param_name}: #{selected_preset[param_name]}"}
453
- end
454
- selected_preset[param_name] = param_value
455
- Log.log.info("Updated: #{preset}: #{param_name} <- #{param_value}")
456
- nil
343
+ # completion sub-commands
344
+ commands_under(:completion) do
345
+ command :bash, description: 'Generate bash completion script',
346
+ arguments: [{name: :words, type: String, multiple: true, mandatory: false}]
457
347
  end
458
348
 
459
- # Set parameter and value in global config
460
- # Creates one if none already created
461
- # @return preset name that contains global default
462
- def set_global_default(key, value)
463
- set_preset_key(global_default_preset, key, value)
464
- end
349
+ attr_accessor :option_cache_tokens
465
350
 
466
- # $HOME/.aspera/`program_name`
467
351
  attr_reader :gem_url
468
- attr_accessor :option_config_file
469
-
470
- # @param config_name name of the preset in config file
471
- # @param include_path used to detect and avoid include loops
472
- # @return copy of the hash from name (also expands possible includes)
473
- def preset_by_name(config_name, include_path = [])
474
- raise Cli::Error, 'loop in include' if include_path.include?(config_name)
475
- include_path = include_path.clone # Avoid messing up if there are multiple branches
476
- current = @config_presets
477
- config_name.split(PRESET_DIG_SEPARATOR).each do |name|
478
- Aspera.assert_type(current, Hash, type: Cli::Error){"sub key: #{include_path}"}
479
- include_path.push(name)
480
- current = current[name]
481
- raise Cli::Error, "Unknown config preset: #{include_path}" if current.nil?
482
- end
483
- current = self.class.deep_clone(current) unless current.is_a?(String)
484
- return ExtendedValue.instance.evaluate(current, context: 'preset')
485
- end
486
352
 
487
353
  def option_plugin_folder=(value)
488
354
  value = [value] unless value.is_a?(Array)
@@ -501,663 +367,222 @@ module Aspera
501
367
  when Hash
502
368
  options.add_option_preset(value, 'set')
503
369
  when String
504
- options.add_option_preset(preset_by_name(value), 'set_by_name')
370
+ options.add_option_preset(presets.by_name(value), 'set_by_name')
505
371
  else
506
372
  raise BadArgument, 'Preset definition must be a String for preset name, or Hash for set of values'
507
373
  end
508
374
  end
509
375
 
510
- # @return [Integer]
511
- def config_checksum
512
- Digest::SHA1.hexdigest(JSON.generate(@config_presets))
513
- end
376
+ # DSL handlers - one method per leaf command
514
377
 
515
- # Read config file and validate format
516
- def read_config_file
517
- Log.log.debug{"config file is: #{@option_config_file}".red}
518
- # Files search for configuration, by default the one given by user
519
- search_files = [@option_config_file]
520
- # Find first existing file (or nil)
521
- conf_file_to_load = search_files.find{ |f| File.exist?(f)}
522
- # If no file found, create default config
523
- if conf_file_to_load.nil?
524
- Log.log.warn{"No config file found. New configuration file: #{@option_config_file}"}
525
- @config_presets = {CONF_PRESET_CONFIG => {CONF_PRESET_VERSION => 'new file'}}
526
- # @config_checksum_on_disk is nil
527
- else
528
- Log.log.debug{"loading #{@option_config_file}"}
529
- @config_presets = YAML.load_file(conf_file_to_load)
530
- @config_checksum_on_disk = config_checksum
531
- end
532
- files_to_copy = []
533
- Log.dump(:available_presets, @config_presets, level: :trace1)
534
- Aspera.assert_type(@config_presets, Hash){'config file YAML'}
535
- # Check there is at least the config section
536
- Aspera.assert(@config_presets.key?(CONF_PRESET_CONFIG)){"Cannot find key: #{CONF_PRESET_CONFIG}"}
537
- version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION]
538
- raise Error, 'No version found in config section.' if version.nil?
539
- Log.log.debug{"conf version: #{version}"}
540
- # VVV if there are any conversion needed, those happen here.
541
- # Fix bug in 4.4 (creating key "true" in "default" preset)
542
- @config_presets[CONF_PRESET_DEFAULTS].delete(true) if @config_presets[CONF_PRESET_DEFAULTS].is_a?(Hash)
543
- # ^^^ Place new compatibility code before this line
544
- # Set version to current
545
- @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = Cli::VERSION
546
- unless files_to_copy.empty?
547
- Log.log.warn('Copying referenced files')
548
- files_to_copy.each do |file|
549
- FileUtils.cp(file, @main_folder)
550
- Log.log.warn{"#{file} -> #{@main_folder}"}
378
+ def action_documentation(location: :github, section: nil, **)
379
+ case location
380
+ when :github
381
+ section = "##{section}" unless section.nil?
382
+ Environment.instance.open_uri("#{Info::DOC_URL}#{section}")
383
+ return Result::Nothing.new
384
+ when :toc, :local
385
+ require 'aspera/markdown'
386
+ local_doc = File.join(self.class.gem_src_root, '..', 'docs', 'README.md')
387
+ Aspera.assert(File.exist?(local_doc), type: Cli::Error){"Local documentation not found: #{local_doc}"}
388
+ content = File.read(local_doc)
389
+ if location == :toc
390
+ entries = Markdown.toc(content)
391
+ return Result::ObjectList.new(entries, fields: %w[level title anchor])
551
392
  end
552
- end
553
- return
554
- rescue Psych::SyntaxError => e
555
- Log.log.error('YAML error in config file')
556
- raise e
557
- rescue StandardError => e
558
- Log.log.debug{"-> #{e.class.name} : #{e}"}
559
- if File.exist?(@option_config_file)
560
- # Then there is a problem with that file.
561
- new_name = "#{@option_config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
562
- File.rename(@option_config_file, new_name)
563
- Log.log.warn{"Renamed config file to #{new_name}."}
564
- Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
565
- end
566
- raise Cli::Error, e.to_s
567
- end
568
-
569
- def install_transfer_sdk
570
- asked_version = options.get_next_argument('transferd version', mandatory: false)
571
- sdk_url = options.get_option(:sdk_url, mandatory: true)
572
- sdk_url = nil if sdk_url.eql?(SpecialValues::DEF)
573
- name, version, folder = Ascp::Installation.instance.retrieve_sdk(url: sdk_url, version: asked_version)
574
- return Main.result_status("Installed #{name} version #{version} in #{folder}")
575
- end
576
-
577
- def execute_action_ascp
578
- command = options.get_next_command(%i[show products info install spec schema errors])
579
- case command
580
- when :show
581
- return Main.result_text(Ascp::Installation.instance.path(:ascp))
582
- when :info
583
- # Collect info from ascp executable
584
- data = Ascp::Installation.instance.ascp_info
585
- # Add command line transfer spec
586
- data['ts'] = transfer.user_transfer_spec
587
- # Add keys
588
- DataRepository::ELEMENTS.each_with_object(data){ |i, h| h[i.to_s] = DataRepository.instance.item(i)}
589
- # Declare those as secrets
590
- SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
591
- return Main.result_single_object(data)
592
- when :products
593
- command = options.get_next_command(%i[list])
594
- case command
595
- when :list
596
- return Main.result_object_list(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
393
+ # :local
394
+ if section
395
+ text = Markdown.extract_section(content, section)
396
+ Aspera.assert(!text.nil?, type: Cli::Error){"Section not found: #{section}"}
397
+ return Result::Text.new(text)
597
398
  end
598
- when :install
599
- return install_transfer_sdk
600
- when :spec
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
- when :schema
604
- schema = Transfer::Spec::SCHEMA.current.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
605
- agent = options.get_next_argument('transfer agent name', mandatory: false)
606
- schema['properties'] = schema['properties'].select{ |_k, v| CommandLineBuilder.supported_by_agent(agent, v)} unless agent.nil?
607
- schema['properties'] = schema['properties'].sort.to_h
608
- return Main.result_single_object(schema)
609
- when :errors
610
- error_data = []
611
- Ascp::Management::ERRORS.each_pair do |code, prop|
612
- error_data.push(code: code, mnemonic: prop[:c], retry: prop[:r], info: prop[:a])
399
+ if Environment.instance.url_method.eql?(:graphical)
400
+ Environment.instance.open_uri("file://#{local_doc}")
401
+ return Result::Nothing.new
613
402
  end
614
- return Main.result_object_list(error_data)
615
- else Aspera.error_unexpected_value(command)
403
+ return Result::Text.new(content)
616
404
  end
617
- Aspera.error_unreachable_line
618
405
  end
619
406
 
620
- def execute_action_transferd
621
- command = options.get_next_command(%i[list install])
622
- case command
623
- when :install
624
- return install_transfer_sdk
625
- when :list
626
- sdk_list = Ascp::Installation.instance.sdk_locations
627
- return Main.result_object_list(
628
- sdk_list,
629
- fields: sdk_list.first.keys - ['url']
630
- )
631
- else Aspera.error_unexpected_value(command)
632
- end
633
- Aspera.error_unreachable_line
407
+ def action_genkey(private_key_path:, private_key_length: OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH, **)
408
+ OAuth::Jwt.generate_rsa_private_key(path: private_key_path, length: private_key_length)
409
+ Result::Status.new("Generated #{private_key_length} bit RSA key: #{private_key_path}")
634
410
  end
635
411
 
636
- # Legacy actions available globally
637
- PRESET_GBL_ACTIONS = %i[list overview lookup secure].freeze
638
- # Operations requiring that preset exists
639
- PRESET_EXIST_ACTIONS = %i[show delete get unset].freeze
640
- # require id
641
- PRESET_INSTANCE_ACTIONS = %i[initialize update ask set].concat(PRESET_EXIST_ACTIONS).freeze
642
- PRESET_ALL_ACTIONS = (PRESET_GBL_ACTIONS + PRESET_INSTANCE_ACTIONS).freeze
643
-
644
- def execute_preset(action: nil, name: nil)
645
- action = options.get_next_command(PRESET_ALL_ACTIONS) if action.nil?
646
- name = options.instance_identifier if name.nil? && PRESET_INSTANCE_ACTIONS.include?(action)
647
- name = global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
648
- # Those operations require existing option
649
- raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
650
- case action
651
- when :list
652
- return Main.result_value_list(@config_presets.keys, name: 'name')
653
- when :overview
654
- # Display process modifies the value (hide secrets): we do not want to save removed secrets
655
- data = self.class.deep_clone(@config_presets)
656
- formatter.hide_secrets(data)
657
- result = []
658
- data.each do |config, preset|
659
- preset.each do |parameter, value|
660
- result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
661
- end
662
- end
663
- return Main.result_object_list(result, fields: CONF_OVERVIEW_KEYS)
664
- when :show
665
- return Main.result_single_object(self.class.deep_clone(@config_presets[name]))
666
- when :delete
667
- @config_presets.delete(name)
668
- return Main.result_status("Deleted: #{name}")
669
- when :get
670
- param_name = options.get_next_argument('parameter name')
671
- value = @config_presets[name][param_name]
672
- raise "no such option in preset #{name} : #{param_name}" if value.nil?
673
- case value
674
- when Numeric, String then return Main.result_text(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
675
- end
676
- return Main.result_single_object(value)
677
- when :unset
678
- param_name = options.get_next_argument('parameter name')
679
- @config_presets[name].delete(param_name)
680
- return Main.result_status("Removed: #{name}: #{param_name}")
681
- when :set
682
- param_name = options.get_next_argument('parameter name')
683
- param_name = Manager.option_line_to_name(param_name)
684
- param_value = options.get_next_argument('parameter value', validation: nil)
685
- set_preset_key(name, param_name, param_value)
686
- return Main.result_nothing
687
- when :initialize
688
- config_value = options.get_next_argument('extended value', validation: Hash)
689
- Log.log.warn{"configuration already exists: #{name}, overwriting"} if @config_presets.key?(name)
690
- @config_presets[name] = config_value
691
- return Main.result_status("Modified: #{@option_config_file}")
692
- when :update
693
- # get unprocessed options
694
- unprocessed_options = options.unprocessed_options_with_value
695
- Log.log.debug{"opts=#{unprocessed_options}"}
696
- @config_presets[name] ||= {}
697
- @config_presets[name].merge!(unprocessed_options)
698
- return Main.result_status("Updated: #{name}")
699
- when :ask
700
- options.ask_missing_mandatory = true
701
- @config_presets[name] ||= {}
702
- options.get_next_argument('option names', multiple: true).each do |option_name|
703
- option_value = options.get_interactive(option_name, check_option: true)
704
- @config_presets[name][option_name] = option_value
705
- end
706
- return Main.result_status("Updated: #{name}")
707
- when :lookup
708
- BasicAuth.declare_options(options)
709
- url = options.get_option(:url, mandatory: true)
710
- user = options.get_option(:username, mandatory: true)
711
- result = lookup_preset(url: url, username: user)
712
- raise Error, 'no such config found' if result.nil?
713
- return Main.result_single_object(result)
714
- when :secure
715
- identifier = options.get_next_argument('config name', mandatory: false)
716
- preset_names = identifier.nil? ? @config_presets.keys : [identifier]
717
- secret_keywords = %w[password secret].freeze
718
- preset_names.each do |preset_name|
719
- preset = @config_presets[preset_name]
720
- next unless preset.is_a?(Hash)
721
- preset.each_key do |option_name|
722
- secret_keywords.each do |keyword|
723
- next unless option_name.end_with?(keyword)
724
- vault_label = preset_name
725
- incr = 0
726
- until vault.get(label: vault_label, exception: false).nil?
727
- vault_label = "#{preset_name}#{incr}"
728
- incr += 1
729
- end
730
- to_set = {label: vault_label, password: preset[option_name]}
731
- puts "need to encode #{preset_name}.#{option_name} -> #{vault_label} -> #{to_set}"
732
- # to_copy=%i[]
733
- vault.set(to_set)
734
- preset[option_name] = "@vault:#{vault_label}.password"
735
- end
736
- end
737
- end
738
- return Main.result_status('Secrets secured in vault: Make sure to save the vault password securely.')
739
- end
412
+ def action_remote_certificate_chain(remote_url:, **)
413
+ remote_chain = Rest.remote_certificate_chain(remote_url, as_string: false)
414
+ Aspera.assert(remote_chain&.first){"No certificate found for #{remote_url}"}
415
+ Result::Text.new(remote_chain.map(&:to_pem).join("\n"))
740
416
  end
741
417
 
742
- ACTIONS = %i[
743
- preset
744
- open
745
- documentation
746
- genkey
747
- pubkey
748
- remote_certificate
749
- gem
750
- plugins
751
- tokens
752
- echo
753
- download
754
- wizard
755
- detect
756
- coffee
757
- image
758
- ascp
759
- sync
760
- transferd
761
- email_test
762
- smtp_settings
763
- proxy_check
764
- folder
765
- file
766
- check_update
767
- initdemo
768
- vault
769
- test
770
- platform
771
- ].freeze
772
-
773
- # Main action procedure for plugin
774
- def execute_action
775
- action = options.get_next_command(ACTIONS)
776
- case action
777
- when :preset # Newer syntax
778
- return execute_preset
779
- when :open
780
- Environment.instance.open_editor(@option_config_file.to_s)
781
- return Main.result_nothing
782
- when :documentation
783
- section = options.get_next_argument('private key file path', mandatory: false)
784
- section = "##{section}" unless section.nil?
785
- Environment.instance.open_uri("#{Info::DOC_URL}#{section}")
786
- return Main.result_nothing
787
- when :genkey # Generate new rsa key
788
- private_key_path = options.get_next_argument('private key file path')
789
- private_key_length = options.get_next_argument('size in bits', mandatory: false, validation: Integer, default: OAuth::Jwt::DEFAULT_PRIV_KEY_LENGTH)
790
- OAuth::Jwt.generate_rsa_private_key(path: private_key_path, length: private_key_length)
791
- return Main.result_status("Generated #{private_key_length} bit RSA key: #{private_key_path}")
792
- when :pubkey # Get pub key
793
- private_key_pem = options.get_next_argument('private key PEM value')
794
- return Main.result_text(OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s)
795
- when :remote_certificate
796
- cert_action = options.get_next_command(%i[chain only name])
797
- remote_url = options.get_next_argument('remote URL')
798
- remote_chain = Rest.remote_certificate_chain(remote_url, as_string: false)
799
- raise "No certificate found for #{remote_url}" unless remote_chain&.first
800
- case cert_action
801
- when :chain
802
- return Main.result_text(remote_chain.map(&:to_pem).join("\n"))
803
- when :only
804
- return Main.result_text(remote_chain.first.to_pem)
805
- when :name
806
- return Main.result_text(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
807
- end
808
- when :echo # Display the content of a value given on command line
809
- return Main.result_auto(options.get_next_argument('value', validation: nil))
810
- when :download
811
- file_url = options.get_next_argument('source URL').chomp
812
- file_dest = options.get_next_argument('file path', mandatory: false)
813
- file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, '')) if file_dest.nil?
814
- Log.log.info("Downloading: #{file_url}")
815
- Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
816
- return Main.result_status("Saved to: #{file_dest}")
817
- when :tokens
818
- require 'aspera/api/node'
819
- case options.get_next_command(%i{flush list show})
820
- when :flush
821
- return Main.result_value_list(OAuth::Factory.instance.flush_tokens, name: 'file')
822
- when :list
823
- return Main.result_object_list(OAuth::Factory.instance.persisted_tokens)
824
- when :show
825
- data = OAuth::Factory.instance.get_token_info(options.instance_identifier)
826
- raise Cli::Error, 'Unknown identifier' if data.nil?
827
- return Main.result_single_object(data)
828
- end
829
- when :plugins
830
- case options.get_next_command(%i[list create])
831
- when :list
832
- result = []
833
- Plugins::Factory.instance.plugin_list.each do |name|
834
- plugin_class = Plugins::Factory.instance.plugin_class(name)
835
- result.push({
836
- plugin: name,
837
- detect: TerminalFormatter.tick(plugin_class.respond_to?(:detect)),
838
- wizard: TerminalFormatter.tick(plugin_class.method_defined?(:wizard)),
839
- path: Plugins::Factory.instance.plugin_source(name)
840
- })
841
- end
842
- return Main.result_object_list(result, fields: %w[plugin detect wizard path])
843
- when :create
844
- plugin_name = options.get_next_argument('name').downcase
845
- destination_folder = options.get_next_argument('folder', mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
846
- plugin_file = File.join(destination_folder, "#{plugin_name}.rb")
847
- content = <<~END_OF_PLUGIN_CODE
848
- require 'aspera/cli/plugins/base'
849
- module Aspera
850
- module Cli
851
- module Plugins
852
- class #{plugin_name.snake_to_capital} < Base
853
- ACTIONS=[]
854
- def execute_action
855
- return Main.result_status('You called plugin #{plugin_name}')
856
- end
857
- end
858
- end
859
- end
860
- end
861
- END_OF_PLUGIN_CODE
862
- File.write(plugin_file, content)
863
- return Main.result_status("Created #{plugin_file}")
864
- end
865
- when :detect, :wizard
866
- # Interactive mode
867
- options.ask_missing_mandatory = true
868
- # Detect plugins by url and optional query
869
- apps = @wizard.identify_plugins_for_url.freeze
870
- return Main.result_object_list(apps) if action.eql?(:detect)
871
- return @wizard.find(apps)
872
- when :coffee
873
- return Main.result_image(COFFEE_IMAGE_URL)
874
- when :image
875
- return Main.result_image(options.get_next_argument('image URI or blob'))
876
- when :ascp
877
- execute_action_ascp
878
- when :sync
879
- case options.get_next_command(%i[spec admin translate])
880
- when :spec
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
- when :admin
884
- return execute_sync_admin
885
- when :translate
886
- return Main.result_single_object(Sync::Operations.args_to_conf(options.get_next_argument('async arguments', multiple: true)))
887
- else Aspera.error_unreachable_line
888
- end
889
- when :transferd
890
- execute_action_transferd
891
- when :gem
892
- case options.get_next_command(%i[path version name])
893
- when :path then return Main.result_text(self.class.gem_src_root)
894
- when :version then return Main.result_text(Cli::VERSION)
895
- when :name then return Main.result_text(Info::GEM_NAME)
896
- else Aspera.error_unreachable_line
897
- end
898
- when :folder
899
- return Main.result_text(@main_folder)
900
- when :file
901
- return Main.result_text(@option_config_file)
902
- when :email_test
903
- send_email_template(email_template_default: EMAIL_TEST_TEMPLATE)
904
- return Main.result_nothing
905
- when :smtp_settings
906
- return Main.result_single_object(email_settings)
907
- when :proxy_check
908
- # Ensure fpac was provided
909
- options.get_option(:fpac, mandatory: true)
910
- server_url = options.get_next_argument('server url')
911
- return Main.result_text(@pac_exec.get_proxies(server_url))
912
- when :check_update
913
- return Main.result_single_object(check_gem_version)
914
- when :initdemo
915
- if @config_presets.key?(DEMO_PRESET)
916
- Log.log.warn{"Demo server preset already present: #{DEMO_PRESET}"}
917
- else
918
- Log.log.info{"Creating Demo server preset: #{DEMO_PRESET}"}
919
- @config_presets[DEMO_PRESET] = {
920
- 'url' => "ssh://#{DEMO_SERVER}.asperasoft.com:33001",
921
- 'username' => ASPERA,
922
- 'ssAP'.downcase.reverse + 'drow'.reverse => DEMO_SERVER + ASPERA # cspell:disable-line
923
- }
924
- end
925
- @config_presets[CONF_PRESET_DEFAULTS] ||= {}
926
- if @config_presets[CONF_PRESET_DEFAULTS].key?(SERVER_COMMAND)
927
- Log.log.warn{"Server default preset already set to: #{@config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND]}"}
928
- Log.log.warn{"Use #{DEMO_PRESET} for demo: -P#{DEMO_PRESET}"} unless
929
- DEMO_PRESET.eql?(@config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND])
930
- else
931
- @config_presets[CONF_PRESET_DEFAULTS][SERVER_COMMAND] = DEMO_PRESET
932
- Log.log.info{"Setting server default preset to : #{DEMO_PRESET}"}
933
- end
934
- return Main.result_status('Done')
935
- when :vault then execute_vault
936
- when :test then return execute_test
937
- when :platform
938
- return Main.result_text(Environment.instance.architecture)
939
- else Aspera.error_unreachable_line
940
- end
418
+ def action_remote_certificate_only(remote_url:, **)
419
+ remote_chain = Rest.remote_certificate_chain(remote_url, as_string: false)
420
+ Aspera.assert(remote_chain&.first){"No certificate found for #{remote_url}"}
421
+ Result::Text.new(remote_chain.first.to_pem)
941
422
  end
942
423
 
943
- # @return [Hash] email server setting with defaults if not defined
944
- def email_settings
945
- smtp = options.get_option(:smtp, mandatory: true)
946
- # Change keys from string into symbol
947
- smtp = smtp.symbolize_keys
948
- unsupported = smtp.keys - SMTP_CONF_PARAMS
949
- raise Cli::Error, "Unsupported SMTP parameter: #{unsupported.join(', ')}, use: #{SMTP_CONF_PARAMS.join(', ')}" unless unsupported.empty?
950
- # Defaults
951
- # smtp[:ssl] = nil (false)
952
- smtp[:tls] = !smtp[:ssl] unless smtp.key?(:tls)
953
- smtp[:port] ||= if smtp[:tls]
954
- 587
955
- elsif smtp[:ssl]
956
- 465
957
- else
958
- 25
959
- end
960
- smtp[:from_email] ||= smtp[:username] if smtp.key?(:username)
961
- smtp[:from_name] ||= smtp[:from_email].sub(/@.*$/, '').gsub(/[^a-zA-Z]/, ' ').capitalize if smtp.key?(:username)
962
- smtp[:domain] ||= smtp[:from_email].sub(/^.*@/, '') if smtp.key?(:from_email)
963
- # Check minimum required
964
- %i[server port domain].each do |n|
965
- Aspera.assert(smtp.key?(n)){"Missing mandatory smtp parameter: #{n}"}
966
- end
967
- Log.log.debug{"smtp=#{smtp}"}
968
- return smtp
424
+ def action_remote_certificate_name(remote_url:, **)
425
+ remote_chain = Rest.remote_certificate_chain(remote_url, as_string: false)
426
+ Aspera.assert(remote_chain&.first){"No certificate found for #{remote_url}"}
427
+ Result::Text.new(remote_chain.first.subject.to_a.find{ |name, _, _| name == 'CN'}[1])
969
428
  end
970
429
 
971
- # Send email using ERB template
972
- # @param email_template_default [String] default template, can be overridden by option
973
- # @param values [Hash] values to be used in template, keys with default: to, from_name, from_email
974
- def send_email_template(email_template_default: nil, values: {})
975
- values[:to] ||= options.get_option(:notify_to, mandatory: true)
976
- notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
977
- mail_conf = email_settings
978
- values[:from_name] ||= mail_conf[:from_name]
979
- values[:from_email] ||= mail_conf[:from_email]
980
- %i[to from_email].each do |n|
981
- Aspera.assert_type(values[n], String){"Missing email parameter: #{n} in config"}
982
- end
983
- start_options = [mail_conf[:domain]]
984
- start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
985
- # Create a binding with only variables defined in values
986
- template_binding = Environment.empty_binding
987
- # Add variables to binding
988
- values.each do |k, v|
989
- Aspera.assert_type(k, Symbol)
990
- template_binding.local_variable_set(k, v)
991
- end
992
- # Execute template
993
- msg_with_headers = ERB.new(notify_template).result(template_binding)
994
- Log.dump(:msg_with_headers, msg_with_headers)
995
- require 'net/smtp'
996
- smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
997
- smtp.enable_starttls if mail_conf[:tls]
998
- smtp.enable_tls if mail_conf[:ssl]
999
- smtp.start(*start_options) do |smtp_session|
1000
- smtp_session.send_message(msg_with_headers, values[:from_email], values[:to])
1001
- end
1002
- nil
430
+ def action_download(file_url:, file_dest: nil, **)
431
+ file_url = file_url.chomp
432
+ file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, '')) if file_dest.nil?
433
+ Log.log.info("Downloading: #{file_url}")
434
+ Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
435
+ Result::Status.new("Saved to: #{file_dest}")
1003
436
  end
1004
437
 
1005
- # Save current configuration to config file
1006
- # @return true if file was saved
1007
- def save_config_file_if_needed
1008
- raise Error, 'no configuration loaded' if @config_presets.nil?
1009
- current_checksum = config_checksum
1010
- return false if @config_checksum_on_disk.eql?(current_checksum)
1011
- FileUtils.mkdir_p(@main_folder)
1012
- Environment.restrict_file_access(@main_folder)
1013
- Log.log.info{"Saving config file: #{@option_config_file}"}
1014
- Environment.write_file_restricted(@option_config_file, force: true){@config_presets.to_yaml}
1015
- @config_checksum_on_disk = current_checksum
1016
- return true
438
+ def action_tokens_show(token_id:, **)
439
+ require 'aspera/api/node'
440
+ data = OAuth::Factory.instance.get_token_info(token_id)
441
+ Aspera.assert(!data.nil?, type: Cli::Error){'Unknown identifier'}
442
+ Result::SingleObject.new(data)
1017
443
  end
1018
444
 
1019
- # @return [String] name if config_presets has default
1020
- # @return nil if there is no config or bypass default params
1021
- def get_plugin_default_config_name(plugin_name_sym)
1022
- Aspera.assert(!@config_presets.nil?){'config_presets shall be defined'}
1023
- if !@use_plugin_defaults
1024
- Log.log.debug('skip default config')
1025
- return
1026
- end
1027
- if !@config_presets.key?(CONF_PRESET_DEFAULTS)
1028
- Log.log.debug('No default section')
1029
- return
1030
- end
1031
- Aspera.assert_type(@config_presets[CONF_PRESET_DEFAULTS], Hash){'default section'}
1032
- if !@config_presets[CONF_PRESET_DEFAULTS].key?(plugin_name_sym.to_s)
1033
- Log.log.debug("No default for #{plugin_name_sym}")
1034
- return
445
+ def action_plugins_list
446
+ result = Plugins::Factory.instance.plugin_list.map do |name|
447
+ plugin_class = Plugins::Factory.instance.plugin_class(name)
448
+ {
449
+ plugin: name,
450
+ detect: TerminalFormatter.tick(plugin_class.respond_to?(:detect)),
451
+ wizard: TerminalFormatter.tick(plugin_class.method_defined?(:wizard)),
452
+ path: Plugins::Factory.instance.plugin_source(name)
453
+ }
1035
454
  end
1036
- default_config_name = @config_presets[CONF_PRESET_DEFAULTS][plugin_name_sym.to_s]
1037
- if !@config_presets.key?(default_config_name)
1038
- Log.log.error do
1039
- "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
1040
- "Please fix the issue: either create preset with one parameter:\n" \
1041
- "#{Info::CMD_NAME} config id #{default_config_name} init @json:'{}'\n" \
1042
- "or remove default:\n#{Info::CMD_NAME} config id default remove #{plugin_name_sym}"
1043
- end
1044
- raise Cli::Error, "No such preset: #{default_config_name}"
1045
- end
1046
- Aspera.assert_type(@config_presets[default_config_name], Hash, type: Cli::Error){'preset type'}
1047
- return default_config_name
455
+ Result::ObjectList.new(result, fields: %w[plugin detect wizard path])
1048
456
  end
1049
457
 
1050
- # @return [Hash] result of execution of vault command
1051
- def execute_vault
1052
- command = options.get_next_command(%i[info list show create delete password])
1053
- case command
1054
- when :info
1055
- return Main.result_single_object(vault.info)
1056
- when :list
1057
- # , fields: %w(label url username password description)
1058
- return Main.result_object_list(vault.list)
1059
- when :show
1060
- return Main.result_single_object(vault.get(label: options.get_next_argument('label')))
1061
- when :create
1062
- vault.set(options.get_next_argument('info', validation: Hash).symbolize_keys)
1063
- return Main.result_status('Secret added')
1064
- when :delete
1065
- label_to_delete = options.get_next_argument('label')
1066
- vault.delete(label: label_to_delete)
1067
- return Main.result_status("Secret deleted: #{label_to_delete}")
1068
- when :password
1069
- Aspera.assert(vault.respond_to?(:change_password)){'Vault does not support password change'}
1070
- vault.change_password(options.get_next_argument('new_password'))
1071
- return Main.result_status('Vault password updated')
1072
- end
458
+ def action_plugins_create(name:, folder: nil, **)
459
+ plugin_name = name.downcase
460
+ destination_folder = folder || File.join(context.main_folder, ASPERA_PLUGINS_FOLDERNAME)
461
+ plugin_file = File.join(destination_folder, "#{plugin_name}.rb")
462
+ content = <<~END_OF_PLUGIN_CODE
463
+ require 'aspera/cli/plugins/base'
464
+ module Aspera
465
+ module Cli
466
+ module Plugins
467
+ class #{plugin_name.snake_to_capital} < Base
468
+ command :example, description: 'example command', action: :action_example
469
+ def action_example
470
+ Result::Status.new('You called plugin #{plugin_name}')
471
+ end
472
+ end
473
+ end
474
+ end
475
+ end
476
+ END_OF_PLUGIN_CODE
477
+ File.write(plugin_file, content)
478
+ Result::Status.new("Created #{plugin_file}")
1073
479
  end
1074
480
 
1075
- # @return [String] value from vault matching <name>.<param>
1076
- def vault_value(name)
1077
- m = name.split('.')
1078
- raise BadArgument, 'vault name shall match <name>.<param>' unless m.length.eql?(2)
1079
- # This raise exception if label not found:
1080
- info = vault.get(label: m[0])
1081
- value = info[m[1].to_sym]
1082
- raise "no such entry value: #{m[1]}" if value.nil?
1083
- return value
481
+ def action_detect(url:, plugin_name: nil, **)
482
+ options.ask_missing_mandatory = true
483
+ apps = @wizard.identify_plugins_for_url(url: url, plugin_name: plugin_name).freeze
484
+ Result::ObjectList.new(apps)
1084
485
  end
1085
486
 
1086
- # @return [Object] vault, from options or cache
1087
- def vault
1088
- return @vault_instance unless @vault_instance.nil?
1089
- info = options.get_option(:vault).symbolize_keys
1090
- info[:type] ||= 'file'
1091
- require 'aspera/keychain/factory'
1092
- @vault_instance = Keychain::Factory.create(
1093
- info,
1094
- Info::CMD_NAME,
1095
- @main_folder,
1096
- options.get_option(:vault_password)
1097
- )
487
+ def action_wizard(url:, plugin_name: nil, preset_name: '', **)
488
+ options.ask_missing_mandatory = true
489
+ apps = @wizard.identify_plugins_for_url(url: url, plugin_name: plugin_name).freeze
490
+ @wizard.find(apps, preset_name: preset_name)
1098
491
  end
1099
492
 
1100
- # Artificially raise an exception for tests
1101
- def execute_test
1102
- case options.get_next_command(%i[throw web])
1103
- when :throw
1104
- # :type [String]
1105
- # Options
1106
- exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1107
- exception_text = options.get_next_argument('exception text', mandatory: true)
1108
- type = Object.const_get(exception_class_name)
1109
- Aspera.assert(type <= Exception){"#{type} is not an exception: #{type.class}"}
1110
- raise type, exception_text
1111
- when :web
493
+ def action_initdemo
494
+ cp = presets.config_presets
495
+ if cp.key?(DEMO_PRESET)
496
+ Log.log.warn{"Demo server preset already present: #{DEMO_PRESET}"}
497
+ else
498
+ Log.log.info{"Creating Demo server preset: #{DEMO_PRESET}"}
499
+ cp[DEMO_PRESET] = {
500
+ 'url' => "ssh://#{DEMO_SERVER}.asperasoft.com:33001",
501
+ 'username' => ASPERA,
502
+ 'ssAP'.downcase.reverse + 'drow'.reverse => DEMO_SERVER + ASPERA # cspell:disable-line
503
+ }
504
+ end
505
+ cp[PresetManager::Key::DEFAULTS] ||= {}
506
+ if cp[PresetManager::Key::DEFAULTS].key?(SERVER_COMMAND)
507
+ Log.log.warn{"Server default preset already set to: #{cp[PresetManager::Key::DEFAULTS][SERVER_COMMAND]}"}
508
+ Log.log.warn{"Use #{DEMO_PRESET} for demo: -P#{DEMO_PRESET}"} unless
509
+ DEMO_PRESET.eql?(cp[PresetManager::Key::DEFAULTS][SERVER_COMMAND])
510
+ else
511
+ cp[PresetManager::Key::DEFAULTS][SERVER_COMMAND] = DEMO_PRESET
512
+ Log.log.info{"Setting server default preset to : #{DEMO_PRESET}"}
513
+ end
514
+ Result::Status.new('Done')
515
+ end
516
+
517
+ def action_commands
518
+ commands = Plugins::Factory.instance.plugin_list.flat_map do |name|
519
+ plugin_class = Plugins::Factory.instance.plugin_class(name)
520
+ reg = plugin_class.command_registry
521
+ reg.all_paths.reject{ |path| reg.children_of(path).any?}.map do |path|
522
+ spec = reg[path]
523
+ arg_tokens = spec&.arguments.to_a.map do |a|
524
+ token =
525
+ if a.allowed
526
+ a.allowed.join('|')
527
+ else
528
+ a.name.to_s
529
+ end
530
+ token += '...' if a.multiple
531
+ a.mandatory ? "<#{token}>" : "[<#{token}>]"
532
+ end
533
+ syntax = ([name.to_s] + path.map(&:to_s) + arg_tokens).join(' ')
534
+ {
535
+ syntax: syntax,
536
+ description: spec&.description.to_s
537
+ }
538
+ end
1112
539
  end
540
+ Result::ObjectList.new(commands, fields: %w[syntax description])
1113
541
  end
1114
542
 
1115
- # Version of URL without trailing "/" and removing default port
1116
- def canonical_url(url)
1117
- url.chomp('/').sub(%r{^(https://[^/]+):443$}, '\1')
543
+ def action_options(plugin_name:, **)
544
+ # Instantiate the plugin so that it registers all its options in context.options
545
+ Plugins::Factory.instance.create(plugin_name.to_sym, context: context)
546
+ rows = context.options.declared_options.map do |sym, opt|
547
+ row = {
548
+ option: "--#{sym.to_s.tr('_', '-')}",
549
+ description: opt.description.to_s
550
+ }
551
+ row[:allowed] = opt.values.join('|') if opt.values&.any?
552
+ row[:deprecated] = opt.deprecation if opt.deprecation
553
+ row
554
+ end
555
+ Result::ObjectList.new(rows, fields: %w[option description allowed deprecated])
1118
556
  end
1119
557
 
1120
- # Look for a preset that has the corresponding URL and username
1121
- # @return the first one matching
1122
- def lookup_preset(url:, username:)
1123
- # Remove extra info to maximize match
1124
- url = canonical_url(url)
1125
- Log.log.debug{"Lookup preset for #{username}@#{url}"}
1126
- @config_presets.each_value do |v|
1127
- next unless v.is_a?(Hash)
1128
- conf_url = v['url'].is_a?(String) ? canonical_url(v['url']) : nil
1129
- return self.class.deep_clone(v) if conf_url.eql?(url) && v['username'].eql?(username)
1130
- end
1131
- nil
558
+ def action_test_throw(exception_class_name:, exception_text:, **)
559
+ type = Object.const_get(exception_class_name)
560
+ Aspera.assert(type <= Exception){"#{type} is not an exception: #{type.class}"}
561
+ raise type, exception_text
1132
562
  end
1133
563
 
1134
- # Lookup the corresponding secret for the given URL and usernames
1135
- # @param url [String] Server URL
1136
- # @param username [String] Username
1137
- # @return [String, nil] Secret if found
1138
- def lookup_secret(url:, username:)
1139
- secret = options.get_option(:secret)
1140
- if secret.eql?('PRESET')
1141
- conf = lookup_preset(url: url, username: username)
1142
- if conf.is_a?(Hash)
1143
- Log.log.debug{"Found preset #{conf} with URL and username"}
1144
- secret = conf['password']
564
+ def action_completion_bash(words: nil, **)
565
+ if words.nil? || words.empty?
566
+ # Level 0: propose plugin names
567
+ Plugins::Factory.instance.plugin_list.each{ |p| puts p}
568
+ else
569
+ plugin_sym = words.first.to_sym
570
+ plugin_class = begin
571
+ Plugins::Factory.instance.plugin_class(plugin_sym)
572
+ rescue StandardError
573
+ Process.exit(0)
1145
574
  end
575
+ # Navigate into the registry using remaining words as path
576
+ path = words[1..].map(&:to_sym)
577
+ plugin_class.command_registry.children_of(path).each_key{ |k| puts k}
1146
578
  end
1147
- return secret
579
+ Process.exit(0)
1148
580
  end
1149
- # Private
1150
- # Folder in $HOME for application files (config, cache)
581
+
582
+ # Folder in $HOME for application files (~/.aspera)
1151
583
  ASPERA_HOME_FOLDER_NAME = '.aspera'
1152
- # Default config file
584
+ # Default config file name
1153
585
  DEFAULT_CONFIG_FILENAME = 'config.yaml'
1154
- # Reserved preset names
1155
- CONF_PRESET_CONFIG = 'config'
1156
- CONF_PRESET_VERSION = 'version'
1157
- CONF_PRESET_DEFAULTS = 'default'
1158
- CONF_PRESET_GLOBAL = 'global_common_defaults'
1159
- # Special name to identify value of default
1160
- GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
1161
586
  CONF_GLOBAL_SYM = :config
1162
587
  # Folder containing custom plugins in user's config folder
1163
588
  ASPERA_PLUGINS_FOLDERNAME = 'plugins'
@@ -1166,7 +591,6 @@ module Aspera
1166
591
  REST_EXCEPTIONS_LOG_FILENAME = 'rest_exceptions.log'
1167
592
  ASPERA = 'aspera'
1168
593
  SERVER_COMMAND = 'server'
1169
- TRANSFERD_APP_NAME = 'sdk'
1170
594
  DEMO_SERVER = 'demo'
1171
595
  DEMO_PRESET = 'demoserver' # cspell: disable-line
1172
596
  EMAIL_TEST_TEMPLATE = <<~END_OF_TEMPLATE
@@ -1179,44 +603,22 @@ module Aspera
1179
603
  # Special extended values
1180
604
  EXTEND_PRESET = :preset
1181
605
  EXTEND_VAULT = :vault
1182
- EXTEND_ARGS = :''
1183
- PRESET_DIG_SEPARATOR = '.'
1184
- DEFAULT_CHECK_NEW_VERSION_DAYS = 7
1185
606
  COFFEE_IMAGE_URL = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
1186
- GEM_CHECK_DATE_FMT = '%Y/%m/%d'
1187
- # For testing only
1188
- SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
1189
- CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
1190
- SMTP_CONF_PARAMS = %i[server tls ssl port domain username password from_name from_email].freeze
1191
- CERT_EXT = %w[crt cer pem der].freeze
1192
607
  private_constant :ASPERA_HOME_FOLDER_NAME,
1193
608
  :DEFAULT_CONFIG_FILENAME,
1194
- :CONF_PRESET_CONFIG,
1195
- :CONF_PRESET_VERSION,
1196
- :CONF_PRESET_DEFAULTS,
1197
- :CONF_PRESET_GLOBAL,
609
+ :CONF_GLOBAL_SYM,
1198
610
  :ASPERA_PLUGINS_FOLDERNAME,
611
+ :PERSISTENCY_FOLDER,
1199
612
  :FILE_LIST_FOLDER_NAME,
1200
613
  :REST_EXCEPTIONS_LOG_FILENAME,
1201
614
  :ASPERA,
615
+ :SERVER_COMMAND,
1202
616
  :DEMO_SERVER,
1203
617
  :DEMO_PRESET,
1204
618
  :EMAIL_TEST_TEMPLATE,
1205
619
  :EXTEND_PRESET,
1206
620
  :EXTEND_VAULT,
1207
- :DEFAULT_CHECK_NEW_VERSION_DAYS,
1208
- :SERVER_COMMAND,
1209
- :PRESET_DIG_SEPARATOR,
1210
- :COFFEE_IMAGE_URL,
1211
- :SELF_SIGNED_CERT,
1212
- :PERSISTENCY_FOLDER,
1213
- :CONF_OVERVIEW_KEYS,
1214
- :SMTP_CONF_PARAMS,
1215
- :TRANSFERD_APP_NAME,
1216
- :GLOBAL_DEFAULT_KEYWORD,
1217
- :CONF_GLOBAL_SYM,
1218
- :GEM_CHECK_DATE_FMT,
1219
- :CERT_EXT
621
+ :COFFEE_IMAGE_URL
1220
622
  end
1221
623
  end
1222
624
  end