aspera-cli 4.25.1 → 4.25.2

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 (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +6 -11
  5. data/README.md +31 -9742
  6. data/bin/asession +111 -88
  7. data/lib/aspera/agent/connect.rb +1 -1
  8. data/lib/aspera/agent/desktop.rb +1 -1
  9. data/lib/aspera/agent/direct.rb +19 -18
  10. data/lib/aspera/agent/node.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +25 -8
  12. data/lib/aspera/api/node.rb +16 -14
  13. data/lib/aspera/ascp/installation.rb +32 -51
  14. data/lib/aspera/assert.rb +1 -1
  15. data/lib/aspera/cli/extended_value.rb +1 -0
  16. data/lib/aspera/cli/formatter.rb +0 -4
  17. data/lib/aspera/cli/hints.rb +11 -4
  18. data/lib/aspera/cli/main.rb +3 -6
  19. data/lib/aspera/cli/manager.rb +8 -4
  20. data/lib/aspera/cli/plugins/aoc.rb +11 -14
  21. data/lib/aspera/cli/plugins/base.rb +1 -1
  22. data/lib/aspera/cli/plugins/config.rb +50 -48
  23. data/lib/aspera/cli/plugins/factory.rb +2 -2
  24. data/lib/aspera/cli/plugins/faspex5.rb +4 -3
  25. data/lib/aspera/cli/plugins/preview.rb +6 -11
  26. data/lib/aspera/cli/transfer_agent.rb +2 -2
  27. data/lib/aspera/cli/version.rb +1 -1
  28. data/lib/aspera/environment.rb +30 -16
  29. data/lib/aspera/faspex_gw.rb +1 -1
  30. data/lib/aspera/faspex_postproc.rb +16 -10
  31. data/lib/aspera/hash_ext.rb +8 -0
  32. data/lib/aspera/log.rb +3 -4
  33. data/lib/aspera/markdown.rb +17 -0
  34. data/lib/aspera/oauth/base.rb +1 -1
  35. data/lib/aspera/oauth/web.rb +1 -1
  36. data/lib/aspera/preview/generator.rb +9 -9
  37. data/lib/aspera/rest_call_error.rb +16 -8
  38. data/lib/aspera/rest_error_analyzer.rb +1 -1
  39. data/lib/aspera/transfer/resumer.rb +2 -2
  40. data/lib/aspera/yaml.rb +49 -0
  41. data.tar.gz.sig +0 -0
  42. metadata +16 -2
  43. metadata.gz.sig +0 -0
  44. data/release_notes.md +0 -8
@@ -30,10 +30,6 @@ module Aspera
30
30
  SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
31
31
 
32
32
  private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES
33
- # prefix to display error messages in user messages (terminal)
34
- ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
35
- WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
36
- HINT_FLASH = 'HINT:'.bg_green.gray.blink.freeze
37
33
 
38
34
  class << self
39
35
  # nicer display for boolean
@@ -26,7 +26,7 @@ module Aspera
26
26
  ]
27
27
  },
28
28
  {
29
- exception: RestCallError,
29
+ exception: Aspera::RestCallError,
30
30
  match: /Signature has expired/,
31
31
  remediation: [
32
32
  'There is too much time difference between your computer and the server',
@@ -108,6 +108,13 @@ module Aspera
108
108
  'Transfer user shall have those parameters in aspera.conf set to: token',
109
109
  'authorization_transfer_in_value authorization_transfer_out_value'
110
110
  ]
111
+ },
112
+ {
113
+ exception: Aspera::RestCallError,
114
+ match: /invalid_grant/,
115
+ remediation: [
116
+ 'Check your public key in your AoC user profile.'
117
+ ]
111
118
  }
112
119
  ]
113
120
  private_constant :ERROR_HINTS
@@ -129,9 +136,9 @@ module Aspera
129
136
  next unless message.match?(m)
130
137
  else Aspera.error_unexpected_value(m)
131
138
  end
132
- remediation = hint[:remediation]
133
- remediation = [remediation] unless remediation.is_a?(Array)
134
- remediation.each{ |r| formatter.display_message(:error, "#{Formatter::HINT_FLASH} #{r}")}
139
+ hint[:remediation].each do |r|
140
+ Log.log.info{"#{'HINT:'.bg_green.gray.blink.freeze} #{r}"}
141
+ end
135
142
  break
136
143
  end
137
144
  end
@@ -228,7 +228,7 @@ module Aspera
228
228
  # 1- processing of error condition
229
229
  unless exception_info.nil?
230
230
  Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
231
- @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
231
+ Log.log.error{"#{exception_info[:t]}: #{exception_info[:e].message}"}
232
232
  Log.log.debug{(['Backtrace:'] + exception_info[:e].backtrace).join("\n")} if exception_info[:debug]
233
233
  @context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
234
234
  # Is that a known error condition with proposal for remediation ?
@@ -237,7 +237,7 @@ module Aspera
237
237
  # 2- processing of command not processed (due to exception or bad command line)
238
238
  if execute_command || @option_show_config
239
239
  @context.options.final_errors.each do |msg|
240
- @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
240
+ Log.log.error{"Argument: #{msg}"}
241
241
  # Add code as exception if there is not already an error
242
242
  exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
243
243
  end
@@ -291,10 +291,7 @@ module Aspera
291
291
  @context.formatter.declare_options(@context.options)
292
292
  # Compare $0 with expected name
293
293
  current_prog_name = File.basename($PROGRAM_NAME)
294
- @context.formatter.display_message(
295
- :error,
296
- "#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"
297
- ) unless current_prog_name.eql?(Info::CMD_NAME)
294
+ Aspera.assert(current_prog_name.eql?(Info::CMD_NAME), type: :warn){"Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"}
298
295
  # Declare and parse global options
299
296
  declare_global_options
300
297
  # Do not display config commands if help is asked
@@ -35,7 +35,8 @@ module Aspera
35
35
  attr_accessor :values
36
36
 
37
37
  # @param option [Symbol] Name of option
38
- # @param allowed [see below] Allowed values
38
+ # @param description [String] Description for help
39
+ # @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
39
40
  # @param handler [Hash] Accessor: keys: :o(object) and :m(method)
40
41
  # @param deprecation [String] Deprecation message
41
42
  # `allowed`:
@@ -61,7 +62,7 @@ module Aspera
61
62
  else
62
63
  :setter
63
64
  end
64
- Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{method}"} unless @access.eql?(:local)
65
+ Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{@read_method}"} unless @access.eql?(:local)
65
66
  @types = nil
66
67
  @values = nil
67
68
  if !allowed.nil?
@@ -600,9 +601,12 @@ module Aspera
600
601
  end
601
602
 
602
603
  # TODO: use formatter
603
- # @return [String] comma separated list of values, with the current value highlighted
604
+ # Highlight current value in list
605
+ # @param list [Array<Symbol>] List of possible values
606
+ # @param current [Symbol] Current value
607
+ # @return [String] comma separated sorted list of values, with the current value highlighted
604
608
  def highlight_current_in_list(list, current)
605
- list.map do |i|
609
+ list.sort.map do |i|
606
610
  if i.eql?(current)
607
611
  $stdout.isatty ? i.to_s.red.bold : "[#{i}]"
608
612
  else
@@ -470,11 +470,11 @@ module Aspera
470
470
  return Main.result_success
471
471
  when :do
472
472
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
473
- return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::SCOPE_ADMIN)
473
+ return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::Scope::ADMIN)
474
474
  when :bearer_token
475
475
  node_api = aoc_api.node_api_from(
476
476
  node_id: res_id,
477
- scope: options.get_next_argument('scope')
477
+ scope: options.get_next_argument('scope', default: Api::Node::Scope::ADMIN)
478
478
  )
479
479
  return Main.result_text(node_api.oauth.authorization)
480
480
  when :dropbox
@@ -504,7 +504,7 @@ module Aspera
504
504
  node_id: shared_folder['node_id'],
505
505
  workspace_id: res_id,
506
506
  workspace_name: nil,
507
- scope: Api::Node::SCOPE_USER
507
+ scope: Api::Node::Scope::USER
508
508
  )
509
509
  result = node_api.read(
510
510
  'permissions',
@@ -742,9 +742,9 @@ module Aspera
742
742
  end
743
743
 
744
744
  # Create a shared link for the given entity
745
- # @param purpose_public [Symbol]
745
+ # @param purpose_public [String] send_package_to_dropbox or view_shared_file
746
746
  # @param shared_data [Hash] information for shared data
747
- # @param block [Proc] Optional: called on creation
747
+ # @param &block [Proc] Optional: called on creation
748
748
  def short_link_command(purpose_public:, **shared_data)
749
749
  link_type = options.get_next_argument('link type', accept_list: %i[public private])
750
750
  purpose_local = case link_type
@@ -780,6 +780,7 @@ module Aspera
780
780
  }
781
781
  end
782
782
  custom_data = value_create_modify(command: command, default: {})
783
+ access_levels = custom_data.delete('access_levels')
783
784
  if (pass = custom_data.delete('password'))
784
785
  entity_data[:data][:url_token_data][:password] = pass
785
786
  entity_data[:password_enabled] = true
@@ -787,7 +788,7 @@ module Aspera
787
788
  entity_data.deep_merge!(custom_data)
788
789
  result_create_short_link = aoc_api.create('short_links', entity_data)
789
790
  # public: Creation: permission on node
790
- yield(result_create_short_link['resource_id']) if block_given? && link_type.eql?(:public)
791
+ yield(result_create_short_link['resource_id'], access_levels) if block_given? && link_type.eql?(:public)
791
792
  return Main.result_single_object(result_create_short_link)
792
793
  when :list, :show
793
794
  query = if link_type.eql?(:private)
@@ -1052,13 +1053,13 @@ module Aspera
1052
1053
  when *Node::NODE4_READ_ACTIONS
1053
1054
  package_id = instance_identifier
1054
1055
  package_info = aoc_api.read("packages/#{package_id}")
1055
- return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::SCOPE_USER)
1056
+ return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1056
1057
  end
1057
1058
  when :files
1058
1059
  command_repo = options.get_next_command([:short_link].concat(NODE4_EXT_COMMANDS))
1059
1060
  case command_repo
1060
1061
  when *NODE4_EXT_COMMANDS
1061
- return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::SCOPE_USER)
1062
+ return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1062
1063
  when :short_link
1063
1064
  folder_dest = options.get_next_argument('path', validation: String)
1064
1065
  home_node_api = aoc_api.node_api_from(
@@ -1071,17 +1072,13 @@ module Aspera
1071
1072
  node_id: shared_apfid[:api].app_info[:node_info]['id'],
1072
1073
  file_id: shared_apfid[:file_id],
1073
1074
  **workspace_id_hash
1074
- ) do |resource_id|
1075
- # TODO: merge with node permissions ?
1076
- # TODO: access level as arg
1077
- access_levels = Api::Node::ACCESS_LEVELS # ['delete','list','mkdir','preview','read','rename','write']
1075
+ ) do |resource_id, access_levels|
1078
1076
  perm_data = {
1079
1077
  'file_id' => shared_apfid[:file_id],
1080
1078
  'access_id' => resource_id,
1081
1079
  'access_type' => 'user',
1082
- 'access_levels' => access_levels,
1080
+ 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1083
1081
  'tags' => {
1084
- # TODO: really just here ? not in tags.aspera.files.workspace ?
1085
1082
  'url_token' => true,
1086
1083
  'folder_name' => File.basename(folder_dest),
1087
1084
  'created_by_name' => aoc_api.current_user_info['name'],
@@ -40,7 +40,7 @@ module Aspera
40
40
  def initialize(context:)
41
41
  # Check presence in descendant of mandatory method and constant
42
42
  Aspera.assert(respond_to?(:execute_action), type: InternalError){"Missing method 'execute_action' in #{self.class}"}
43
- Aspera.assert(self.class.constants.include?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
43
+ Aspera.assert(self.class.const_defined?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
44
44
  @context = context
45
45
  add_manual_header if @context.man_header
46
46
  end
@@ -36,6 +36,7 @@ require 'digest'
36
36
  require 'open3'
37
37
  require 'date'
38
38
  require 'erb'
39
+ require 'net/http'
39
40
 
40
41
  module Aspera
41
42
  module Cli
@@ -107,11 +108,7 @@ module Aspera
107
108
  )
108
109
  options.parse_options!
109
110
  Log.log.debug{"#{Info::CMD_NAME} folder: #{@main_folder}"}
110
- # Data persistency manager, created by config plugin, set for global object
111
- context.persistency = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
112
- # Set folders for plugin lookup
113
- Plugins::Factory.instance.add_lookup_folder(self.class.gem_plugins_folder)
114
- Plugins::Factory.instance.add_lookup_folder(File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME))
111
+ setup_persistency_and_plugin_folders
115
112
  # Option to set config file
116
113
  options.declare(
117
114
  :config_file, 'Path to YAML file with preset configuration',
@@ -121,12 +118,7 @@ module Aspera
121
118
  options.parse_options!
122
119
  # Read config file (set @config_presets)
123
120
  read_config_file
124
- # Add preset handler (needed for smtp)
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)}
128
- # Load defaults before it can be overridden
129
- add_plugin_default_preset(CONF_GLOBAL_SYM)
121
+ setup_extended_value_handlers
130
122
  # Vault options
131
123
  options.declare(:secret, 'Secret for access keys')
132
124
  options.declare(:vault, 'Vault for secrets', allowed: Hash)
@@ -145,9 +137,8 @@ module Aspera
145
137
  options.declare(:sdk_url, 'Ascp: URL to get Aspera Transfer Executables', default: SpecialValues::DEF)
146
138
  options.parse_options!
147
139
  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
140
  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})
141
+ options.declare(:sdk_folder, 'Ascp: Path to folder with ascp (or product with "product:")', handler: {o: Products::Transferd, m: :sdk_directory})
151
142
  options.declare(:progress_bar, 'Display progress bar', allowed: Allowed::TYPES_BOOLEAN, default: Environment.terminal?)
152
143
  # Email options
153
144
  options.declare(:smtp, 'Email: SMTP configuration', allowed: Hash)
@@ -165,17 +156,39 @@ module Aspera
165
156
  options.declare(:proxy_credentials, 'HTTP proxy credentials for fpac: user, password', allowed: [Array, NilClass])
166
157
  options.parse_options!
167
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
168
179
  pac_script = options.get_option(:fpac)
169
- # Create PAC executor
170
- if !pac_script.nil?
171
- @pac_exec = ProxyAutoConfig.new(pac_script).register_uri_generic
172
- proxy_user_pass = options.get_option(:proxy_credentials)
173
- if !proxy_user_pass.nil?
174
- Aspera.assert(proxy_user_pass.length.eql?(2), type: Cli::BadArgument){"proxy_credentials shall have two elements (#{proxy_user_pass.length})"}
175
- @pac_exec.proxy_user = proxy_user_pass[0]
176
- @pac_exec.proxy_pass = proxy_user_pass[1]
177
- end
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]
178
188
  end
189
+ end
190
+
191
+ def setup_rest_and_transfer_runtime
179
192
  RestParameters.instance.user_agent = Info::CMD_NAME
180
193
  RestParameters.instance.progress_bar = @progress_bar
181
194
  RestParameters.instance.session_cb = lambda{ |http_session| update_http_session(http_session)}
@@ -197,12 +210,14 @@ module Aspera
197
210
  keys_to_delete.each{ |k| @option_http_options.delete(k)}
198
211
  OAuth::Factory.instance.persist_mgr = persistency if @option_cache_tokens
199
212
  OAuth::Web.additional_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
200
- Transfer::Parameters.file_list_folder = File.join(@main_folder, 'filelists')
201
- RestErrorAnalyzer.instance.log_file = File.join(@main_folder, 'rest_exceptions.log')
213
+ Transfer::Parameters.file_list_folder = File.join(@main_folder, FILELIST_FOLDERNAME)
214
+ RestErrorAnalyzer.instance.log_file = File.join(@main_folder, REST_EXCEPTIONS_LOG_FILENAME)
202
215
  # Register aspera REST call error handlers
203
216
  RestErrorsAspera.register_handlers
204
217
  end
205
218
 
219
+ public
220
+
206
221
  attr_accessor :main_folder, :option_cache_tokens, :option_insecure, :option_warn_insecure_cert, :option_http_options
207
222
  attr_reader :option_ignore_cert_host_port, :progress_bar
208
223
 
@@ -307,10 +322,7 @@ module Aspera
307
322
  if @option_warn_insecure_cert
308
323
  base_url = "https://#{address}:#{port}"
309
324
  if !@ssl_warned_urls.include?(base_url)
310
- formatter.display_message(
311
- :error,
312
- "#{Formatter::WARNING_FLASH} Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."
313
- )
325
+ Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
314
326
  @ssl_warned_urls.push(base_url)
315
327
  end
316
328
  end
@@ -416,7 +428,7 @@ module Aspera
416
428
  raise Cli::Error, "Preset already exists: #{preset_name} (use --override=yes or provide alternate name on command line)" \
417
429
  if !option_override && @config_presets.key?(preset_name)
418
430
  if option_default
419
- formatter.display_status("Setting config preset as default for #{plugin_name}")
431
+ Log.log.info("Setting config preset as default for #{plugin_name}")
420
432
  @config_presets[CONF_PRESET_DEFAULTS][plugin_name.to_s] = preset_name
421
433
  end
422
434
  @config_presets[preset_name] = preset_values
@@ -439,7 +451,7 @@ module Aspera
439
451
  Log.log.warn{"overwriting value for #{param_name}: #{selected_preset[param_name]}"}
440
452
  end
441
453
  selected_preset[param_name] = param_value
442
- formatter.display_status("Updated: #{preset}: #{param_name} <- #{param_value}")
454
+ Log.log.info("Updated: #{preset}: #{param_name} <- #{param_value}")
443
455
  nil
444
456
  end
445
457
 
@@ -591,24 +603,16 @@ module Aspera
591
603
  end
592
604
 
593
605
  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
606
  asked_version = options.get_next_argument('transferd version', mandatory: false)
597
607
  name, version, folder = Ascp::Installation.instance.install_sdk(url: options.get_option(:sdk_url, mandatory: true), version: asked_version)
598
608
  return Main.result_status("Installed #{name} version #{version} in #{folder}")
599
609
  end
600
610
 
601
611
  def execute_action_ascp
602
- command = options.get_next_command(%i[connect use show products info install spec schema errors])
612
+ command = options.get_next_command(%i[connect show products info install spec schema errors])
603
613
  case command
604
614
  when :connect
605
615
  return execute_connect_action
606
- when :use
607
- ascp_path = options.get_next_argument('path to ascp')
608
- Ascp::Installation.instance.ascp_path = ascp_path
609
- formatter.display_status("ascp version: #{Ascp::Installation.instance.get_ascp_version(ascp_path)}")
610
- set_global_default(:ascp_path, ascp_path)
611
- return Main.result_nothing
612
616
  when :show
613
617
  return Main.result_text(Ascp::Installation.instance.path(:ascp))
614
618
  when :info
@@ -622,15 +626,10 @@ module Aspera
622
626
  SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
623
627
  return Main.result_single_object(data)
624
628
  when :products
625
- command = options.get_next_command(%i[list use])
629
+ command = options.get_next_command(%i[list])
626
630
  case command
627
631
  when :list
628
632
  return Main.result_object_list(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
629
- when :use
630
- default_product = options.get_next_argument('product name')
631
- Ascp::Installation.instance.use_ascp_from_product(default_product)
632
- set_global_default(:ascp_path, "#{Ascp::Installation::USE_PRODUCT_PREFIX}#{default_product}")
633
- return Main.result_nothing
634
633
  end
635
634
  when :install
636
635
  return install_transfer_sdk
@@ -848,7 +847,7 @@ module Aspera
848
847
  file_url = options.get_next_argument('source URL').chomp
849
848
  file_dest = options.get_next_argument('file path', mandatory: false)
850
849
  file_dest = File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_url.gsub(%r{.*/}, '')) if file_dest.nil?
851
- formatter.display_status("Downloading: #{file_url}")
850
+ Log.log.info("Downloading: #{file_url}")
852
851
  Rest.new(base_url: file_url).call(operation: 'GET', save_to_file: file_dest)
853
852
  return Main.result_status("Saved to: #{file_dest}")
854
853
  when :tokens
@@ -1047,8 +1046,7 @@ module Aspera
1047
1046
  return false if @config_checksum_on_disk.eql?(current_checksum)
1048
1047
  FileUtils.mkdir_p(@main_folder)
1049
1048
  Environment.restrict_file_access(@main_folder)
1050
- Log.log.info{"Writing #{@option_config_file}"}
1051
- formatter.display_status('Saving config file.')
1049
+ Log.log.info{"Saving config file: #{@option_config_file}"}
1052
1050
  Environment.write_file_restricted(@option_config_file, force: true){@config_presets.to_yaml}
1053
1051
  @config_checksum_on_disk = current_checksum
1054
1052
  return true
@@ -1200,6 +1198,8 @@ module Aspera
1200
1198
  # Folder containing custom plugins in user's config folder
1201
1199
  ASPERA_PLUGINS_FOLDERNAME = 'plugins'
1202
1200
  PERSISTENCY_FOLDER = 'persist_store'
1201
+ FILELIST_FOLDERNAME = 'filelists'
1202
+ REST_EXCEPTIONS_LOG_FILENAME = 'rest_exceptions.log'
1203
1203
  ASPERA = 'aspera'
1204
1204
  SERVER_COMMAND = 'server'
1205
1205
  TRANSFERD_APP_NAME = 'sdk'
@@ -1232,6 +1232,8 @@ module Aspera
1232
1232
  :CONF_PRESET_DEFAULTS,
1233
1233
  :CONF_PRESET_GLOBAL,
1234
1234
  :ASPERA_PLUGINS_FOLDERNAME,
1235
+ :FILELIST_FOLDERNAME,
1236
+ :REST_EXCEPTIONS_LOG_FILENAME,
1235
1237
  :ASPERA,
1236
1238
  :DEMO_SERVER,
1237
1239
  :DEMO_PRESET,
@@ -20,9 +20,9 @@ module Aspera
20
20
  @plugins = {}
21
21
  end
22
22
 
23
- # @return list of registered plugins
23
+ # @return [Array<Symbol>] Sorted list of registered plugins
24
24
  def plugin_list
25
- @plugins.keys
25
+ @plugins.keys.sort
26
26
  end
27
27
 
28
28
  # add a folder to the list of folders to look for plugins
@@ -506,7 +506,7 @@ module Aspera
506
506
  )
507
507
  return Main.result_single_object(result)
508
508
  when :members, :saml_groups
509
- # :shared_inboxes, :workgroups
509
+ # res_command := :shared_inboxes, :workgroups
510
510
  res_id = instance_identifier{ |field, value| lookup_entity_by_field(api: @api_v5, entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
511
511
  res_path = "#{res_sym}/#{res_id}/#{res_command}"
512
512
  list_key = res_command.to_s
@@ -533,6 +533,7 @@ module Aspera
533
533
  access = options.get_next_argument('level', mandatory: false, accept_list: SHARED_INBOX_MEMBER_LEVELS, default: :standard)
534
534
  options.unshift_next_argument({user: users.map{ |u| {id: u, access: access}}})
535
535
  end
536
+ # TODO: test SAML group
536
537
  return entity_execute(
537
538
  api: @api_v5,
538
539
  entity: res_path,
@@ -541,11 +542,11 @@ module Aspera
541
542
  ) do |field, value|
542
543
  lookup_entity_by_field(
543
544
  api: @api_v5,
544
- entity: 'contacts',
545
+ entity: res_path,
545
546
  field: field,
546
547
  value: value,
547
548
  query: Rest.php_style({type: %w[user]})
548
- )['id']
549
+ )['user_id']
549
550
  end
550
551
  when :reset_password
551
552
  # :accounts
@@ -106,17 +106,12 @@ module Aspera
106
106
  Log.log.debug{"tmpdir: #{@tmp_folder}"}
107
107
  end
108
108
 
109
- # /files/id/files is normally cached in redis, but we can discard the cache
109
+ # /files/id/files is normally cached in Redis, but we can discard the cache
110
110
  # but /files/id is not cached
111
111
  def get_folder_entries(file_id, request_args = nil)
112
112
  headers = {'Accept' => Rest::MIME_JSON}
113
113
  headers['X-Aspera-Cache-Control'] = 'no-cache' if @option_folder_reset_cache.eql?(:header)
114
- return @api_node.call(
115
- operation: 'GET',
116
- subpath: "files/#{file_id}/files",
117
- headers: headers,
118
- query: request_args
119
- )
114
+ return @api_node.read("files/#{file_id}/files", request_args, headers: headers)
120
115
  end
121
116
 
122
117
  # old version based on folders
@@ -153,7 +148,7 @@ module Aspera
153
148
  end
154
149
  # log/persist periodically or last one
155
150
  next unless @periodic.trigger? || event.equal?(events.last)
156
- Log.log.info{"Processed event #{event['id']}"}
151
+ Log.log.debug{"Processed event #{event['id']}"}
157
152
  # save checkpoint to avoid losing processing in case of error
158
153
  if !iteration_persistency.nil?
159
154
  iteration_persistency.data[0] = event['id'].to_s
@@ -186,7 +181,7 @@ module Aspera
186
181
  end
187
182
  # log/persist periodically or last one
188
183
  next unless @periodic.trigger? || event.equal?(events.last)
189
- Log.log.info{"Processing event #{event['id']}"}
184
+ Log.log.debug{"Processing event #{event['id']}"}
190
185
  # save checkpoint to avoid losing processing in case of error
191
186
  if !iteration_persistency.nil?
192
187
  iteration_persistency.data[0] = event['id'].to_s
@@ -303,7 +298,7 @@ module Aspera
303
298
  # download original file to temp folder
304
299
  do_transfer(Transfer::Spec::DIRECTION_RECEIVE, entry['parent_file_id'], entry['name'], @tmp_folder)
305
300
  end
306
- Log.log.info{"source: #{entry['id']}: #{entry['path']}"}
301
+ Log.log.debug{"source: #{entry['id']}: #{entry['path']}"}
307
302
  gen_infos.each do |gen_info|
308
303
  gen_info[:generator].generate rescue nil
309
304
  end
@@ -335,7 +330,7 @@ module Aspera
335
330
  entry = entries_to_process.shift
336
331
  # process this entry only if it is within the top_path
337
332
  entry_path_with_slash = entry['path']
338
- Log.log.info{"processing entry #{entry_path_with_slash}"} if @periodic.trigger?
333
+ Log.log.debug{"processing entry #{entry_path_with_slash}"} if @periodic.trigger?
339
334
  entry_path_with_slash = "#{entry_path_with_slash}/" unless entry_path_with_slash.end_with?('/')
340
335
  if !top_path.nil? && !top_path.start_with?(entry_path_with_slash) && !entry_path_with_slash.start_with?(top_path)
341
336
  Log.log.debug{"#{entry['path']} folder (skip start)".bg_red}
@@ -33,8 +33,8 @@ module Aspera
33
33
  :DEFAULT_TRANSFER_NOTIFY_TEMPLATE
34
34
 
35
35
  class << self
36
- # @return :success if all sessions statuses returned by "start" are success
37
- # else return the first error exception object
36
+ # @return [:success] if all sessions statuses returned by "start" are success
37
+ # @return [Exception] if one sessions statuses returned by "start" is failed
38
38
  def session_status(statuses)
39
39
  error_statuses = statuses.reject{ |i| i.eql?(:success)}
40
40
  return :success if error_statuses.empty?
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # For beta add extension : .beta1
6
6
  # For dev version add extension : .pre
7
- VERSION = '4.25.1'
7
+ VERSION = '4.25.2'
8
8
  end
9
9
  end
@@ -58,8 +58,8 @@ module Aspera
58
58
  # @param code [String] Ruby code to execute
59
59
  # @param file [String] File name for error reporting
60
60
  # @param line [Integer] Line number for error reporting
61
- def secure_eval(code, file, line)
62
- Kernel.send('lave'.reverse, code, empty_binding, file, line)
61
+ def secure_eval(code, file, line, user_binding = nil)
62
+ Kernel.send('lave'.reverse, code, user_binding || empty_binding, file, line)
63
63
  end
64
64
 
65
65
  # Build argv for Process.spawn / Kernel.system (no shell)
@@ -74,26 +74,40 @@ module Aspera
74
74
  argv
75
75
  end
76
76
 
77
- # Execute a process securely (no shell)
78
- # mode:
79
- # :execute -> Kernel.system, return nil
80
- # :background -> Process.spawn, return pid
81
- # :capture -> Open3.capture3, return stdout
82
- # @param cmd [Array] Command and arguments
83
- # @param env [Hash, nil] Environment variables
84
- # @param mode [Symbol] Execution mode (see above)
85
- # @param kwargs [Hash] Additional arguments to underlying method, includes:
86
- # :exception [Boolean] for :capture mode, raise error if process fails
87
- # :close_others [Boolean] for :background mode
88
- # :env [Hash] for :execute mode
77
+ # like `Shellwords.shellescape`, but does not escape `=`
78
+ def shell_escape_pretty(str)
79
+ # Safe unquoted characters + '=' explicitly allowed
80
+ return str if str.match?(%r{\A[A-Za-z0-9_.,:/@+=-]+\z})
81
+ # return str if Shellwords.shellescape(str) == str
82
+
83
+ # Otherwise use single quotes
84
+ "'#{str.gsub("'", %q('\'\''))}'"
85
+ end
86
+
87
+ # Execute a process securely without shell.
88
+ #
89
+ # Execution `mode` can be:
90
+ # - :execute -> Kernel.system, return nil
91
+ # - :background -> Process.spawn, return pid
92
+ # - :capture -> Open3.capture3, return stdout
93
+ #
94
+ # @param cmd [Array<String>] Executable and arguments (mapped "to_s")
95
+ # @param mode [:execute,:background,:capture] Execution mode
96
+ # @param kwargs [Hash] Additional arguments to underlying method
97
+ # @option kwargs [Hash] :env Environment variables
98
+ # @option kwargs [Boolean] :exception for :capture mode, raise error if process fails
99
+ # @option kwargs [Boolean] :close_others for :background mode
100
+ # @return [nil] for :execute mode
101
+ # @return [Integer] pid for :background mode
102
+ # @return [String] stdout for :capture mode
89
103
  def secure_execute(*cmd, mode: :execute, **kwargs)
90
104
  cmd = cmd.map(&:to_s)
91
105
  Aspera.assert(cmd.size.positive?, type: ArgumentError){'executable must be present'}
92
106
  Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
93
107
  Log.log.debug do
94
108
  parts = [mode.to_s, 'command:']
95
- kwargs[:env]&.each{ |k, v| parts << "#{k}=#{Shellwords.shellescape(v.to_s)}"}
96
- cmd.each{ |a| parts << Shellwords.shellescape(a)}
109
+ kwargs[:env]&.each{ |k, v| parts << "#{k}=#{shell_escape_pretty(v.to_s)}"}
110
+ cmd.each{ |a| parts << shell_escape_pretty(a)}
97
111
  parts.join(' ')
98
112
  end
99
113
  case mode
@@ -80,7 +80,7 @@ module Aspera
80
80
  faspex4_send_to_faspex5(faspex_pkg_parameters)
81
81
  else Aspera.error_unexpected_value(@app_api.class.name)
82
82
  end
83
- Log.log.info{"faspex_package_create_result=#{faspex_package_create_result}"}
83
+ Log.log.debug{"faspex_package_create_result=#{faspex_package_create_result}"}
84
84
  response.status = 200
85
85
  response.content_type = Rest::MIME_JSON
86
86
  response.body = JSON.generate(faspex_package_create_result)