aspera-cli 4.23.0 → 4.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +37 -1
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +2109 -1300
  6. data/bin/ascli +2 -1
  7. data/bin/asession +4 -4
  8. data/lib/aspera/agent/base.rb +4 -0
  9. data/lib/aspera/agent/connect.rb +20 -18
  10. data/lib/aspera/agent/desktop.rb +14 -11
  11. data/lib/aspera/agent/direct.rb +39 -31
  12. data/lib/aspera/agent/httpgw.rb +2 -2
  13. data/lib/aspera/agent/node.rb +9 -11
  14. data/lib/aspera/agent/transferd.rb +18 -11
  15. data/lib/aspera/api/aoc.rb +44 -31
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +15 -18
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +22 -16
  20. data/lib/aspera/ascp/installation.rb +37 -40
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +54 -23
  23. data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
  24. data/lib/aspera/cli/error.rb +1 -1
  25. data/lib/aspera/cli/extended_value.rb +28 -29
  26. data/lib/aspera/cli/formatter.rb +191 -168
  27. data/lib/aspera/cli/hints.rb +29 -3
  28. data/lib/aspera/cli/main.rb +138 -107
  29. data/lib/aspera/cli/manager.rb +50 -30
  30. data/lib/aspera/cli/plugin.rb +148 -77
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +189 -70
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +100 -214
  35. data/lib/aspera/cli/plugins/console.rb +49 -18
  36. data/lib/aspera/cli/plugins/cos.rb +4 -4
  37. data/lib/aspera/cli/plugins/faspex.rb +45 -51
  38. data/lib/aspera/cli/plugins/faspex5.rb +164 -165
  39. data/lib/aspera/cli/plugins/faspio.rb +6 -5
  40. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  41. data/lib/aspera/cli/plugins/node.rb +144 -162
  42. data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
  43. data/lib/aspera/cli/plugins/preview.rb +26 -29
  44. data/lib/aspera/cli/plugins/server.rb +28 -28
  45. data/lib/aspera/cli/plugins/shares.rb +40 -28
  46. data/lib/aspera/cli/sync_actions.rb +101 -80
  47. data/lib/aspera/cli/transfer_agent.rb +51 -50
  48. data/lib/aspera/cli/transfer_progress.rb +29 -20
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +157 -0
  51. data/lib/aspera/colors.rb +13 -8
  52. data/lib/aspera/command_line_builder.rb +28 -22
  53. data/lib/aspera/command_line_converter.rb +31 -0
  54. data/lib/aspera/environment.rb +145 -101
  55. data/lib/aspera/faspex_gw.rb +1 -1
  56. data/lib/aspera/faspex_postproc.rb +3 -2
  57. data/lib/aspera/hash_ext.rb +1 -1
  58. data/lib/aspera/id_generator.rb +10 -10
  59. data/lib/aspera/keychain/base.rb +18 -0
  60. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  61. data/lib/aspera/keychain/factory.rb +9 -3
  62. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  63. data/lib/aspera/keychain/macos_security.rb +13 -13
  64. data/lib/aspera/log.rb +91 -19
  65. data/lib/aspera/nagios.rb +5 -6
  66. data/lib/aspera/node_simulator.rb +12 -7
  67. data/lib/aspera/oauth/base.rb +5 -3
  68. data/lib/aspera/oauth/factory.rb +24 -18
  69. data/lib/aspera/oauth/jwt.rb +13 -1
  70. data/lib/aspera/oauth/url_json.rb +3 -3
  71. data/lib/aspera/oauth/web.rb +5 -3
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -3
  74. data/lib/aspera/preview/generator.rb +25 -12
  75. data/lib/aspera/preview/terminal.rb +10 -7
  76. data/lib/aspera/preview/utils.rb +11 -9
  77. data/lib/aspera/products/connect.rb +1 -1
  78. data/lib/aspera/products/desktop.rb +1 -1
  79. data/lib/aspera/products/other.rb +2 -2
  80. data/lib/aspera/products/transferd.rb +8 -6
  81. data/lib/aspera/proxy_auto_config.rb +1 -1
  82. data/lib/aspera/rest.rb +29 -22
  83. data/lib/aspera/rest_call_error.rb +1 -1
  84. data/lib/aspera/resumer.rb +1 -1
  85. data/lib/aspera/secret_hider.rb +46 -40
  86. data/lib/aspera/ssh.rb +13 -3
  87. data/lib/aspera/sync/args.schema.yaml +102 -0
  88. data/lib/aspera/sync/conf.schema.yaml +701 -0
  89. data/lib/aspera/sync/database.rb +83 -0
  90. data/lib/aspera/sync/operations.rb +296 -0
  91. data/lib/aspera/temp_file_manager.rb +3 -2
  92. data/lib/aspera/transfer/error.rb +1 -1
  93. data/lib/aspera/transfer/error_info.rb +1 -2
  94. data/lib/aspera/transfer/faux_file.rb +11 -10
  95. data/lib/aspera/transfer/parameters.rb +6 -5
  96. data/lib/aspera/transfer/spec.rb +15 -1
  97. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  98. data/lib/aspera/transfer/spec_doc.rb +34 -16
  99. data/lib/aspera/transfer/uri.rb +5 -5
  100. data/lib/aspera/uri_reader.rb +14 -10
  101. data/lib/aspera/web_auth.rb +2 -2
  102. data/lib/aspera/web_server_simple.rb +2 -2
  103. data.tar.gz.sig +0 -0
  104. metadata +15 -13
  105. metadata.gz.sig +0 -0
  106. data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
  107. data/lib/aspera/transfer/convert.rb +0 -29
  108. data/lib/aspera/transfer/sync.rb +0 -232
  109. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
  110. data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
data/lib/aspera/colors.rb CHANGED
@@ -10,7 +10,7 @@ class String
10
10
  def vt_cmd(code); "\e[#{code}m"; end
11
11
  end
12
12
  # see https://en.wikipedia.org/wiki/ANSI_escape_code
13
- # symbol is the method name added to String
13
+ # symbol is the method name added to String, e.g. "hello".bold
14
14
  # it adds control chars to set color (and reset at the end).
15
15
  VT_STYLES = {
16
16
  bold: 1,
@@ -20,6 +20,7 @@ class String
20
20
  blink: 5,
21
21
  reverse_color: 7,
22
22
  invisible: 8,
23
+ strike: 9,
23
24
  black: 30,
24
25
  red: 31,
25
26
  green: 32,
@@ -38,21 +39,25 @@ class String
38
39
  bg_gray: 47
39
40
  }.freeze
40
41
  private_constant :VT_STYLES
41
- # defines methods to String, one per entry in VT_STYLES
42
+ # Defines methods to String, one per entry in VT_STYLES
42
43
  VT_STYLES.each do |name, code|
43
44
  if $stdout.tty?
44
45
  begin_seq = vt_cmd(code)
45
- end_code = 0 # by default reset all
46
- if code <= 7 then code + 20
47
- elsif code <= 37 then 39
48
- elsif code <= 47 then 49
49
- end
50
- end_seq = vt_cmd(end_code)
46
+ end_seq = vt_cmd(
47
+ if code <= 2 then 22
48
+ elsif code <= 8 then code + 20
49
+ elsif code <= 37 then 39
50
+ elsif code <= 47 then 49
51
+ else
52
+ 0 # by default reset all
53
+ end
54
+ )
51
55
  define_method(name){"#{begin_seq}#{self}#{end_seq}"}
52
56
  else
53
57
  define_method(name){self}
54
58
  end
55
59
  end
60
+ # Transform capitalized to snake case
56
61
  def capital_to_snake
57
62
  return gsub(/([a-z\d])([A-Z])/, '\1_\2')
58
63
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -13,19 +13,24 @@ module Aspera
13
13
  # type [String,Array] Accepted type(s) for non-enum
14
14
  # default [String] Default value if not specified
15
15
  # enum [Array] Set with list of values for enum types accepted in transfer spec
16
+ # items [Array]
17
+ # properties [Array]
16
18
  # x-cli-envvar [String] Name of env var
17
19
  # x-cli-option [String] Command line option (starts with "-")
18
20
  # x-cli-switch [Bool] true if option has no arg, else by default option has a value
19
21
  # x-cli-special [Bool] true if special handling (defered)
20
22
  # x-cli-convert [String,Hash] Method name for Convert object or Conversion for enum ts to arg
21
23
  # x-agents [Array] Supported agents (for doc only), if not specified: all
22
- # x-tspec [Bool,String] (async) true if same name in transfer spec, else name in transfer spec, else ignored
24
+ # x-ts-name [Bool,String] (async) true if same name in transfer spec, else real name in transfer spec, else ignored
25
+ # x-ts-convert [String] (async) Method name for Convert object
23
26
  # x-deprecation [String] Deprecation message for doc
24
- SCHEMA_KEYS = %w[
27
+ PROPERTY_KEYS = %w[
25
28
  description
26
29
  type
27
30
  default
28
31
  enum
32
+ items
33
+ properties
29
34
  required
30
35
  $comment
31
36
  x-cli-envvar
@@ -34,13 +39,13 @@ module Aspera
34
39
  x-cli-special
35
40
  x-cli-convert
36
41
  x-agents
37
- x-tspec
42
+ x-ts-name
38
43
  x-deprecation
39
44
  ].freeze
40
45
 
41
46
  CLI_AGENT = 'direct'
42
47
 
43
- private_constant :SCHEMA_KEYS, :CLI_AGENT
48
+ private_constant :PROPERTY_KEYS, :CLI_AGENT
44
49
 
45
50
  class << self
46
51
  # @return true if given agent supports that field
@@ -48,22 +53,23 @@ module Aspera
48
53
  !properties.key?('x-agents') || properties['x-agents'].include?(agent)
49
54
  end
50
55
 
51
- # Called by provider of definition before constructor of this class so that schema has all mandatory fields
52
- def read_schema(source_path, suffix=nil)
53
- suffix = "_#{suffix}" unless suffix.nil?
54
- schema = YAML.load_file("#{source_path[0..-4]}#{suffix}.schema.yaml")
55
- schema['properties'].each do |name, properties|
56
- Aspera.assert_type(properties, Hash){name}
57
- unsupported_keys = properties.keys - SCHEMA_KEYS
56
+ # fill default values
57
+ def adjust_properties_defaults(properties)
58
+ properties.each do |name, info|
59
+ Aspera.assert_type(info, Hash){"#{info.class} for #{name}"}
60
+ unsupported_keys = info.keys - PROPERTY_KEYS
58
61
  Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
59
62
  # by default : string, unless it's without arg
60
- properties['type'] ||= properties['x-cli-switch'] ? 'boolean' : 'string'
63
+ info['type'] ||= info['x-cli-switch'] ? 'boolean' : 'string'
61
64
  # add default cli option name if not present, and if supported in "direct".
62
- properties['x-cli-option'] = '--' + name.to_s.tr('_', '-') if !properties.key?('x-cli-option') && !properties['x-cli-envvar'] && (properties.key?('x-cli-switch') || supported_by_agent(CLI_AGENT, properties))
63
- properties.freeze
65
+ info['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if !info.key?('x-cli-option') && !info['x-cli-envvar'] && (info.key?('x-cli-switch') || supported_by_agent(CLI_AGENT, info))
66
+ info.freeze
64
67
  end
65
- schema['required'] = [] unless schema.key?('required')
66
- schema.freeze
68
+ end
69
+
70
+ # Called by provider of definition before constructor of this class so that schema has all mandatory fields
71
+ def read_schema(source_path, name)
72
+ YAML.load_file(File.join(File.dirname(source_path), "#{name}.schema.yaml"))
67
73
  end
68
74
  end
69
75
 
@@ -83,7 +89,7 @@ module Aspera
83
89
  # Change required-ness of property in schema
84
90
  def required(name, required)
85
91
  if required
86
- @schema['required'].push(name) unless @schema['required'].include?(name)
92
+ @schema['required'].push(name) unless @schema['required']&.include?(name)
87
93
  else
88
94
  @schema['required'].delete(name)
89
95
  end
@@ -100,7 +106,7 @@ module Aspera
100
106
  # set result
101
107
  env_args[:env].merge!(@result[:env])
102
108
  env_args[:args].concat(@result[:args])
103
- return nil
109
+ return
104
110
  end
105
111
 
106
112
  # add options directly to command line
@@ -132,7 +138,7 @@ module Aspera
132
138
  return
133
139
  end
134
140
  # check mandatory parameter (nil is valid value), TODO: change exception ?
135
- raise Transfer::Error, "Missing mandatory parameter: #{name}" if @schema['required'].include?(name) && !properties['x-cli-special'] && !@object.key?(name)
141
+ raise Transfer::Error, "Missing mandatory parameter: #{name}" if @schema['required']&.include?(name) && !properties['x-cli-special'] && !@object.key?(name)
136
142
  parameter_value = @object[name]
137
143
  # no default setting
138
144
  # parameter_value=properties['default'] if parameter_value.nil? and properties.has_key?('default')
@@ -145,17 +151,17 @@ module Aspera
145
151
  when 'object' then [Hash]
146
152
  when 'integer' then [Integer]
147
153
  when 'boolean' then [TrueClass, FalseClass]
148
- else Aspera.error_unexpected_value(properties['type'])
154
+ else Aspera.error_unexpected_value(properties['type']){"Property #{name}"}
149
155
  end
150
156
  end.flatten
151
157
  # check that value is of expected type
152
- raise Transfer::Error, "#{name} is : #{parameter_value.class} (#{parameter_value}), shall be #{properties['type']}, " \
158
+ raise Transfer::Error, "#{name} is #{parameter_value.class} (#{parameter_value}), shall be #{properties['type']}, " \
153
159
  unless parameter_value.nil? || expected_classes.include?(parameter_value.class)
154
160
  # special processing will be requested with type get_value
155
161
  @processed_parameters.push(name) if !properties['x-cli-special'] || read
156
162
 
157
163
  # process only non-nil values
158
- return nil if parameter_value.nil?
164
+ return if parameter_value.nil?
159
165
 
160
166
  # check that value is of an accepted type (string, integer, boolean)
161
167
  raise "Enum value #{parameter_value} is not allowed for #{name}" if properties.key?('enum') && !properties['enum'].include?(parameter_value)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/assert'
4
+ module Aspera
5
+ # conversion class for transfer spec values to CLI values (ascp)
6
+ class CommandLineConverter
7
+ class << self
8
+ # special encoding methods used in YAML (key: convert)
9
+ def remove_hyphen(value); value.tr('-', ''); end
10
+
11
+ # special encoding methods used in YAML (key: convert)
12
+ def json64(value); Base64.strict_encode64(JSON.generate(value)); end
13
+
14
+ # special encoding methods used in YAML (key: convert)
15
+ def base64(value); Base64.strict_encode64(value); end
16
+
17
+ # transform yes/no to true/false
18
+ def yes_to_true(value)
19
+ case value
20
+ when 'yes' then return true
21
+ when 'no' then return false
22
+ else Aspera.error_unexpected_value(value){'only: yes or no: '}
23
+ end
24
+ end
25
+
26
+ def kbps_to_bps(value)
27
+ 1000 * value
28
+ end
29
+ end
30
+ end
31
+ end
@@ -14,13 +14,12 @@ module Aspera
14
14
  class Environment
15
15
  include Singleton
16
16
 
17
- USER_INTERFACES = %i[text graphical].freeze
18
-
19
17
  OS_WINDOWS = :windows
20
18
  OS_MACOS = :osx
21
19
  OS_LINUX = :linux
22
20
  OS_AIX = :aix
23
21
  OS_LIST = [OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
22
+
24
23
  CPU_X86_64 = :x86_64
25
24
  CPU_ARM64 = :arm64
26
25
  CPU_PPC64 = :ppc64
@@ -32,60 +31,18 @@ module Aspera
32
31
  MEBI = 1024 * 1024
33
32
  BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
34
33
 
34
+ I18N_VARS = %w(LC_ALL LC_CTYPE LANG).freeze
35
+
36
+ # "/" is invalid on both Unix and Windows, other are Windows special characters
37
+ # See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
38
+ WINDOWS_FILENAME_INVALID_CHARACTERS = '<>:"/\\|?*'
39
+ REPLACE_CHARACTER = '_'
40
+
35
41
  class << self
36
42
  def ruby_version
37
43
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
38
44
  end
39
45
 
40
- def os
41
- case RbConfig::CONFIG['host_os']
42
- when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
43
- return OS_WINDOWS
44
- when /darwin/, /mac os/
45
- return OS_MACOS
46
- when /linux/
47
- return OS_LINUX
48
- when /aix/
49
- return OS_AIX
50
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
51
- end
52
- end
53
-
54
- def cpu
55
- case RbConfig::CONFIG['host_cpu']
56
- when /x86_64/, /x64/
57
- return CPU_X86_64
58
- when /powerpc/, /ppc64/
59
- return CPU_PPC64LE if os.eql?(OS_LINUX)
60
- return CPU_PPC64
61
- when /s390/
62
- return CPU_S390
63
- when /arm/, /aarch64/
64
- return CPU_ARM64
65
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
66
- end
67
- end
68
-
69
- # normalized architecture name
70
- # see constants: OS_* and CPU_*
71
- def architecture
72
- return "#{os}-#{cpu}"
73
- end
74
-
75
- # executable file extension for current OS
76
- def exe_file(name='')
77
- return "#{name}.exe" if os.eql?(OS_WINDOWS)
78
- return name
79
- end
80
-
81
- # on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
82
- # so, tell Ruby the right way
83
- def fix_home
84
- return unless os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
85
- ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
86
- Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
87
- end
88
-
89
46
  # empty variable binding for secure eval
90
47
  def empty_binding
91
48
  return Kernel.binding
@@ -120,9 +77,9 @@ module Aspera
120
77
  # @raise [Exception] if problem
121
78
  def secure_spawn(exec:, args: nil, env: nil, **options)
122
79
  Aspera.assert_type(exec, String)
123
- Aspera.assert_type(args, Array) unless args.nil?
124
- Aspera.assert_type(env, Hash) unless env.nil?
125
- Aspera.assert_type(options, Hash) unless options.nil?
80
+ Aspera.assert_type(args, Array, NilClass)
81
+ Aspera.assert_type(env, Hash, NilClass)
82
+ Aspera.assert_type(options, Hash, NilClass)
126
83
  Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
127
84
  # start ascp in separate process
128
85
  spawn_args = []
@@ -143,8 +100,8 @@ module Aspera
143
100
  # @return [String] PID of process
144
101
  def secure_execute(exec:, args: nil, env: nil, **system_args)
145
102
  Aspera.assert_type(exec, String)
146
- Aspera.assert_type(args, Array) unless args.nil?
147
- Aspera.assert_type(env, Hash) unless env.nil?
103
+ Aspera.assert_type(args, Array, NilClass)
104
+ Aspera.assert_type(env, Hash, NilClass)
148
105
  Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
149
106
  # start in separate process
150
107
  spawn_args = []
@@ -163,15 +120,17 @@ module Aspera
163
120
  # @param args [Array] arguments to executable
164
121
  # @param opts [Hash] options to capture3
165
122
  # @return stdout of executable or raise exception
166
- def secure_capture(exec:, args: [], **opts)
123
+ def secure_capture(exec:, args: [], exception: true, **opts)
167
124
  Aspera.assert_type(exec, String)
168
125
  Aspera.assert_type(args, Array)
169
126
  Aspera.assert_type(opts, Hash)
170
127
  Log.log.debug{log_spawn(exec: exec, args: args)}
128
+ Log.dump(:opts, opts, level: :trace2)
129
+ Log.dump(:ENV, ENV.to_h, level: :trace1)
171
130
  stdout, stderr, status = Open3.capture3(exec, *args, **opts)
172
131
  Log.log.debug{"status=#{status}, stderr=#{stderr}"}
173
132
  Log.log.trace1{"stdout=#{stdout}"}
174
- raise "process failed: #{status.exitstatus} : #{stderr}" unless status.success?
133
+ raise "process failed: #{status.exitstatus} (#{stderr})" if !status.success? && exception
175
134
  return stdout
176
135
  end
177
136
 
@@ -181,7 +140,7 @@ module Aspera
181
140
  # @param mode [Integer] the file mode (permissions)
182
141
  # @block [Proc] return the content to write to the file
183
142
  def write_file_restricted(path, force: false, mode: nil)
184
- Aspera.assert(block_given?, exception_class: Aspera::InternalError)
143
+ Aspera.assert(block_given?, type: Aspera::InternalError)
185
144
  if force || !File.exist?(path)
186
145
  # Windows may give error
187
146
  File.unlink(path) rescue nil
@@ -214,71 +173,156 @@ module Aspera
214
173
  $stdout.tty?
215
174
  end
216
175
 
217
- # @return :text or :graphical depending on the environment
218
- def default_gui_mode
219
- # assume not remotely connected on macos and windows
220
- return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
221
- # unix family
222
- return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
223
- return :text
176
+ # force locale to C so that unicode characters are not used
177
+ def force_terminal_c
178
+ I18N_VARS.each{ |var| ENV[var] = 'C'}
224
179
  end
225
180
 
226
- # open a URI in a graphical browser
227
- # command must be non blocking
228
- def open_uri_graphical(uri)
229
- case Environment.os
230
- when Environment::OS_MACOS then return system('open', uri.to_s)
231
- when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
232
- when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
233
- else
234
- raise "no graphical open method for #{Environment.os}"
235
- end
181
+ # @return true if we can display Unicode characters
182
+ # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
183
+ # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
184
+ def terminal_supports_unicode?
185
+ terminal? && I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
236
186
  end
187
+ end
188
+ attr_accessor :url_method, :file_illegal_characters
189
+ attr_reader :os, :cpu, :executable_extension, :default_gui_mode
190
+
191
+ def initialize
192
+ initialize_fields
193
+ end
237
194
 
238
- # open a file in an editor
239
- def open_editor(file_path)
240
- if ENV.key?('EDITOR')
241
- system(ENV['EDITOR'], file_path.to_s)
242
- elsif Environment.os.eql?(Environment::OS_WINDOWS)
243
- system('notepad.exe', %Q{"#{file_path}"})
195
+ # initialize fields from environment
196
+ def initialize_fields
197
+ @os =
198
+ case RbConfig::CONFIG['host_os']
199
+ when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
200
+ OS_WINDOWS
201
+ when /darwin/, /mac os/
202
+ OS_MACOS
203
+ when /linux/
204
+ OS_LINUX
205
+ when /aix/
206
+ OS_AIX
207
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
208
+ end
209
+ @cpu =
210
+ case RbConfig::CONFIG['host_cpu']
211
+ when /x86_64/, /x64/
212
+ CPU_X86_64
213
+ when /powerpc/, /ppc64/
214
+ @os.eql?(OS_LINUX) ? CPU_PPC64LE : CPU_PPC64
215
+ when /s390/
216
+ CPU_S390
217
+ when /arm/, /aarch64/
218
+ CPU_ARM64
219
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
220
+ end
221
+ @executable_extension = @os.eql?(OS_WINDOWS) ? 'exe' : nil
222
+ # :text or :graphical depending on the environment
223
+ @default_gui_mode =
224
+ if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(os) ||
225
+ (ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?)
226
+ # assume not remotely connected on macos and windows or unix family
227
+ :graphical
244
228
  else
245
- open_uri_graphical(file_path.to_s)
229
+ :text
246
230
  end
247
- end
231
+ @url_method = @default_gui_mode
232
+ @file_illegal_characters = REPLACE_CHARACTER + WINDOWS_FILENAME_INVALID_CHARACTERS
233
+ nil
248
234
  end
249
- attr_accessor :url_method
250
235
 
251
- def initialize
252
- @url_method = self.class.default_gui_mode
253
- @terminal_supports_unicode = nil
236
+ # Normalized architecture name
237
+ # See constants: OS_* and CPU_*
238
+ def architecture
239
+ "#{@os}-#{@cpu}"
254
240
  end
255
241
 
256
- # @return true if we can display Unicode characters
257
- # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
258
- # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
259
- def terminal_supports_unicode?
260
- @terminal_supports_unicode = self.class.terminal? && %w(LC_ALL LC_CTYPE LANG).any?{ |var| ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
261
- return @terminal_supports_unicode
242
+ # executable file extension for current OS
243
+ def exe_file(name)
244
+ return name unless @executable_extension
245
+ return "#{name}#{@executable_extension}"
262
246
  end
263
247
 
264
- # Allows a user to open a Url
265
- # if method is "text", then URL is displayed on terminal
266
- # if method is "graphical", then the URL will be opened with the default browser.
248
+ # on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
249
+ # so, tell Ruby the right way
250
+ def fix_home
251
+ return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
252
+ ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
253
+ Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
254
+ end
255
+
256
+ def graphical?
257
+ @default_gui_mode == :graphical
258
+ end
259
+
260
+ # Open a URI in a graphical browser
261
+ # Command must be non blocking
262
+ # @param uri [String] the URI to open
263
+ def open_uri_graphical(uri)
264
+ case @os
265
+ when Environment::OS_MACOS then return self.class.secure_execute(exec: 'open', args: [uri.to_s])
266
+ when Environment::OS_WINDOWS then return self.class.secure_execute(exec: 'start', args: ['explorer', %Q{"#{uri}"}])
267
+ when Environment::OS_LINUX then return self.class.secure_execute(exec: 'xdg-open', args: [uri.to_s])
268
+ else Assert.error_unexpected_value(os){'no graphical open method'}
269
+ end
270
+ end
271
+
272
+ # open a file in an editor
273
+ def open_editor(file_path)
274
+ if ENV.key?('EDITOR')
275
+ self.class.secure_execute(exec: ENV['EDITOR'], args: [file_path.to_s])
276
+ elsif @os.eql?(Environment::OS_WINDOWS)
277
+ self.class.secure_execute(exec: 'notepad.exe', args: [%Q{"#{file_path}"}])
278
+ else
279
+ open_uri_graphical(file_path.to_s)
280
+ end
281
+ end
282
+
283
+ # Allows a user to open a URL
284
+ # if method is :text, then URL is displayed on terminal
285
+ # if method is :graphical, then the URL will be opened with the default browser.
267
286
  # this is non blocking
268
287
  def open_uri(the_url)
269
288
  case @url_method
270
289
  when :graphical
271
- self.class.open_uri_graphical(the_url)
290
+ open_uri_graphical(the_url)
272
291
  when :text
273
292
  case the_url.to_s
274
293
  when /^http/
275
- puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
294
+ puts "USER ACTION: please enter this URL in a browser:\n#{the_url.to_s.red}\n"
276
295
  else
277
296
  puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
278
297
  end
279
- else
280
- raise StandardError, "unsupported url open method: #{@url_method}"
298
+ else Aspera.error_unexpected_value(@url_method){'URL open method'}
299
+ end
300
+ end
301
+
302
+ # Replacement character for illegal filename characters
303
+ # Can also be used as safe "join" character
304
+ def safe_filename_character
305
+ return REPLACE_CHARACTER if @file_illegal_characters.nil? || @file_illegal_characters.empty?
306
+ @file_illegal_characters[0]
307
+ end
308
+
309
+ # Sanitize a filename by replacing illegal characters
310
+ # @param filename [String] the original filename
311
+ # @return [String] A file name safe to use on file system
312
+ def sanitized_filename(filename)
313
+ safe_char = safe_filename_character
314
+ # Windows does not allow file name:
315
+ # - with control characters anywhere
316
+ # - ending with space or dot
317
+ filename = filename
318
+ .gsub(/[\x00-\x1F\x7F]/, safe_char)
319
+ .sub(/[. ]+\z/, safe_char)
320
+ if @file_illegal_characters&.size.to_i >= 2
321
+ # replace all illegal characters with safe_char
322
+ filename = filename.tr(@file_illegal_characters[1..-1], safe_char)
281
323
  end
324
+ # ensure only one safe_char is used at a time
325
+ return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
282
326
  end
283
327
  end
284
328
  end
@@ -66,7 +66,7 @@ module Aspera
66
66
  case request.path
67
67
  when '/aspera/faspex/send'
68
68
  begin
69
- raise 'no payload' if request.body.nil?
69
+ Aspera.assert(!request.body.nil?){'payload missing'}
70
70
  faspex_pkg_parameters = JSON.parse(request.body)
71
71
  Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
72
72
  # compare string, as class is not yet known here
@@ -14,7 +14,7 @@ module Aspera
14
14
  def initialize(server, parameters)
15
15
  Aspera.assert_type(parameters, Hash)
16
16
  @parameters = parameters.symbolize_keys
17
- Log.log.debug{Log.dump(:post_proc_parameters, @parameters)}
17
+ Log.dump(:post_proc_parameters, @parameters)
18
18
  not_allowed = @parameters.keys - ALLOWED_PARAMETERS
19
19
  raise "unsupported parameters: #{not_allowed.join(', ')}" unless not_allowed.empty?
20
20
  @parameters[:script_folder] ||= '.'
@@ -24,6 +24,7 @@ module Aspera
24
24
  Log.log.debug{'Faspex4PostProcServlet initialized'}
25
25
  end
26
26
 
27
+ # :reek:UncommunicativeMethodName
27
28
  def do_POST(request, response)
28
29
  Log.log.debug{"request=#{request.path}"}
29
30
  begin
@@ -46,7 +47,7 @@ module Aspera
46
47
  script_path = File.join(@parameters[:script_folder], script_file)
47
48
  Log.log.debug{"script=#{script_path}"}
48
49
  webhook_parameters = JSON.parse(request.body)
49
- Log.log.debug{Log.dump(:webhook_parameters, webhook_parameters)}
50
+ Log.dump(:webhook_parameters, webhook_parameters)
50
51
  # env expects only strings
51
52
  environment = webhook_parameters.each_with_object({}){ |(k, v), h| h[k] = v.to_s}
52
53
  post_proc_pid = Environment.secure_spawn(env: environment, exec: script_path)
@@ -9,7 +9,7 @@ class ::Hash
9
9
  merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
10
10
  end
11
11
 
12
- def deep_do(memory=nil, &block)
12
+ def deep_do(memory = nil, &block)
13
13
  each do |key, value|
14
14
  if value.is_a?(Hash)
15
15
  value.deep_do(memory, &block)
@@ -1,27 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/assert'
4
+ require 'aspera/environment'
4
5
  require 'uri'
5
6
 
6
7
  module Aspera
7
8
  class IdGenerator
8
- ID_SEPARATOR = '_'
9
- WINDOWS_PROTECTED_CHAR = %r{[/:"<>\\*?]}.freeze
10
- PROTECTED_CHAR_REPLACE = '_'
11
- private_constant :ID_SEPARATOR, :PROTECTED_CHAR_REPLACE, :WINDOWS_PROTECTED_CHAR
12
9
  class << self
10
+ # Generate an ID from a list of object IDs
11
+ # The generated ID is safe as file name
12
+ # @param object_id [Array<String>, String] the object IDs
13
+ # @return [String] the generated ID
13
14
  def from_list(object_id)
15
+ safe_char = Environment.instance.safe_filename_character
14
16
  if object_id.is_a?(Array)
15
17
  # compact: remove nils
16
- object_id = object_id.compact.map do |i|
18
+ object_id = object_id.flatten.compact.map do |i|
17
19
  i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
18
- end.join(ID_SEPARATOR)
20
+ end.join(safe_char)
19
21
  end
20
22
  Aspera.assert_type(object_id, String)
21
- return object_id
22
- .gsub(WINDOWS_PROTECTED_CHAR, PROTECTED_CHAR_REPLACE) # remove windows forbidden chars
23
- .gsub('.', PROTECTED_CHAR_REPLACE) # keep dot for extension only (nicer)
24
- .downcase
23
+ # keep dot for extension only (nicer)
24
+ return Environment.instance.sanitized_filename(object_id.gsub('.', safe_char)).downcase
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Keychain
5
+ class Base
6
+ CONTENT_KEYS = %i[label username password url description].freeze
7
+ def validate_set(options)
8
+ Aspera.assert_type(options, Hash){'options'}
9
+ unsupported = options.keys - CONTENT_KEYS
10
+ Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}, use #{CONTENT_KEYS.join(', ')}"}
11
+ options.each_pair do |k, v|
12
+ Aspera.assert_type(v, String){k.to_s}
13
+ end
14
+ Aspera.assert(options.key?(:label)){'label is required'}
15
+ end
16
+ end
17
+ end
18
+ end