aspera-cli 4.27.3 → 4.27.5

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 (79) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +118 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +1 -0
  6. data/docs/README.md +224 -42
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +0 -4
  9. data/lib/aspera/agent/desktop.rb +0 -4
  10. data/lib/aspera/agent/direct.rb +49 -21
  11. data/lib/aspera/agent/node.rb +6 -5
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/faspex.rb +2 -0
  14. data/lib/aspera/api/httpgw.rb +1 -2
  15. data/lib/aspera/api/node.rb +3 -3
  16. data/lib/aspera/ascp/installation.rb +1 -1
  17. data/lib/aspera/cli/async_transfer_store.rb +10 -9
  18. data/lib/aspera/cli/bootstrapper.rb +3 -1
  19. data/lib/aspera/cli/command_registry.rb +69 -7
  20. data/lib/aspera/cli/command_spec.rb +1 -1
  21. data/lib/aspera/cli/extended_value.rb +4 -3
  22. data/lib/aspera/cli/formatter.rb +10 -8
  23. data/lib/aspera/cli/http.rb +8 -20
  24. data/lib/aspera/cli/option_types.rb +3 -1
  25. data/lib/aspera/cli/option_value.rb +16 -19
  26. data/lib/aspera/cli/options.schema.yaml +86 -10
  27. data/lib/aspera/cli/parser.rb +30 -18
  28. data/lib/aspera/cli/plugins/aoc.rb +133 -156
  29. data/lib/aspera/cli/plugins/ats.rb +1 -7
  30. data/lib/aspera/cli/plugins/base.rb +40 -34
  31. data/lib/aspera/cli/plugins/config.rb +24 -11
  32. data/lib/aspera/cli/plugins/console.rb +1 -1
  33. data/lib/aspera/cli/plugins/faspex5.rb +30 -11
  34. data/lib/aspera/cli/plugins/faspio.rb +1 -1
  35. data/lib/aspera/cli/plugins/node.rb +26 -19
  36. data/lib/aspera/cli/plugins/orchestrator.rb +73 -44
  37. data/lib/aspera/cli/plugins/preview.rb +21 -19
  38. data/lib/aspera/cli/plugins/server.rb +3 -4
  39. data/lib/aspera/cli/plugins/shares.rb +21 -24
  40. data/lib/aspera/cli/preset_actions.rb +28 -18
  41. data/lib/aspera/cli/preset_manager.rb +34 -19
  42. data/lib/aspera/cli/prompt.rb +2 -1
  43. data/lib/aspera/cli/result.rb +33 -22
  44. data/lib/aspera/cli/runner.rb +1 -5
  45. data/lib/aspera/cli/sync_actions.rb +17 -16
  46. data/lib/aspera/cli/transfer_actions.rb +14 -3
  47. data/lib/aspera/cli/transfer_agent.rb +6 -4
  48. data/lib/aspera/cli/transfer_progress.rb +290 -55
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +1 -1
  51. data/lib/aspera/coverage.rb +0 -1
  52. data/lib/aspera/environment.rb +29 -5
  53. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  54. data/lib/aspera/keychain/factory.rb +2 -1
  55. data/lib/aspera/log.rb +25 -2
  56. data/lib/aspera/node_emulator.rb +759 -0
  57. data/lib/aspera/oauth/base.rb +2 -1
  58. data/lib/aspera/oauth/factory.rb +6 -3
  59. data/lib/aspera/oauth/json_credentials.rb +34 -0
  60. data/lib/aspera/oauth/jwt.rb +3 -4
  61. data/lib/aspera/oauth.rb +1 -0
  62. data/lib/aspera/persistency_folder.rb +1 -3
  63. data/lib/aspera/preview/generator.rb +4 -1
  64. data/lib/aspera/preview/options.schema.yaml +119 -0
  65. data/lib/aspera/rest/aspera_errors.rb +12 -0
  66. data/lib/aspera/rest/client.rb +28 -19
  67. data/lib/aspera/rest/list.rb +14 -8
  68. data/lib/aspera/rest/parameters.rb +2 -2
  69. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  70. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  71. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  72. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  73. data/lib/aspera/schema/reader.rb +12 -18
  74. data/lib/aspera/schema/registry.rb +6 -1
  75. data.tar.gz.sig +0 -0
  76. metadata +5 -3
  77. metadata.gz.sig +0 -0
  78. data/lib/aspera/node_simulator.rb +0 -345
  79. data/lib/aspera/preview/options.rb +0 -45
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'monitor'
4
+ require 'aspera/environment'
3
5
  require 'aspera/log'
4
6
  require 'aspera/assert'
5
7
  require 'ruby-progressbar'
@@ -7,95 +9,328 @@ require 'ruby-progressbar'
7
9
  module Aspera
8
10
  module Cli
9
11
  # Progress bar for transfers.
10
- # Supports multi-session.
12
+ # Supports multi-session: events may come from several threads.
11
13
  # Note that we can have this case:
12
- # 2 sessions (-C x:2), but one session fails and restarts...
14
+ # 2 sessions (-C x:2), but one session fails and restarts (resume): it restarts with the same session id.
15
+ # Errors are logged, never raised: the progress bar shall not fail a transfer.
13
16
  class TransferProgress
14
- def initialize
15
- @progress_bar = nil
16
- # Key is session id
17
- @sessions = {}
18
- @completed = false
19
- @title = nil
17
+ # Rate is computed on this sliding window
18
+ RATE_WINDOW_SEC = 5.0
19
+ # Rate is not displayed on a shorter period
20
+ RATE_MIN_SEC = 1.0
21
+ # Decimal units, like `ascp`
22
+ KILO = 1000
23
+ BITS_PER_MEGABIT = KILO * KILO
24
+ BYTE_UNITS = %w[B KB MB GB TB PB].freeze
25
+ private_constant :RATE_WINDOW_SEC, :RATE_MIN_SEC, :KILO, :BITS_PER_MEGABIT, :BYTE_UNITS
26
+
27
+ class << self
28
+ # @param bytes [Integer] Size in bytes
29
+ # @return [String] Size with decimal unit, e.g. `12.3 MB`
30
+ def format_bytes(bytes)
31
+ value = bytes.to_f
32
+ index = 0
33
+ while value >= KILO && index < BYTE_UNITS.length - 1
34
+ value /= KILO
35
+ index += 1
36
+ end
37
+ return "#{bytes} B" if index.zero?
38
+ format('%.1f %s', value, BYTE_UNITS[index])
39
+ end
40
+
41
+ # @param bytes_per_second [Float, nil] Rate, `nil` if unknown
42
+ # @return [String] Rate in megabits per second (decimal, like `ascp`)
43
+ def format_rate(bytes_per_second)
44
+ return '-- Mbps' if bytes_per_second.nil?
45
+ mbps = bytes_per_second * Environment::BITS_PER_BYTE / BITS_PER_MEGABIT
46
+ mbps < 10 ? format('%.2f Mbps', mbps) : format('%.0f Mbps', mbps)
47
+ end
48
+
49
+ # @param seconds [Numeric, nil] Duration, `nil` if unknown
50
+ # @return [String] Duration as `hh:mm:ss`
51
+ def format_duration(seconds)
52
+ return '--:--:--' if seconds.nil?
53
+ seconds = seconds.round
54
+ format('%02d:%02d:%02d', seconds / 3600, (seconds / 60) % 60, seconds % 60)
55
+ end
56
+ end
57
+
58
+ # @param output [IO] Where the progress bar is displayed: not on `$stdout`, which may be redirected for command output
59
+ def initialize(output: $stderr)
60
+ @output = output
61
+ # Re-entrant, see `suspend`
62
+ @mutex = Monitor.new
63
+ reset_state
20
64
  end
21
65
 
22
66
  # Reset progress bar, to re-use it.
23
67
  def reset
24
- send(:initialize)
68
+ @mutex.synchronize { reset_state }
25
69
  end
26
70
 
27
- # Called by user of progress bar with a status on a transfer session
28
- # @param session_id [String] the unique identifier of a transfer session
29
- # @param type [Symbol] one of: sessions_init, session_start, session_size, transfer, session_end and end
30
- # @param info [Object, nil] optional specific additional info for the given event type
71
+ # Called by user of progress bar with a status on a transfer session.
72
+ # Event types:
73
+ # - `sessions_init` : `info` is a status message, displayed until a session starts
74
+ # - `session_start` : a session starts, or restarts (same `session_id`)
75
+ # - `session_size` : `info` is the total size of the session
76
+ # - `transfer` : `info` is the number of bytes transferred by the session
77
+ # - `skip` : `info` is the number of bytes of the session already at destination (not transferred)
78
+ # - `session_end` : a session ends
79
+ # - `end` : all sessions are finished, `info` is `false` if the transfer failed
80
+ # @param type [Symbol] Event type
81
+ # @param session_id [String, nil] Unique identifier of a transfer session, same when the session is restarted
82
+ # @param info [Object, nil] Optional specific additional info for the given event type
31
83
  def event(type, session_id: nil, info: nil)
32
84
  Log.log.trace1 { "progress: #{type} #{session_id} #{info}" }
33
- return if @completed
34
- if @progress_bar.nil?
35
- @progress_bar = ProgressBar.create(
36
- format: '%t %a %B %p%% %r Mbps %E',
37
- rate_scale: lambda { |rate| rate / Environment::BYTES_PER_MEBIBIT },
38
- title: '',
39
- total: nil
40
- )
85
+ @mutex.synchronize { process_event(type, session_id, info) }
86
+ rescue StandardError => e
87
+ Log.log.warn { "Progress bar: #{e.class}: #{e.message}" } unless @error_logged
88
+ @error_logged = true
89
+ end
90
+
91
+ # Execute the block with the progress bar cleared, and then display it again.
92
+ # Used to write log lines on the terminal.
93
+ # Does not wait if the progress bar is being updated by another thread (no deadlock with a logger lock).
94
+ def suspend
95
+ return yield unless @mutex.try_enter
96
+ begin
97
+ return yield if @progress_bar.nil? || !@output.tty?
98
+ @progress_bar.clear
99
+ @output.flush
100
+ begin
101
+ yield
102
+ ensure
103
+ @progress_bar.refresh(force: true)
104
+ end
105
+ ensure
106
+ @mutex.exit
41
107
  end
42
- progress_provided = false
108
+ end
109
+
110
+ private
111
+
112
+ def reset_state
113
+ @progress_bar = nil
114
+ # Key is session id
115
+ @sessions = {}
116
+ # Status displayed before session start
117
+ @title = nil
118
+ # Format currently set on the progress bar
119
+ @format = nil
120
+ # [Integer] Bytes actually transferred, by all sessions, including previous runs of restarted sessions
121
+ @transferred = 0
122
+ # [Array<Array(Float, Integer)>] (time, transferred bytes) on the rate window
123
+ @samples = []
124
+ # [Array(Float, Integer), nil] First sample (data starts to flow, not session start), for average rate
125
+ @start = nil
126
+ @error_logged = false
127
+ end
128
+
129
+ # @param type [Symbol] Event type
130
+ # @param session_id [String, nil] Session identifier
131
+ # @param info [Object, nil] Event information
132
+ def process_event(type, session_id, info)
43
133
  case type
44
134
  when :sessions_init
45
135
  # Give opportunity to show progress of initialization with multiple status
46
136
  Aspera.assert(session_id.nil?, 'session_id must be nil for :sessions_init event')
47
137
  Aspera.assert_type(info, String)
48
- # Initialization of progress bar
49
138
  @title = info
50
139
  when :session_start
51
- Aspera.assert_type(session_id, String)
52
140
  Aspera.assert(info.nil?, 'info must be nil for :session_start event')
53
- Aspera.assert(!@sessions[session_id]) { "Session #{session_id} already started" }
54
- @sessions[session_id] = {
55
- job_size: 0, # Total size of transfer (pre-calc)
56
- current: 0,
57
- running: true
58
- }
141
+ start_session(session_id)
59
142
  # Remove last pre-start message if any
60
143
  @title = nil
61
144
  when :session_size
62
- Aspera.assert_type(session_id, String)
63
- Aspera.assert(!info.nil?, 'info must not be nil for :session_size event')
64
- Aspera.assert_type(@sessions[session_id], Hash)
65
- @sessions[session_id][:job_size] = info.to_i
66
- sessions_total = total(:job_size)
67
- @progress_bar.total = sessions_total unless sessions_total.eql?(@progress_bar.total) || sessions_total < @progress_bar.progress
145
+ session(session_id)[:size] = info.to_i unless info.nil?
68
146
  when :transfer
69
- Aspera.assert_type(session_id, String)
70
- Aspera.assert_type(@sessions[session_id], Hash)
71
- if !@progress_bar.total.nil? && !info.nil?
72
- progress_provided = true
73
- @sessions[session_id][:current] = info.to_i
74
- sessions_current = total(:current)
75
- @progress_bar.progress = sessions_current unless @progress_bar.progress.eql?(sessions_current) || sessions_current > total(:job_size)
147
+ unless info.nil?
148
+ current_session = session(session_id)
149
+ transferred = info.to_i
150
+ @transferred += transferred - current_session[:transferred] if transferred > current_session[:transferred]
151
+ current_session[:transferred] = transferred
152
+ add_sample
76
153
  end
154
+ when :skip
155
+ session(session_id)[:skipped] = info.to_i unless info.nil?
77
156
  when :session_end
78
- Aspera.assert_type(session_id, String)
79
157
  Aspera.assert(info.nil?, 'info must be nil for :session_end event')
80
158
  # A session may be too short and finish before it has been started
81
- @sessions[session_id][:running] = false if @sessions[session_id].is_a?(Hash)
159
+ @sessions[session_id][:running] = false if @sessions.key?(session_id)
82
160
  when :end
83
161
  Aspera.assert(session_id.nil?, 'session_id must be nil for :end event')
84
- Aspera.assert(info.nil?, 'info must be nil for :end event')
85
- @progress_bar.finish
162
+ Aspera.assert_values(info, [nil, true, false]) { ':end event info' }
163
+ begin
164
+ finish(success: !info.eql?(false))
165
+ ensure
166
+ reset_state
167
+ end
168
+ return
86
169
  else Aspera.error_unexpected_value(type) { 'event type' }
87
170
  end
88
- new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.count { |_i, d| d[:running] }}] #{@title}"
89
- @progress_bar&.title = new_title unless @progress_bar&.title.eql?(new_title)
90
- @progress_bar&.increment if !progress_provided && @progress_bar.progress.nil?
91
- rescue ProgressBar::InvalidProgressError => e
92
- Log.log.error { "Progress error: #{e}" }
171
+ display
93
172
  end
94
173
 
95
- private
174
+ # @param session_id [String] Session identifier
175
+ # @return [Hash] Session information, created if not already started
176
+ def session(session_id)
177
+ Aspera.assert_type(session_id, String)
178
+ @sessions[session_id] ||= {
179
+ size: nil, # Total size of session, `nil` if unknown
180
+ transferred: 0, # Bytes transferred
181
+ skipped: 0, # Bytes already at destination
182
+ floor: 0, # Progress reached before restart of session
183
+ running: true
184
+ }
185
+ end
186
+
187
+ # Start a session, or restart it: a restarted session notifies again its progress from zero
188
+ # @param session_id [String] Session identifier
189
+ def start_session(session_id)
190
+ restarted = @sessions.key?(session_id)
191
+ current_session = session(session_id)
192
+ if restarted
193
+ current_session[:floor] = done_bytes(current_session)
194
+ current_session[:transferred] = 0
195
+ current_session[:skipped] = 0
196
+ end
197
+ current_session[:running] = true
198
+ end
199
+
200
+ # @param session [Hash] Session information
201
+ # @return [Integer] Bytes processed by the session (transferred or skipped), not more than its size
202
+ def done_bytes(session)
203
+ done = [session[:floor], session[:transferred] + session[:skipped]].max
204
+ session[:size].nil? ? done : [done, session[:size]].min
205
+ end
206
+
207
+ # @return [Integer] Bytes processed by all sessions
208
+ def current_bytes
209
+ @sessions.each_value.sum { |s| done_bytes(s) }
210
+ end
211
+
212
+ # @return [Integer, nil] Total size of all sessions, `nil` if unknown for one of them
213
+ def total_bytes
214
+ return if @sessions.empty? || @sessions.each_value.any? { |s| s[:size].nil? }
215
+ @sessions.each_value.sum { |s| s[:size] }
216
+ end
217
+
218
+ # @return [Float] Monotonic time in seconds
219
+ def now
220
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
221
+ end
222
+
223
+ # Record transferred bytes for rate computation
224
+ # @return [Array(Float, Integer)] The new sample
225
+ def add_sample
226
+ sample = [now, @transferred]
227
+ @start ||= sample
228
+ @samples.push(sample)
229
+ # Keep one sample older than the window
230
+ @samples.shift while @samples.length > 2 && @samples[1].first <= sample.first - RATE_WINDOW_SEC
231
+ sample
232
+ end
233
+
234
+ # @return [Float, nil] Rate on the sliding window, in bytes per second, `nil` if unknown
235
+ def rate
236
+ return if @samples.length < 2
237
+ elapsed = @samples.last.first - @samples.first.first
238
+ return if elapsed < RATE_MIN_SEC
239
+ (@samples.last.last - @samples.first.last) / elapsed
240
+ end
241
+
242
+ # @return [Float, nil] Average rate between first and last sample, in bytes per second, `nil` if unknown
243
+ def average_rate
244
+ return if @start.nil?
245
+ last = @samples.last
246
+ elapsed = last.first - @start.first
247
+ return unless elapsed.positive?
248
+ (last.last - @start.last) / elapsed
249
+ end
250
+
251
+ # Update the progress bar with current state
252
+ def display
253
+ if @progress_bar.nil?
254
+ @progress_bar = ProgressBar.create(output: @output, format: '%a %B', total: nil, autofinish: false, throttle_rate: 0)
255
+ @format = '%a %B'
256
+ end
257
+ current = current_bytes
258
+ total = total_bytes
259
+ update_bar(total, current)
260
+ fields = ['%a', '%B']
261
+ unless @sessions.empty?
262
+ bytes_per_second = rate
263
+ # Fixed width, so that bar does not change size
264
+ rate_text = self.class.format_rate(bytes_per_second).rjust(11)
265
+ if total.nil?
266
+ fields.push(self.class.format_bytes(current).rjust(8), rate_text)
267
+ else
268
+ eta = bytes_per_second&.positive? ? (total - current) / bytes_per_second : nil
269
+ fields.push('%j%%', rate_text, "ETA: #{self.class.format_duration(eta)}")
270
+ end
271
+ end
272
+ update_format(title_text(final: false), fields)
273
+ end
274
+
275
+ # Display final state of the progress bar
276
+ # @param success [Boolean] `false` if the transfer failed: progress is not completed
277
+ def finish(success:)
278
+ return if @progress_bar.nil?
279
+ current = current_bytes
280
+ total = total_bytes
281
+ if success
282
+ total ||= current
283
+ current = total
284
+ end
285
+ fields = ['%a']
286
+ unless total.nil?
287
+ update_bar(total, current)
288
+ fields.push('%B', '%j%%')
289
+ end
290
+ fields.push(self.class.format_bytes(current), self.class.format_rate(average_rate)) unless @sessions.empty?
291
+ # Last status (e.g. `running`) is no more relevant
292
+ @title = success ? nil : 'failed'
293
+ # Final line may be shorter (no bar)
294
+ @progress_bar.clear
295
+ update_format(title_text(final: true), fields)
296
+ success ? @progress_bar.finish : @progress_bar.stop
297
+ end
298
+
299
+ # Set total and progress of the bar, in an order accepted by `ruby-progressbar`
300
+ # @param total [Integer, nil] Total size, `nil` if unknown
301
+ # @param current [Integer] Processed bytes
302
+ def update_bar(total, current)
303
+ if total.nil?
304
+ # Unknown total: progress of the bar is used for animation only
305
+ @progress_bar.total = nil unless @progress_bar.total.nil?
306
+ @progress_bar.increment
307
+ elsif total >= @progress_bar.progress
308
+ @progress_bar.total = total unless @progress_bar.total.eql?(total)
309
+ @progress_bar.progress = current unless @progress_bar.progress.eql?(current)
310
+ else
311
+ @progress_bar.progress = current
312
+ @progress_bar.total = total
313
+ end
314
+ end
315
+
316
+ # @param final [Boolean] `true` for the final display
317
+ # @return [String] Title of the progress bar, with number of running sessions if several sessions
318
+ def title_text(final:)
319
+ parts = []
320
+ running = @sessions.count { |_, s| s[:running] }
321
+ parts.push("[#{running}]") if !final && running.positive? && @sessions.length > 1
322
+ parts.push(@title) unless @title.to_s.empty?
323
+ parts.join(' ')
324
+ end
96
325
 
97
- def total(key)
98
- @sessions.values.inject(0) { |m, s| m + s[key] }
326
+ # Change format of the bar, if changed
327
+ # @param title [String] Literal text
328
+ # @param fields [Array<String>] `ruby-progressbar` format elements
329
+ def update_format(title, fields)
330
+ new_format = (title.empty? ? fields : [title.gsub('%', '%%'), *fields]).join(' ')
331
+ return if new_format.eql?(@format)
332
+ @progress_bar.format = new_format
333
+ @format = new_format
99
334
  end
100
335
  end
101
336
  end
@@ -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.27.3'
7
+ VERSION = '4.27.5'
8
8
  end
9
9
  end
@@ -17,6 +17,7 @@ module Aspera
17
17
  DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
18
18
  private_constant :WIZARD_RESULT_KEYS, :DEFAULT_PRIV_KEY_FILENAME
19
19
 
20
+ # Declared by the `config` plugin (`use_options Wizard`)
20
21
  option :override, description: 'Wizard: override existing value', allowed: Type::BOOLEAN, default: false
21
22
  option :default, description: 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Type::BOOLEAN, default: true
22
23
  option :key_path, description: 'Wizard: path to private key for JWT'
@@ -24,7 +25,6 @@ module Aspera
24
25
  def initialize(parent, main_folder)
25
26
  @parent = parent
26
27
  @main_folder = main_folder
27
- self.class.declare_options(options)
28
28
  end
29
29
 
30
30
  # @return [Boolean] false if in test mode to avoid interactive input
@@ -33,7 +33,6 @@ if ENV.key?('ENABLE_COVERAGE')
33
33
  false
34
34
  end
35
35
  # no coverage in these files
36
- add_filter 'lib/aspera/node_simulator.rb'
37
36
  add_filter 'lib/aspera/keychain/macos_security.rb'
38
37
  add_filter 'lib/aspera/assert.rb'
39
38
  end
@@ -5,13 +5,14 @@ require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
7
  require 'singleton'
8
+ require 'openssl'
8
9
  require 'open3'
9
10
  require 'English'
10
11
  require 'shellwords'
11
12
  require 'aspera/rainbow'
12
13
  using Rainbow
13
14
 
14
- # cspell:words MEBI mswin bccwin
15
+ # cspell:words mswin bccwin
15
16
 
16
17
  module Aspera
17
18
  # detect OS, architecture, and specific stuff
@@ -36,8 +37,6 @@ module Aspera
36
37
  CPU_LIST = [CPU_X86_64, CPU_ARM64, CPU_PPC64, CPU_PPC64LE, CPU_S390].freeze
37
38
 
38
39
  BITS_PER_BYTE = 8
39
- MEBI = 1024 * 1024
40
- BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
41
40
 
42
41
  I18N_VARS = %w(LC_ALL LC_CTYPE LANG).freeze
43
42
 
@@ -50,6 +49,14 @@ module Aspera
50
49
 
51
50
  PROCESS_MODES = %i[execute background capture].freeze
52
51
 
52
+ # System CA certificate bundles, used when the default locations of Ruby's OpenSSL do not exist
53
+ CA_BUNDLE_FILES = [
54
+ '/etc/ssl/certs/ca-certificates.crt', # Debian, Ubuntu, Alpine, Arch
55
+ '/etc/pki/tls/certs/ca-bundle.crt', # RHEL, Fedora
56
+ '/etc/ssl/ca-bundle.pem', # SUSE
57
+ '/etc/ssl/cert.pem' # macOS, BSD
58
+ ].freeze
59
+
53
60
  class << self
54
61
  def ruby_version
55
62
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
@@ -165,8 +172,8 @@ module Aspera
165
172
  if force || !File.exist?(path)
166
173
  # Windows may give error
167
174
  File.unlink(path) rescue nil
168
- # content provided by block
169
- File.write(path, yield)
175
+ # content provided by block, file created with restricted access: never readable by others, even briefly
176
+ File.write(path, yield, perm: mode || 0o600)
170
177
  restrict_file_access(path, mode: mode)
171
178
  end
172
179
  return path
@@ -299,6 +306,23 @@ module Aspera
299
306
  nil
300
307
  end
301
308
 
309
+ # When Ruby's OpenSSL was built on another system (e.g. single executable built on RHEL, used on Ubuntu),
310
+ # its default CA locations may not exist: use the system CA bundle instead.
311
+ # @return [String, nil] CA bundle file used, or nil if no change
312
+ def fix_ca_certificates
313
+ return if defined?(JRUBY_VERSION)
314
+ return if [OpenSSL::X509::DEFAULT_CERT_FILE_ENV, OpenSSL::X509::DEFAULT_CERT_DIR_ENV].any? { |var| ENV.key?(var) }
315
+ return if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) || Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR)
316
+ ca_file = CA_BUNDLE_FILES.find { |file| File.file?(file) }
317
+ return if ca_file.nil?
318
+ # Used by OpenSSL for new default stores, and by child processes
319
+ ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] = ca_file
320
+ # Default store of SSL contexts is initialized when openssl is loaded
321
+ OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE.add_file(ca_file)
322
+ Log.log.debug { "Using system CA certificates: #{ca_file}" }
323
+ ca_file
324
+ end
325
+
302
326
  def graphical?
303
327
  @default_gui_mode == :graphical
304
328
  end
@@ -181,7 +181,7 @@ module Aspera
181
181
  'kdf' => @kdf_params,
182
182
  'data' => @cipher.encrypt(YAML.dump(@all_secrets))
183
183
  }
184
- File.write(@path, YAML.dump(vault_info))
184
+ Environment.write_file_restricted(@path, force: true) { YAML.dump(vault_info) }
185
185
  end
186
186
  end
187
187
  end
@@ -18,7 +18,8 @@ module Aspera
18
18
  Aspera.assert_values(vault_type, LIST.map(&:to_s)) { 'vault.type' }
19
19
  case vault_type
20
20
  when 'file'
21
- info[:file] = info.delete(:name) || 'vault.bin' if info.key?(:name)
21
+ # `name` (or `file`), default: `vault.bin`
22
+ info[:file] = info.delete(:name) || info[:file] || 'vault.bin'
22
23
  info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
23
24
  Aspera.assert(!password.nil?, 'please provide password')
24
25
  info[:password] = password
data/lib/aspera/log.rb CHANGED
@@ -68,6 +68,26 @@ module Aspera
68
68
  # Where logs are sent to
69
69
  LOG_TYPES = %i[stderr stdout syslog].freeze
70
70
 
71
+ # Logger device for a terminal: the status line (e.g. progress bar) is suspended while a log line is written
72
+ class TerminalDevice
73
+ # @param io [IO] Output stream
74
+ def initialize(io)
75
+ @io = io
76
+ end
77
+
78
+ # Called by `Logger`
79
+ # @param message [String] Formatted log line
80
+ def write(message)
81
+ status_line = Log.instance.status_line if @io.tty?
82
+ return @io.write(message) if status_line.nil?
83
+ status_line.suspend { @io.write(message) }
84
+ end
85
+
86
+ # Standard streams are not closed
87
+ def close; end
88
+ end
89
+ private_constant :TerminalDevice
90
+
71
91
  # Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown
72
92
  LEVELS = Logger::Severity.constants.sort { |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b) }.map { |c| c.downcase.to_sym }.freeze
73
93
 
@@ -146,6 +166,8 @@ module Aspera
146
166
 
147
167
  attr_reader :logger_type, :logger
148
168
  attr_accessor :dump_format
169
+ # [#suspend, nil] Object displaying a status line on the terminal (e.g. progress bar), see TerminalDevice
170
+ attr_accessor :status_line
149
171
 
150
172
  # Set the program name used in log output
151
173
  # @param value [String] Program name
@@ -197,9 +219,9 @@ module Aspera
197
219
  current_severity_integer = @logger&.level || ENV['AS_LOG_LEVEL']&.to_i || Logger::Severity::INFO
198
220
  case new_log_type
199
221
  when :stderr
200
- @logger = Logger.new($stderr, progname: @program_name, formatter: DEFAULT_FORMATTER)
222
+ @logger = Logger.new(TerminalDevice.new($stderr), progname: @program_name, formatter: DEFAULT_FORMATTER)
201
223
  when :stdout
202
- @logger = Logger.new($stdout, progname: @program_name, formatter: DEFAULT_FORMATTER)
224
+ @logger = Logger.new(TerminalDevice.new($stdout), progname: @program_name, formatter: DEFAULT_FORMATTER)
203
225
  when :syslog
204
226
  require 'syslog/logger'
205
227
  # The syslog class automatically creates methods from the severity names.
@@ -225,6 +247,7 @@ module Aspera
225
247
 
226
248
  def initialize
227
249
  @logger = nil
250
+ @status_line = nil
228
251
  @program_name = 'aspera'
229
252
  @dump_format = :json
230
253
  @logger_type = :stderr