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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +118 -0
- data/CONTRIBUTING.md +5 -2
- data/bin/ascli +1 -0
- data/docs/README.md +224 -42
- data/lib/aspera/agent/base.rb +7 -2
- data/lib/aspera/agent/connect.rb +0 -4
- data/lib/aspera/agent/desktop.rb +0 -4
- data/lib/aspera/agent/direct.rb +49 -21
- data/lib/aspera/agent/node.rb +6 -5
- data/lib/aspera/agent/transferd.rb +2 -2
- data/lib/aspera/api/faspex.rb +2 -0
- data/lib/aspera/api/httpgw.rb +1 -2
- data/lib/aspera/api/node.rb +3 -3
- data/lib/aspera/ascp/installation.rb +1 -1
- data/lib/aspera/cli/async_transfer_store.rb +10 -9
- data/lib/aspera/cli/bootstrapper.rb +3 -1
- data/lib/aspera/cli/command_registry.rb +69 -7
- data/lib/aspera/cli/command_spec.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +4 -3
- data/lib/aspera/cli/formatter.rb +10 -8
- data/lib/aspera/cli/http.rb +8 -20
- data/lib/aspera/cli/option_types.rb +3 -1
- data/lib/aspera/cli/option_value.rb +16 -19
- data/lib/aspera/cli/options.schema.yaml +86 -10
- data/lib/aspera/cli/parser.rb +30 -18
- data/lib/aspera/cli/plugins/aoc.rb +133 -156
- data/lib/aspera/cli/plugins/ats.rb +1 -7
- data/lib/aspera/cli/plugins/base.rb +40 -34
- data/lib/aspera/cli/plugins/config.rb +24 -11
- data/lib/aspera/cli/plugins/console.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +30 -11
- data/lib/aspera/cli/plugins/faspio.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +26 -19
- data/lib/aspera/cli/plugins/orchestrator.rb +73 -44
- data/lib/aspera/cli/plugins/preview.rb +21 -19
- data/lib/aspera/cli/plugins/server.rb +3 -4
- data/lib/aspera/cli/plugins/shares.rb +21 -24
- data/lib/aspera/cli/preset_actions.rb +28 -18
- data/lib/aspera/cli/preset_manager.rb +34 -19
- data/lib/aspera/cli/prompt.rb +2 -1
- data/lib/aspera/cli/result.rb +33 -22
- data/lib/aspera/cli/runner.rb +1 -5
- data/lib/aspera/cli/sync_actions.rb +17 -16
- data/lib/aspera/cli/transfer_actions.rb +14 -3
- data/lib/aspera/cli/transfer_agent.rb +6 -4
- data/lib/aspera/cli/transfer_progress.rb +290 -55
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +1 -1
- data/lib/aspera/coverage.rb +0 -1
- data/lib/aspera/environment.rb +29 -5
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +2 -1
- data/lib/aspera/log.rb +25 -2
- data/lib/aspera/node_emulator.rb +759 -0
- data/lib/aspera/oauth/base.rb +2 -1
- data/lib/aspera/oauth/factory.rb +6 -3
- data/lib/aspera/oauth/json_credentials.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +3 -4
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_folder.rb +1 -3
- data/lib/aspera/preview/generator.rb +4 -1
- data/lib/aspera/preview/options.schema.yaml +119 -0
- data/lib/aspera/rest/aspera_errors.rb +12 -0
- data/lib/aspera/rest/client.rb +28 -19
- data/lib/aspera/rest/list.rb +14 -8
- data/lib/aspera/rest/parameters.rb +2 -2
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
- data/lib/aspera/schema/reader.rb +12 -18
- data/lib/aspera/schema/registry.rb +6 -1
- data.tar.gz.sig +0 -0
- metadata +5 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/node_simulator.rb +0 -345
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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.
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/cli/wizard.rb
CHANGED
|
@@ -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
|
data/lib/aspera/coverage.rb
CHANGED
data/lib/aspera/environment.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|