aspera-cli 4.22.0 → 4.24.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 (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +405 -364
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +1856 -961
  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 +53 -43
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +23 -22
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +35 -21
  20. data/lib/aspera/ascp/installation.rb +43 -43
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +55 -24
  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 +38 -4
  28. data/lib/aspera/cli/main.rb +139 -108
  29. data/lib/aspera/cli/manager.rb +51 -31
  30. data/lib/aspera/cli/plugin.rb +149 -78
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +217 -88
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +105 -227
  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 +162 -163
  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 +233 -247
  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 +29 -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 +55 -58
  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 +160 -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/data_repository.rb +1 -0
  55. data/lib/aspera/environment.rb +144 -100
  56. data/lib/aspera/faspex_gw.rb +1 -1
  57. data/lib/aspera/faspex_postproc.rb +3 -2
  58. data/lib/aspera/hash_ext.rb +1 -1
  59. data/lib/aspera/id_generator.rb +10 -10
  60. data/lib/aspera/keychain/base.rb +18 -0
  61. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  62. data/lib/aspera/keychain/factory.rb +9 -3
  63. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  64. data/lib/aspera/keychain/macos_security.rb +13 -13
  65. data/lib/aspera/log.rb +70 -20
  66. data/lib/aspera/nagios.rb +5 -6
  67. data/lib/aspera/node_simulator.rb +12 -7
  68. data/lib/aspera/oauth/base.rb +6 -2
  69. data/lib/aspera/oauth/factory.rb +25 -18
  70. data/lib/aspera/oauth/jwt.rb +13 -1
  71. data/lib/aspera/oauth/url_json.rb +3 -3
  72. data/lib/aspera/oauth/web.rb +5 -3
  73. data/lib/aspera/persistency_folder.rb +2 -2
  74. data/lib/aspera/preview/file_types.rb +43 -35
  75. data/lib/aspera/preview/generator.rb +26 -13
  76. data/lib/aspera/preview/terminal.rb +10 -7
  77. data/lib/aspera/preview/utils.rb +11 -9
  78. data/lib/aspera/products/connect.rb +2 -1
  79. data/lib/aspera/products/desktop.rb +1 -1
  80. data/lib/aspera/products/other.rb +2 -2
  81. data/lib/aspera/products/transferd.rb +8 -6
  82. data/lib/aspera/proxy_auto_config.rb +1 -1
  83. data/lib/aspera/rest.rb +46 -28
  84. data/lib/aspera/rest_call_error.rb +1 -1
  85. data/lib/aspera/rest_error_analyzer.rb +1 -0
  86. data/lib/aspera/resumer.rb +1 -1
  87. data/lib/aspera/secret_hider.rb +46 -40
  88. data/lib/aspera/ssh.rb +14 -4
  89. data/lib/aspera/sync/args.schema.yaml +102 -0
  90. data/lib/aspera/sync/conf.schema.yaml +701 -0
  91. data/lib/aspera/sync/database.rb +83 -0
  92. data/lib/aspera/{transfer/sync.rb → sync/operations.rb} +145 -68
  93. data/lib/aspera/temp_file_manager.rb +4 -2
  94. data/lib/aspera/timer_limiter.rb +7 -5
  95. data/lib/aspera/transfer/error.rb +1 -1
  96. data/lib/aspera/transfer/error_info.rb +1 -2
  97. data/lib/aspera/transfer/faux_file.rb +11 -10
  98. data/lib/aspera/transfer/parameters.rb +6 -5
  99. data/lib/aspera/transfer/spec.rb +15 -1
  100. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  101. data/lib/aspera/transfer/spec_doc.rb +34 -16
  102. data/lib/aspera/transfer/uri.rb +5 -5
  103. data/lib/aspera/uri_reader.rb +14 -10
  104. data/lib/aspera/web_auth.rb +2 -2
  105. data/lib/aspera/web_server_simple.rb +2 -2
  106. data.tar.gz.sig +0 -0
  107. metadata +15 -15
  108. metadata.gz.sig +0 -0
  109. data/examples/dascli +0 -30
  110. data/examples/get_proto_file.rb +0 -8
  111. data/examples/proxy.pac +0 -60
  112. data/lib/aspera/transfer/convert.rb +0 -29
  113. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -13
  114. data/lib/aspera/transfer/sync_session.schema.yaml +0 -79
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
@@ -8,6 +8,7 @@ module Aspera
8
8
  # a simple binary data repository
9
9
  class DataRepository
10
10
  include Singleton
11
+
11
12
  # in same order as elements in folder
12
13
  ELEMENTS = %i[dsa rsa uuid aspera.global-cli-client aspera.drive license]
13
14
  START_INDEX = 1
@@ -13,13 +13,13 @@ module Aspera
13
13
  # detect OS, architecture, and specific stuff
14
14
  class Environment
15
15
  include Singleton
16
- USER_INTERFACES = %i[text graphical].freeze
17
16
 
18
17
  OS_WINDOWS = :windows
19
18
  OS_MACOS = :osx
20
19
  OS_LINUX = :linux
21
20
  OS_AIX = :aix
22
21
  OS_LIST = [OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
22
+
23
23
  CPU_X86_64 = :x86_64
24
24
  CPU_ARM64 = :arm64
25
25
  CPU_PPC64 = :ppc64
@@ -31,60 +31,18 @@ module Aspera
31
31
  MEBI = 1024 * 1024
32
32
  BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
33
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
+
34
41
  class << self
35
42
  def ruby_version
36
43
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
37
44
  end
38
45
 
39
- def os
40
- case RbConfig::CONFIG['host_os']
41
- when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
42
- return OS_WINDOWS
43
- when /darwin/, /mac os/
44
- return OS_MACOS
45
- when /linux/
46
- return OS_LINUX
47
- when /aix/
48
- return OS_AIX
49
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
50
- end
51
- end
52
-
53
- def cpu
54
- case RbConfig::CONFIG['host_cpu']
55
- when /x86_64/, /x64/
56
- return CPU_X86_64
57
- when /powerpc/, /ppc64/
58
- return CPU_PPC64LE if os.eql?(OS_LINUX)
59
- return CPU_PPC64
60
- when /s390/
61
- return CPU_S390
62
- when /arm/, /aarch64/
63
- return CPU_ARM64
64
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
65
- end
66
- end
67
-
68
- # normalized architecture name
69
- # see constants: OS_* and CPU_*
70
- def architecture
71
- return "#{os}-#{cpu}"
72
- end
73
-
74
- # executable file extension for current OS
75
- def exe_file(name='')
76
- return "#{name}.exe" if os.eql?(OS_WINDOWS)
77
- return name
78
- end
79
-
80
- # on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
81
- # so, tell Ruby the right way
82
- def fix_home
83
- return unless os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
84
- ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
85
- Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
86
- end
87
-
88
46
  # empty variable binding for secure eval
89
47
  def empty_binding
90
48
  return Kernel.binding
@@ -119,9 +77,9 @@ module Aspera
119
77
  # @raise [Exception] if problem
120
78
  def secure_spawn(exec:, args: nil, env: nil, **options)
121
79
  Aspera.assert_type(exec, String)
122
- Aspera.assert_type(args, Array) unless args.nil?
123
- Aspera.assert_type(env, Hash) unless env.nil?
124
- 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)
125
83
  Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
126
84
  # start ascp in separate process
127
85
  spawn_args = []
@@ -142,8 +100,8 @@ module Aspera
142
100
  # @return [String] PID of process
143
101
  def secure_execute(exec:, args: nil, env: nil, **system_args)
144
102
  Aspera.assert_type(exec, String)
145
- Aspera.assert_type(args, Array) unless args.nil?
146
- Aspera.assert_type(env, Hash) unless env.nil?
103
+ Aspera.assert_type(args, Array, NilClass)
104
+ Aspera.assert_type(env, Hash, NilClass)
147
105
  Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
148
106
  # start in separate process
149
107
  spawn_args = []
@@ -162,15 +120,17 @@ module Aspera
162
120
  # @param args [Array] arguments to executable
163
121
  # @param opts [Hash] options to capture3
164
122
  # @return stdout of executable or raise exception
165
- def secure_capture(exec:, args: [], **opts)
123
+ def secure_capture(exec:, args: [], exception: true, **opts)
166
124
  Aspera.assert_type(exec, String)
167
125
  Aspera.assert_type(args, Array)
168
126
  Aspera.assert_type(opts, Hash)
169
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)
170
130
  stdout, stderr, status = Open3.capture3(exec, *args, **opts)
171
131
  Log.log.debug{"status=#{status}, stderr=#{stderr}"}
172
132
  Log.log.trace1{"stdout=#{stdout}"}
173
- raise "process failed: #{status.exitstatus} : #{stderr}" unless status.success?
133
+ raise "process failed: #{status.exitstatus} (#{stderr})" if !status.success? && exception
174
134
  return stdout
175
135
  end
176
136
 
@@ -180,7 +140,7 @@ module Aspera
180
140
  # @param mode [Integer] the file mode (permissions)
181
141
  # @block [Proc] return the content to write to the file
182
142
  def write_file_restricted(path, force: false, mode: nil)
183
- Aspera.assert(block_given?, exception_class: Aspera::InternalError)
143
+ Aspera.assert(block_given?, type: Aspera::InternalError)
184
144
  if force || !File.exist?(path)
185
145
  # Windows may give error
186
146
  File.unlink(path) rescue nil
@@ -213,71 +173,155 @@ module Aspera
213
173
  $stdout.tty?
214
174
  end
215
175
 
216
- # @return :text or :graphical depending on the environment
217
- def default_gui_mode
218
- # assume not remotely connected on macos and windows
219
- return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
220
- # unix family
221
- return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
222
- 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'}
223
179
  end
224
180
 
225
- # open a URI in a graphical browser
226
- # command must be non blocking
227
- def open_uri_graphical(uri)
228
- case Environment.os
229
- when Environment::OS_MACOS then return system('open', uri.to_s)
230
- when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
231
- when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
232
- else
233
- raise "no graphical open method for #{Environment.os}"
234
- 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')}
235
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
236
194
 
237
- # open a file in an editor
238
- def open_editor(file_path)
239
- if ENV.key?('EDITOR')
240
- system(ENV['EDITOR'], file_path.to_s)
241
- elsif Environment.os.eql?(Environment::OS_WINDOWS)
242
- 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
243
228
  else
244
- open_uri_graphical(file_path.to_s)
229
+ :text
245
230
  end
246
- end
231
+ @url_method = @default_gui_mode
232
+ @file_illegal_characters = REPLACE_CHARACTER + WINDOWS_FILENAME_INVALID_CHARACTERS
233
+ nil
247
234
  end
248
- attr_accessor :url_method
249
235
 
250
- def initialize
251
- @url_method = self.class.default_gui_mode
252
- @terminal_supports_unicode = nil
236
+ # Normalized architecture name
237
+ # See constants: OS_* and CPU_*
238
+ def architecture
239
+ "#{@os}-#{@cpu}"
253
240
  end
254
241
 
255
- # @return true if we can display Unicode characters
256
- # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
257
- # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
258
- def terminal_supports_unicode?
259
- @terminal_supports_unicode = self.class.terminal? && %w(LC_ALL LC_CTYPE LANG).any?{ |var| ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
260
- 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}"
261
246
  end
262
247
 
263
- # Allows a user to open a Url
264
- # if method is "text", then URL is displayed on terminal
265
- # 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.
266
286
  # this is non blocking
267
287
  def open_uri(the_url)
268
288
  case @url_method
269
289
  when :graphical
270
- self.class.open_uri_graphical(the_url)
290
+ open_uri_graphical(the_url)
271
291
  when :text
272
292
  case the_url.to_s
273
293
  when /^http/
274
- 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"
275
295
  else
276
296
  puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
277
297
  end
278
- else
279
- 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 ending with space or dot
315
+ # nor control characters anywhere.
316
+ filename = filename
317
+ .gsub(/[\. ]+$/, safe_char)
318
+ .gsub(/[\x00-\x1F\x7F]/, safe_char)
319
+ if @file_illegal_characters&.size.to_i >= 2
320
+ # replace all illegal characters with safe_char
321
+ filename = filename.tr(@file_illegal_characters[1..-1], safe_char)
280
322
  end
323
+ # ensure only one safe_char is used at a time
324
+ return filename.gsub(/#{Regexp.escape(safe_char)}+/, safe_char).chomp(safe_char)
281
325
  end
282
326
  end
283
327
  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