aspera-cli 4.25.0.pre → 4.25.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +23 -17
- data/CONTRIBUTING.md +119 -47
- data/README.md +325 -239
- data/lib/aspera/agent/direct.rb +14 -12
- data/lib/aspera/agent/factory.rb +9 -6
- data/lib/aspera/agent/transferd.rb +8 -8
- data/lib/aspera/api/aoc.rb +33 -24
- data/lib/aspera/api/ats.rb +1 -0
- data/lib/aspera/api/faspex.rb +11 -5
- data/lib/aspera/ascmd.rb +1 -1
- data/lib/aspera/ascp/installation.rb +7 -7
- data/lib/aspera/ascp/management.rb +9 -5
- data/lib/aspera/assert.rb +3 -3
- data/lib/aspera/cli/extended_value.rb +10 -2
- data/lib/aspera/cli/formatter.rb +15 -62
- data/lib/aspera/cli/manager.rb +9 -43
- data/lib/aspera/cli/plugins/aoc.rb +71 -66
- data/lib/aspera/cli/plugins/ats.rb +30 -36
- data/lib/aspera/cli/plugins/base.rb +11 -6
- data/lib/aspera/cli/plugins/config.rb +21 -16
- data/lib/aspera/cli/plugins/console.rb +2 -1
- data/lib/aspera/cli/plugins/faspex.rb +7 -4
- data/lib/aspera/cli/plugins/faspex5.rb +12 -9
- data/lib/aspera/cli/plugins/faspio.rb +5 -2
- data/lib/aspera/cli/plugins/httpgw.rb +2 -1
- data/lib/aspera/cli/plugins/node.rb +10 -6
- data/lib/aspera/cli/plugins/oauth.rb +12 -11
- data/lib/aspera/cli/plugins/orchestrator.rb +2 -1
- data/lib/aspera/cli/plugins/preview.rb +2 -2
- data/lib/aspera/cli/plugins/server.rb +3 -2
- data/lib/aspera/cli/plugins/shares.rb +59 -20
- data/lib/aspera/cli/transfer_agent.rb +1 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +5 -5
- data/lib/aspera/coverage.rb +5 -1
- data/lib/aspera/dot_container.rb +108 -0
- data/lib/aspera/environment.rb +69 -89
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/id_generator.rb +7 -10
- data/lib/aspera/keychain/macos_security.rb +2 -2
- data/lib/aspera/log.rb +2 -1
- data/lib/aspera/oauth/base.rb +25 -38
- data/lib/aspera/oauth/factory.rb +5 -6
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +4 -3
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/preview/file_types.rb +1 -1
- data/lib/aspera/preview/terminal.rb +95 -29
- data/lib/aspera/preview/utils.rb +6 -5
- data/lib/aspera/rest.rb +5 -2
- data/lib/aspera/ssh.rb +6 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +3 -3
- data/lib/aspera/transfer/parameters.rb +6 -6
- data/lib/aspera/transfer/spec.schema.yaml +4 -4
- data/lib/aspera/transfer/spec_doc.rb +11 -21
- data/lib/aspera/uri_reader.rb +17 -3
- data.tar.gz.sig +0 -0
- metadata +17 -2
- metadata.gz.sig +0 -0
data/lib/aspera/agent/direct.rb
CHANGED
|
@@ -186,20 +186,21 @@ module Aspera
|
|
|
186
186
|
Log.log.debug('fasp local shutdown')
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
# @param id [String] Transfer session identifier
|
|
189
190
|
# @return [Array] list of sessions for a job
|
|
190
|
-
def sessions_by_job(
|
|
191
|
-
@sessions.select{ |session| session[:job_id].eql?(
|
|
191
|
+
def sessions_by_job(id)
|
|
192
|
+
@sessions.select{ |session| session[:job_id].eql?(id)}
|
|
192
193
|
end
|
|
193
194
|
|
|
194
195
|
# Send command to management port of command (used in `asession).
|
|
195
196
|
# Examples:
|
|
196
197
|
# {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
197
198
|
# {'type'=>'DONE'}
|
|
198
|
-
# @param data [Hash] Command on mgt port
|
|
199
|
-
# @param id [String] Optional identifier
|
|
199
|
+
# @param data [Hash] Command on mgt port (snake case)
|
|
200
|
+
# @param id [String] Optional identifier of transfer session
|
|
200
201
|
def send_command(data, id: nil)
|
|
201
202
|
Log.dump(:command, data)
|
|
202
|
-
sessions = id ?
|
|
203
|
+
sessions = id ? sessions_by_job(id) : @sessions
|
|
203
204
|
if sessions.empty?
|
|
204
205
|
Log.log.warn('No transfer session')
|
|
205
206
|
return
|
|
@@ -251,11 +252,14 @@ module Aspera
|
|
|
251
252
|
Aspera.assert_type(session, Hash)
|
|
252
253
|
notify_progress(:sessions_init, info: 'starting')
|
|
253
254
|
begin
|
|
255
|
+
# do not use
|
|
254
256
|
capture_stderr = false
|
|
255
257
|
stderr_r, stderr_w = nil
|
|
256
258
|
spawn_args = {}
|
|
257
259
|
command_pid = nil
|
|
258
|
-
|
|
260
|
+
# get location of command executable (ascp, async)
|
|
261
|
+
command_path = Ascp::Installation.instance.path(name)
|
|
262
|
+
command_arguments = [command_path]
|
|
259
263
|
if @monitor
|
|
260
264
|
# we use Socket directly, instead of TCPServer, as it gives access to lower level options
|
|
261
265
|
socket_class = defined?(JRUBY_VERSION) ? ServerSocket : Socket
|
|
@@ -266,20 +270,18 @@ module Aspera
|
|
|
266
270
|
mgt_server_socket.listen(1)
|
|
267
271
|
# build arguments and add mgt port
|
|
268
272
|
command_arguments = if name.eql?(:async)
|
|
269
|
-
["--exclusive-mgmt-port=#{mgt_server_socket.local_address.ip_port}"]
|
|
273
|
+
[command_path, "--exclusive-mgmt-port=#{mgt_server_socket.local_address.ip_port}"]
|
|
270
274
|
else
|
|
271
|
-
['-M', mgt_server_socket.local_address.ip_port.to_s]
|
|
275
|
+
[command_path, '-M', mgt_server_socket.local_address.ip_port.to_s]
|
|
272
276
|
end
|
|
273
277
|
end
|
|
274
278
|
command_arguments.concat(args)
|
|
275
279
|
if capture_stderr
|
|
276
280
|
# capture process stderr
|
|
277
281
|
stderr_r, stderr_w = IO.pipe
|
|
278
|
-
spawn_args[err] = stderr_w
|
|
282
|
+
spawn_args[:err] = stderr_w
|
|
279
283
|
end
|
|
280
|
-
|
|
281
|
-
command_path = Ascp::Installation.instance.path(name)
|
|
282
|
-
command_pid = Environment.secure_spawn(env: env, exec: command_path, args: command_arguments, **spawn_args)
|
|
284
|
+
command_pid = Environment.secure_execute(*command_arguments, mode: :background, env: env, **spawn_args)
|
|
283
285
|
# close here, but still used in other process (pipe)
|
|
284
286
|
stderr_w&.close
|
|
285
287
|
notify_progress(:sessions_init, info: "waiting for #{name} to start")
|
data/lib/aspera/agent/factory.rb
CHANGED
|
@@ -16,15 +16,18 @@ module Aspera
|
|
|
16
16
|
Aspera::Agent.const_get(agent.to_s.capitalize).new(**options)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
19
|
+
IGNORED_ITEMS = %i[factory base]
|
|
20
|
+
# Available agents: :long : Capitalized name string, :short : single character symbol
|
|
21
|
+
ALL =
|
|
22
22
|
Dir.children(File.dirname(File.expand_path(__FILE__)))
|
|
23
23
|
.select{ |file| file.end_with?(Environment::RB_EXT)}
|
|
24
24
|
.map{ |file| File.basename(file, Environment::RB_EXT).to_sym}
|
|
25
|
-
.reject{ |item| IGNORED_ITEMS.include?(item)}
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
.reject{ |item| IGNORED_ITEMS.include?(item)}.each_with_object({}) do |agent_sym, hash|
|
|
26
|
+
hash[agent_sym] = {
|
|
27
|
+
long: agent_sym.to_s.capitalize,
|
|
28
|
+
short: agent_sym.eql?(:direct) ? :a : agent_sym.to_s[0].to_sym
|
|
29
|
+
}.freeze
|
|
30
|
+
end.freeze
|
|
28
31
|
private_constant :IGNORED_ITEMS
|
|
29
32
|
end
|
|
30
33
|
end
|
|
@@ -21,10 +21,10 @@ module Aspera
|
|
|
21
21
|
|
|
22
22
|
private_constant :LOCAL_SOCKET_ADDR, :PORT_SEP, :AUTO_LOCAL_TCP_PORT
|
|
23
23
|
|
|
24
|
-
# @param url [String]
|
|
25
|
-
# @param start [
|
|
26
|
-
# @param stop [
|
|
27
|
-
# @param base [Hash]
|
|
24
|
+
# @param url [String] URL of the transfer manager daemon
|
|
25
|
+
# @param start [Boolean] If `false`, expect that an external daemon is already running
|
|
26
|
+
# @param stop [Boolean] If `false`, do not shutdown daemon on exit
|
|
27
|
+
# @param base [Hash] Base class options
|
|
28
28
|
def initialize(
|
|
29
29
|
url: AUTO_LOCAL_TCP_PORT,
|
|
30
30
|
start: true,
|
|
@@ -79,9 +79,9 @@ module Aspera
|
|
|
79
79
|
log_stdout = "#{transferd_base_tmp}.out"
|
|
80
80
|
log_stderr = "#{transferd_base_tmp}.err"
|
|
81
81
|
File.write(conf_file, config.to_json)
|
|
82
|
-
@daemon_pid = Environment.
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
@daemon_pid = Environment.secure_execute(
|
|
83
|
+
Ascp::Installation.instance.path(:transferd), '--config', conf_file,
|
|
84
|
+
mode: :background,
|
|
85
85
|
out: log_stdout,
|
|
86
86
|
err: log_stderr
|
|
87
87
|
)
|
|
@@ -170,7 +170,7 @@ module Aspera
|
|
|
170
170
|
def stop_daemon
|
|
171
171
|
if !@daemon_pid.nil?
|
|
172
172
|
Log.log.debug("Stopping daemon #{@daemon_pid}")
|
|
173
|
-
Process.kill(:
|
|
173
|
+
Process.kill(:KILL, @daemon_pid)
|
|
174
174
|
_, status = Process.wait2(@daemon_pid)
|
|
175
175
|
Log.log.debug("daemon stopped #{status}")
|
|
176
176
|
@daemon_pid = nil
|
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Aspera
|
|
|
33
33
|
# types of events for shared folder creation
|
|
34
34
|
# Node events: permission.created permission.modified permission.deleted
|
|
35
35
|
PERMISSIONS_CREATED = ['permission.created'].freeze
|
|
36
|
-
# Special
|
|
36
|
+
# Special user identifier when creating workspace shared folders
|
|
37
37
|
ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
|
|
38
38
|
|
|
39
39
|
private_constant :MAX_AOC_URL_REDIRECT,
|
|
@@ -48,11 +48,13 @@ module Aspera
|
|
|
48
48
|
:ID_AK_ADMIN
|
|
49
49
|
|
|
50
50
|
# various API scopes supported
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
module Scope
|
|
52
|
+
SELF = 'self'
|
|
53
|
+
USER = 'user:all'
|
|
54
|
+
ADMIN = 'admin:all'
|
|
55
|
+
ADMIN_USER = 'admin-user:all'
|
|
56
|
+
ADMIN_USER_USER = "#{ADMIN_USER}+#{USER}"
|
|
57
|
+
end
|
|
56
58
|
FILES_APP = 'files'
|
|
57
59
|
PACKAGES_APP = 'packages'
|
|
58
60
|
API_V1 = 'api/v1'
|
|
@@ -227,10 +229,12 @@ module Aspera
|
|
|
227
229
|
@workspace_info = nil
|
|
228
230
|
@home_info = nil
|
|
229
231
|
auth_params = {
|
|
230
|
-
type:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
type: :oauth2,
|
|
233
|
+
params: {
|
|
234
|
+
client_id: client_id,
|
|
235
|
+
client_secret: client_secret,
|
|
236
|
+
scope: scope
|
|
237
|
+
}
|
|
234
238
|
}
|
|
235
239
|
# analyze type of url
|
|
236
240
|
url_info = AoC.link_info(url)
|
|
@@ -256,20 +260,20 @@ module Aspera
|
|
|
256
260
|
Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
|
|
257
261
|
auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
|
|
258
262
|
auth_params[:payload] = {
|
|
259
|
-
iss:
|
|
263
|
+
iss: client_id, # issuer
|
|
260
264
|
sub: username, # subject
|
|
261
265
|
aud: JWT_AUDIENCE
|
|
262
266
|
}
|
|
263
267
|
# add jwt payload for global client id
|
|
264
|
-
auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(
|
|
268
|
+
auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(client_id)
|
|
265
269
|
auth_params[:cache_ids] = [url_info[:organization]]
|
|
266
270
|
when :url_json
|
|
267
|
-
auth_params[:url] = {grant_type: 'url_token'} #
|
|
271
|
+
auth_params[:url] = {grant_type: 'url_token'} # Query arguments
|
|
268
272
|
auth_params[:json] = {url_token: url_info[:token]} # JSON body
|
|
269
273
|
# password protection of link
|
|
270
274
|
auth_params[:json][:password] = password unless password.nil?
|
|
271
275
|
# basic auth required for /token
|
|
272
|
-
auth_params[:auth] = {type: :basic, username:
|
|
276
|
+
auth_params[:auth] = {type: :basic, username: client_id, password: client_secret}
|
|
273
277
|
else Aspera.error_unexpected_value(auth_params[:grant_method]){'auth, use one of: :web, :jwt'}
|
|
274
278
|
end
|
|
275
279
|
super(
|
|
@@ -386,11 +390,12 @@ module Aspera
|
|
|
386
390
|
@home_info
|
|
387
391
|
end
|
|
388
392
|
|
|
389
|
-
#
|
|
390
|
-
# @param
|
|
391
|
-
# @param
|
|
392
|
-
# @param
|
|
393
|
-
# @param
|
|
393
|
+
# Return a Node API for given node id, in a given context (files, packages), for the given scope.
|
|
394
|
+
# @param node_id [String] identifier of node in AoC
|
|
395
|
+
# @param workspace_id [String,nil] workspace identifier
|
|
396
|
+
# @param workspace_name [String,nil] workspace name
|
|
397
|
+
# @param scope [String,nil] e.g. Node::SCOPE_USER, or Node::SCOPE_ADMIN, or nil (requires secret)
|
|
398
|
+
# @param package_info [Hash,nil] created package information
|
|
394
399
|
# @returns [Node] a node API for access key
|
|
395
400
|
def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::SCOPE_USER, package_info: nil)
|
|
396
401
|
Aspera.assert_type(node_id, String)
|
|
@@ -409,18 +414,22 @@ module Aspera
|
|
|
409
414
|
app_info[:package_name] = package_info['name']
|
|
410
415
|
end
|
|
411
416
|
node_params = {base_url: node_info['url']}
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
ak_secret = @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'])
|
|
418
|
+
# If secret is available, or no scope, use basic auth
|
|
419
|
+
if scope.nil? || ak_secret
|
|
420
|
+
Aspera.assert(ak_secret, "Secret not found for access key #{node_info['access_key']}@#{node_info['url']}", type: Error)
|
|
414
421
|
node_params[:auth] = {
|
|
415
422
|
type: :basic,
|
|
416
423
|
username: node_info['access_key'],
|
|
417
|
-
password:
|
|
424
|
+
password: ak_secret
|
|
418
425
|
}
|
|
419
426
|
else
|
|
420
427
|
# OAuth bearer token
|
|
421
428
|
node_params[:auth] = auth_params.clone
|
|
422
|
-
node_params[:auth][:
|
|
423
|
-
|
|
429
|
+
node_params[:auth][:params] ||= {}
|
|
430
|
+
node_params[:auth][:params][:scope] = Node.token_scope(node_info['access_key'], scope)
|
|
431
|
+
node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::SCOPE_ADMIN)
|
|
432
|
+
# Special header required for bearer token only
|
|
424
433
|
node_params[:headers] = {Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
|
|
425
434
|
end
|
|
426
435
|
node_params[:app_info] = app_info
|
data/lib/aspera/api/ats.rb
CHANGED
data/lib/aspera/api/faspex.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Aspera
|
|
|
34
34
|
query: {
|
|
35
35
|
response_type: :code,
|
|
36
36
|
state: @context,
|
|
37
|
-
client_id: client_id,
|
|
37
|
+
client_id: params[:client_id],
|
|
38
38
|
redirect_uri: @redirect_uri
|
|
39
39
|
},
|
|
40
40
|
exception: false,
|
|
@@ -46,7 +46,7 @@ module Aspera
|
|
|
46
46
|
raise Error, info['action_message'] if info['action_message']
|
|
47
47
|
Aspera.assert(info['code']){'Missing code in answer'}
|
|
48
48
|
# Exchange code for token
|
|
49
|
-
return create_token_call(
|
|
49
|
+
return create_token_call(base_params.merge(
|
|
50
50
|
grant_type: 'authorization_code',
|
|
51
51
|
code: info['code'],
|
|
52
52
|
redirect_uri: @redirect_uri
|
|
@@ -155,7 +155,9 @@ module Aspera
|
|
|
155
155
|
base_url: "#{base_url}/#{PATH_AUTH}",
|
|
156
156
|
grant_method: :faspex_pub_link,
|
|
157
157
|
context: encoded_context,
|
|
158
|
-
|
|
158
|
+
params: {
|
|
159
|
+
client_id: config[:client_id]
|
|
160
|
+
},
|
|
159
161
|
redirect_uri: config[:redirect_uri]
|
|
160
162
|
}
|
|
161
163
|
}
|
|
@@ -177,7 +179,9 @@ module Aspera
|
|
|
177
179
|
type: :oauth2,
|
|
178
180
|
base_url: "#{url}/#{PATH_AUTH}",
|
|
179
181
|
grant_method: :web,
|
|
180
|
-
|
|
182
|
+
params: {
|
|
183
|
+
client_id: client_id
|
|
184
|
+
},
|
|
181
185
|
redirect_uri: redirect_uri
|
|
182
186
|
}
|
|
183
187
|
}
|
|
@@ -190,7 +194,9 @@ module Aspera
|
|
|
190
194
|
type: :oauth2,
|
|
191
195
|
base_url: "#{url}/#{PATH_AUTH}",
|
|
192
196
|
grant_method: :jwt,
|
|
193
|
-
|
|
197
|
+
params: {
|
|
198
|
+
client_id: client_id
|
|
199
|
+
},
|
|
194
200
|
payload: {
|
|
195
201
|
iss: client_id, # issuer
|
|
196
202
|
aud: client_id, # audience (this field is not clear...)
|
data/lib/aspera/ascmd.rb
CHANGED
|
@@ -89,7 +89,7 @@ module Aspera
|
|
|
89
89
|
# Version 2 allows use of reverse proxy with multiple addresses.
|
|
90
90
|
# @param [Symbol] one of OPERATIONS
|
|
91
91
|
# @param [Array] parameters for "as" command
|
|
92
|
-
# @return result of command, type depends on command
|
|
92
|
+
# @return [Boolean,Array,Hash] result of command, type depends on command
|
|
93
93
|
def execute_single(action_sym, arguments, version: 1, host: nil)
|
|
94
94
|
arguments = [] if arguments.nil?
|
|
95
95
|
Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
|
|
@@ -294,12 +294,12 @@ module Aspera
|
|
|
294
294
|
end
|
|
295
295
|
|
|
296
296
|
# Retrieves ascp binary for current system architecture from URL or file
|
|
297
|
-
# @param url [String]
|
|
298
|
-
# @param folder [String]
|
|
299
|
-
# @param backup [
|
|
300
|
-
# @param with_exe [
|
|
301
|
-
# @param &block [Proc]
|
|
302
|
-
# @return ascp version (from execution)
|
|
297
|
+
# @param url [String] URL to SDK archive, or SpecialValues::DEF
|
|
298
|
+
# @param folder [String] Destination folder path
|
|
299
|
+
# @param backup [Boolean] If destination folder exists, then rename
|
|
300
|
+
# @param with_exe [Boolean] If false, only retrieves files, but do not generate or restrict access
|
|
301
|
+
# @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
|
|
302
|
+
# @return [Array] name, ascp version (from execution), folder
|
|
303
303
|
def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
|
|
304
304
|
url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
|
|
305
305
|
folder = Products::Transferd.sdk_directory if folder.nil?
|
|
@@ -348,7 +348,7 @@ module Aspera
|
|
|
348
348
|
sdk_name = 'IBM Aspera Transfer SDK'
|
|
349
349
|
sdk_version = transferd_version || sdk_ascp_version
|
|
350
350
|
File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
|
|
351
|
-
return sdk_name, sdk_version
|
|
351
|
+
return sdk_name, sdk_version, folder
|
|
352
352
|
end
|
|
353
353
|
|
|
354
354
|
attr_accessor :transferd_urls
|
|
@@ -5,8 +5,12 @@ require 'aspera/assert'
|
|
|
5
5
|
module Aspera
|
|
6
6
|
module Ascp
|
|
7
7
|
# processing of ascp management port events
|
|
8
|
+
# Reference: `mgmtmess.c`
|
|
8
9
|
class Management
|
|
9
|
-
#
|
|
10
|
+
# References:
|
|
11
|
+
# https://www.google.com/search?q=FASP+error+codes
|
|
12
|
+
# https://www.ibm.com/support/pages/error-code-reference-tables
|
|
13
|
+
# mgmtmess.c : as_mgmt_err_is_retryable
|
|
10
14
|
# Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
|
|
11
15
|
# rubocop:disable Layout/FirstHashElementLineBreak
|
|
12
16
|
ERRORS = {
|
|
@@ -289,7 +293,7 @@ module Aspera
|
|
|
289
293
|
# cspell: enable
|
|
290
294
|
|
|
291
295
|
class << self
|
|
292
|
-
#
|
|
296
|
+
# Translate native event name to snake case
|
|
293
297
|
def field_native_to_snake(name)
|
|
294
298
|
case name
|
|
295
299
|
when 'Elapsedusec' then 'elapsed_usec'
|
|
@@ -298,7 +302,7 @@ module Aspera
|
|
|
298
302
|
end
|
|
299
303
|
end
|
|
300
304
|
|
|
301
|
-
#
|
|
305
|
+
# Translate snake case event name to native
|
|
302
306
|
# @param name [String] Field name
|
|
303
307
|
def field_snake_to_native(name)
|
|
304
308
|
field = name.delete('_')
|
|
@@ -320,7 +324,7 @@ module Aspera
|
|
|
320
324
|
end
|
|
321
325
|
|
|
322
326
|
# Build command to send on management port
|
|
323
|
-
# @param data [Hash] e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
327
|
+
# @param data [Hash] keys are snake case: e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
324
328
|
# @return [String] frame to send on management port
|
|
325
329
|
def command_to_stream(data)
|
|
326
330
|
data
|
|
@@ -339,7 +343,7 @@ module Aspera
|
|
|
339
343
|
end
|
|
340
344
|
attr_reader :last_event
|
|
341
345
|
|
|
342
|
-
#
|
|
346
|
+
# Process line of mgt port event
|
|
343
347
|
# @param line [String] line of mgt port event
|
|
344
348
|
# @return [Hash] event hash or nil if event is not yet complete
|
|
345
349
|
def process_line(line)
|
data/lib/aspera/assert.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Aspera
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Assert that a condition is true, else raise exception
|
|
34
|
-
# @param assertion [
|
|
34
|
+
# @param assertion [TrueClass, FalseClass] Must be true
|
|
35
35
|
# @param info [String,nil] Fixed message in case assert fails, else use `block`
|
|
36
36
|
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
37
37
|
# @param block [Proc] Produces a string that describes the problem for complex messages
|
|
@@ -78,7 +78,7 @@ module Aspera
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Assert that value is one of the given values
|
|
81
|
-
# @param value [
|
|
81
|
+
# @param value [Object] Value to check
|
|
82
82
|
# @param values [Array] Accepted values
|
|
83
83
|
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
84
84
|
# @param block [Proc] Additional description in front of message
|
|
@@ -91,7 +91,7 @@ module Aspera
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# The value is not one of the expected values
|
|
94
|
-
# @param value [
|
|
94
|
+
# @param value [Object] The wrong value
|
|
95
95
|
# @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
|
|
96
96
|
# @param block [Proc] Additional description in front of message
|
|
97
97
|
def error_unexpected_value(value, type: InternalError)
|
|
@@ -64,6 +64,15 @@ module Aspera
|
|
|
64
64
|
def assert_no_value(value, ext_type)
|
|
65
65
|
Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
def read_stdin(mode)
|
|
69
|
+
case mode
|
|
70
|
+
when '' then $stdin.read
|
|
71
|
+
when 'bin' then $stdin.binmode.read
|
|
72
|
+
when 'chomp' then $stdin.chomp
|
|
73
|
+
else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
private
|
|
@@ -87,8 +96,7 @@ module Aspera
|
|
|
87
96
|
re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
|
|
88
97
|
ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
|
|
89
98
|
secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
|
|
90
|
-
stdin: lambda{ |i| ExtendedValue.
|
|
91
|
-
stdbin: lambda{ |i| ExtendedValue.assert_no_value(i, :stdbin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
|
|
99
|
+
stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
|
|
92
100
|
yaml: lambda{ |i| YAML.load(i)},
|
|
93
101
|
zlib: lambda{ |i| Zlib::Inflate.inflate(i)},
|
|
94
102
|
extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
|
data/lib/aspera/cli/formatter.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'aspera/environment'
|
|
|
8
8
|
require 'aspera/log'
|
|
9
9
|
require 'aspera/assert'
|
|
10
10
|
require 'aspera/markdown'
|
|
11
|
+
require 'aspera/dot_container'
|
|
11
12
|
require 'terminal-table'
|
|
12
13
|
require 'tty-spinner'
|
|
13
14
|
require 'yaml'
|
|
@@ -64,9 +65,10 @@ module Aspera
|
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# Give Markdown String, or matched data, return formatted string for terminal
|
|
67
69
|
# used by spec_doc
|
|
68
70
|
# @param match [MatchData,String]
|
|
69
|
-
def
|
|
71
|
+
def markdown_text(match)
|
|
70
72
|
if match.is_a?(String)
|
|
71
73
|
match = Markdown::FORMATS.match(match)
|
|
72
74
|
Aspera.assert(match)
|
|
@@ -84,11 +86,11 @@ module Aspera
|
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
|
-
#
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
until
|
|
91
|
-
current =
|
|
89
|
+
# Replace special values with a readable version on terminal
|
|
90
|
+
def replace_specific_for_terminal(input_hash)
|
|
91
|
+
hash_to_process = [input_hash]
|
|
92
|
+
until hash_to_process.empty?
|
|
93
|
+
current = hash_to_process.pop
|
|
92
94
|
current.each do |key, value|
|
|
93
95
|
case value
|
|
94
96
|
when NilClass
|
|
@@ -100,73 +102,24 @@ module Aspera
|
|
|
100
102
|
when Array
|
|
101
103
|
if value.empty?
|
|
102
104
|
current[key] = special_format('empty list')
|
|
105
|
+
elsif value.all?(String)
|
|
106
|
+
current[key] = value.join(',')
|
|
103
107
|
else
|
|
104
108
|
value.each do |item|
|
|
105
|
-
|
|
109
|
+
hash_to_process.push(item) if item.is_a?(Hash)
|
|
106
110
|
end
|
|
107
111
|
end
|
|
108
112
|
when Hash
|
|
109
113
|
if value.empty?
|
|
110
114
|
current[key] = special_format('empty dict')
|
|
111
115
|
else
|
|
112
|
-
|
|
116
|
+
hash_to_process.push(value)
|
|
113
117
|
end
|
|
114
118
|
end
|
|
115
119
|
end
|
|
116
120
|
end
|
|
117
121
|
end
|
|
118
122
|
|
|
119
|
-
# Given a list of string, display that list in a single cell
|
|
120
|
-
def list_to_string(list)
|
|
121
|
-
list.join(',')
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Build new prefix
|
|
125
|
-
def add_prefix(prefix, key)
|
|
126
|
-
[prefix, key].compact.join('.')
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Add elements of enumerator to the stack, in reverse order
|
|
130
|
-
def add_elements(stack, prefix, enum)
|
|
131
|
-
enum.reverse_each do |key, value|
|
|
132
|
-
stack.push([add_prefix(prefix, key), value])
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Flatten a Hash into single level hash
|
|
137
|
-
def flatten_hash(input)
|
|
138
|
-
Aspera.assert_type(input, Hash)
|
|
139
|
-
return input if input.empty?
|
|
140
|
-
flat = {}
|
|
141
|
-
# tail (pop,push) contains the next element to display
|
|
142
|
-
stack = [[nil, input]]
|
|
143
|
-
until stack.empty?
|
|
144
|
-
prefix, current = stack.pop
|
|
145
|
-
# empty things will be displayed as such
|
|
146
|
-
if current.respond_to?(:empty?) && current.empty?
|
|
147
|
-
flat[prefix] = current
|
|
148
|
-
next
|
|
149
|
-
end
|
|
150
|
-
case current
|
|
151
|
-
when Hash
|
|
152
|
-
add_elements(stack, prefix, current)
|
|
153
|
-
when Array
|
|
154
|
-
if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
|
|
155
|
-
flat[prefix] = list_to_string(current.map(&:to_s))
|
|
156
|
-
elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
|
|
157
|
-
flat[prefix] = list_to_string(current.map{ |i| i['name']})
|
|
158
|
-
elsif current.all?{ |i| i.is_a?(Hash) && i.keys.sort == %w[name value]}
|
|
159
|
-
add_elements(stack, prefix, current.each_with_object({}){ |i, h| h[i['name']] = i['value']})
|
|
160
|
-
else
|
|
161
|
-
add_elements(stack, prefix, current.each_with_index.map{ |v, i| [i, v]})
|
|
162
|
-
end
|
|
163
|
-
else
|
|
164
|
-
flat[prefix] = current
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
flat
|
|
168
|
-
end
|
|
169
|
-
|
|
170
123
|
def all_but(list)
|
|
171
124
|
list = [list] unless list.is_a?(Array)
|
|
172
125
|
return list.map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
|
|
@@ -356,13 +309,13 @@ module Aspera
|
|
|
356
309
|
if data.empty?
|
|
357
310
|
display_message(:data, self.class.special_format('empty dict'))
|
|
358
311
|
else
|
|
359
|
-
data =
|
|
312
|
+
data = DotContainer.new(data).to_dotted if @options[:flat_hash]
|
|
360
313
|
display_table([data], compute_fields([data], fields), single: true)
|
|
361
314
|
end
|
|
362
315
|
when :object_list
|
|
363
316
|
# :object_list is an Array of Hash, where key=column name
|
|
364
317
|
Aspera.assert_array_all(data, Hash){'result'}
|
|
365
|
-
data = data.map{ |obj|
|
|
318
|
+
data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
|
|
366
319
|
display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
|
|
367
320
|
when :value_list
|
|
368
321
|
# :value_list is a simple array of values, name of column provided in `name`
|
|
@@ -474,7 +427,7 @@ module Aspera
|
|
|
474
427
|
return
|
|
475
428
|
end
|
|
476
429
|
filter_columns_on_select(object_array)
|
|
477
|
-
object_array.each{ |i| self.class.
|
|
430
|
+
object_array.each{ |i| self.class.replace_specific_for_terminal(i)}
|
|
478
431
|
# if table has only one element, and only one field, display the value
|
|
479
432
|
if object_array.length == 1 && fields.length == 1
|
|
480
433
|
Log.log.debug("display_table: single element, field: #{fields.first}")
|