aspera-cli 4.18.1 → 4.20.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 +33 -0
- data/CONTRIBUTING.md +17 -12
- data/README.md +396 -185
- data/bin/asession +26 -19
- data/examples/build_exec +74 -0
- data/examples/{rubyc → build_exec_rubyc} +18 -2
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +4 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +123 -120
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +128 -99
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +104 -64
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +142 -70
- data/lib/aspera/ascp/management.rb +7 -3
- data/lib/aspera/ascp/products.rb +13 -7
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +42 -26
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +15 -10
- data/lib/aspera/cli/plugin.rb +17 -31
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +66 -53
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +155 -96
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +6 -10
- data/lib/aspera/cli/plugins/shares.rb +13 -9
- data/lib/aspera/cli/sync_actions.rb +72 -31
- data/lib/aspera/cli/transfer_agent.rb +13 -14
- data/lib/aspera/cli/transfer_progress.rb +36 -18
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +59 -10
- data/lib/aspera/faspex_gw.rb +3 -3
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +4 -4
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/url_json.rb +3 -2
- data/lib/aspera/oauth/web.rb +10 -6
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/secret_hider.rb +3 -2
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +7 -5
- data/lib/aspera/transfer/parameters.rb +41 -35
- data/lib/aspera/transfer/spec.rb +16 -18
- data/lib/aspera/transfer/sync.rb +51 -50
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -18
- data/lib/aspera/web_server_simple.rb +27 -15
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +25 -6
- metadata.gz.sig +0 -0
@@ -5,16 +5,16 @@ require 'aspera/environment'
|
|
5
5
|
require 'aspera/data_repository'
|
6
6
|
require 'aspera/ascp/products'
|
7
7
|
require 'aspera/log'
|
8
|
+
require 'aspera/rest'
|
8
9
|
require 'aspera/assert'
|
9
10
|
require 'aspera/web_server_simple'
|
10
11
|
require 'English'
|
11
12
|
require 'singleton'
|
12
13
|
require 'xmlsimple'
|
13
|
-
require 'zlib'
|
14
14
|
require 'base64'
|
15
15
|
require 'fileutils'
|
16
16
|
require 'openssl'
|
17
|
-
|
17
|
+
require 'yaml'
|
18
18
|
module Aspera
|
19
19
|
module Ascp
|
20
20
|
# Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
|
@@ -28,7 +28,7 @@ module Aspera
|
|
28
28
|
include Singleton
|
29
29
|
# protobuf generated files from sdk
|
30
30
|
EXT_RUBY_PROTOBUF = '_pb.rb'
|
31
|
-
|
31
|
+
RB_SDK_SUBFOLDER = 'lib'
|
32
32
|
DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
|
33
33
|
<?xml version='1.0' encoding='UTF-8'?>
|
34
34
|
<CONF version="2">
|
@@ -41,8 +41,15 @@ module Aspera
|
|
41
41
|
</CONF>
|
42
42
|
END_OF_CONFIG_FILE
|
43
43
|
# all ascp files (in SDK)
|
44
|
-
|
45
|
-
|
44
|
+
EXE_FILES = %i[ascp ascp4 async].freeze
|
45
|
+
FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
|
46
|
+
TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
|
47
|
+
FILE_SCHEME_PREFIX = 'file:///'
|
48
|
+
SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
|
49
|
+
private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
|
50
|
+
# options for SSH client private key
|
51
|
+
CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
|
52
|
+
|
46
53
|
# set ascp executable path
|
47
54
|
def ascp_path=(v)
|
48
55
|
@path_to_ascp = v
|
@@ -52,12 +59,6 @@ module Aspera
|
|
52
59
|
path(:ascp)
|
53
60
|
end
|
54
61
|
|
55
|
-
def sdk_ruby_folder
|
56
|
-
ruby_pb_folder = File.join(sdk_folder, RB_SDK_FOLDER)
|
57
|
-
FileUtils.mkdir_p(ruby_pb_folder)
|
58
|
-
return ruby_pb_folder
|
59
|
-
end
|
60
|
-
|
61
62
|
# location of SDK files
|
62
63
|
def sdk_folder=(v)
|
63
64
|
Log.log.debug{"sdk_folder=#{v}"}
|
@@ -80,7 +81,7 @@ module Aspera
|
|
80
81
|
def use_ascp_from_product(product_name)
|
81
82
|
if product_name.eql?(FIRST_FOUND)
|
82
83
|
pl = Products.installed_products.first
|
83
|
-
raise "no
|
84
|
+
raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
|
84
85
|
else
|
85
86
|
pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
|
86
87
|
raise "no such product installed: #{product_name}" if pl.nil?
|
@@ -110,14 +111,14 @@ module Aspera
|
|
110
111
|
def path(k)
|
111
112
|
file_is_optional = false
|
112
113
|
case k
|
113
|
-
when
|
114
|
+
when *EXE_FILES
|
115
|
+
file_is_optional = k.eql?(:async)
|
114
116
|
use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
|
115
|
-
file = @path_to_ascp
|
116
117
|
# NOTE: that there might be a .exe at the end
|
117
|
-
file =
|
118
|
+
file = @path_to_ascp.gsub('ascp', k.to_s)
|
118
119
|
when :transferd
|
119
|
-
file = transferd_filepath
|
120
120
|
file_is_optional = true
|
121
|
+
file = transferd_filepath
|
121
122
|
when :ssh_private_dsa, :ssh_private_rsa
|
122
123
|
# assume last 3 letters are type
|
123
124
|
type = k.to_s[-3..-1].to_sym
|
@@ -151,8 +152,16 @@ module Aspera
|
|
151
152
|
return DataRepository.instance.item(:uuid)
|
152
153
|
end
|
153
154
|
|
154
|
-
|
155
|
-
|
155
|
+
# get paths of SSH keys to use for ascp client
|
156
|
+
# @param types [Symbol] types to use
|
157
|
+
def aspera_token_ssh_key_paths(types)
|
158
|
+
Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
|
159
|
+
return case types
|
160
|
+
when :dsa_rsa, :rsa
|
161
|
+
types.to_s.split('_').map{|i|Installation.instance.path("ssh_private_#{i}".to_sym)}
|
162
|
+
when :per_client
|
163
|
+
raise 'Not yet implemented'
|
164
|
+
end
|
156
165
|
end
|
157
166
|
|
158
167
|
# use in plugin `config`
|
@@ -166,16 +175,17 @@ module Aspera
|
|
166
175
|
return nil unless File.exist?(exe_path)
|
167
176
|
exe_version = nil
|
168
177
|
cmd_out = %x("#{exe_path}" #{vers_arg})
|
169
|
-
raise "An error occurred when testing #{
|
178
|
+
raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
|
170
179
|
# get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
|
171
180
|
m = cmd_out.match(/ version ([0-9.]+)/)
|
172
|
-
exe_version = m[1] unless m.nil?
|
181
|
+
exe_version = m[1].gsub(/\.$/, '') unless m.nil?
|
173
182
|
return exe_version
|
174
183
|
end
|
175
184
|
|
176
|
-
def
|
185
|
+
def ascp_pvcl_info
|
186
|
+
data = {}
|
177
187
|
# read PATHs from ascp directly, and pvcl modules as well
|
178
|
-
Open3.popen3(
|
188
|
+
Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
|
179
189
|
last_line = ''
|
180
190
|
while (line = stderr.gets)
|
181
191
|
line.chomp!
|
@@ -194,88 +204,150 @@ module Aspera
|
|
194
204
|
data['product_name'] = Regexp.last_match(1)
|
195
205
|
data['product_version'] = Regexp.last_match(2)
|
196
206
|
when /^LOG Initializing FASP version ([^,]+),/
|
197
|
-
data['
|
207
|
+
data['sdk_ascp_version'] = Regexp.last_match(1)
|
198
208
|
end
|
199
209
|
end
|
200
210
|
if !thread.value.exitstatus.eql?(1) && !data.key?('root')
|
201
211
|
raise last_line
|
202
212
|
end
|
203
213
|
end
|
214
|
+
return data
|
204
215
|
end
|
205
216
|
|
206
217
|
# extract some stings from ascp binary
|
207
|
-
def
|
208
|
-
|
209
|
-
File.binread(
|
218
|
+
def ascp_ssl_info
|
219
|
+
data = {}
|
220
|
+
File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
|
210
221
|
if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
|
211
222
|
data['openssldir'] = m[1]
|
212
223
|
elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
|
213
224
|
data['openssl_version'] = m[1]
|
214
225
|
end
|
215
|
-
end if File.file?(
|
226
|
+
end if File.file?(ascp_path)
|
227
|
+
return data
|
216
228
|
end
|
217
229
|
|
230
|
+
# information for `ascp info`
|
218
231
|
def ascp_info
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
232
|
+
ascp_data = file_paths
|
233
|
+
ascp_data.merge!(ascp_pvcl_info)
|
234
|
+
ascp_data['sdk_locations'] = TRANSFER_SDK_LOCATION_URL
|
235
|
+
ascp_data.merge!(ascp_ssl_info)
|
236
|
+
return ascp_data
|
237
|
+
end
|
238
|
+
|
239
|
+
# Loads YAML from cloud with locations of SDK archives for all platforms
|
240
|
+
# @return location structure
|
241
|
+
def sdk_locations
|
242
|
+
yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
|
243
|
+
YAML.load(yaml_text)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @return the url for download of SDK archive for the given platform and version
|
247
|
+
def sdk_url_for_platform(platform: nil, version: nil)
|
248
|
+
locations = sdk_locations
|
249
|
+
platform = Environment.architecture if platform.nil?
|
250
|
+
locations = locations.select{|l|l['platform'].eql?(platform)}
|
251
|
+
raise "No SDK for platform: #{platform}" if locations.empty?
|
252
|
+
version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
|
253
|
+
info = locations.select{|entry| entry['version'].eql?(version)}
|
254
|
+
raise "No such version: #{version} for #{platform}" if info.empty?
|
255
|
+
return info.first['url']
|
256
|
+
end
|
257
|
+
|
258
|
+
def extract_archive_files(sdk_archive_path)
|
259
|
+
raise 'missing block' unless block_given?
|
260
|
+
case sdk_archive_path
|
261
|
+
# Windows and Mac use zip
|
262
|
+
when /\.zip$/
|
263
|
+
require 'zip'
|
264
|
+
# extract files from archive
|
265
|
+
Zip::File.open(sdk_archive_path) do |zip_file|
|
266
|
+
zip_file.each do |entry|
|
267
|
+
next if entry.name.end_with?('/')
|
268
|
+
yield(entry.name, entry.get_input_stream)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
# Other Unixes use tar.gz
|
272
|
+
when /\.tar\.gz/
|
273
|
+
require 'zlib'
|
274
|
+
require 'rubygems/package'
|
275
|
+
Zlib::GzipReader.open(sdk_archive_path) do |gzip|
|
276
|
+
Gem::Package::TarReader.new(gzip) do |tar|
|
277
|
+
tar.each do |entry|
|
278
|
+
next if entry.directory?
|
279
|
+
yield(entry.full_name, entry)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
else
|
284
|
+
raise "unknown archive extension: #{sdk_archive_path}"
|
285
|
+
end
|
223
286
|
end
|
224
287
|
|
225
288
|
# download aspera SDK or use local file
|
226
289
|
# extracts ascp binary for current system architecture
|
290
|
+
# @param url [String] URL to SDK archive, or SpecialValues::DEF
|
227
291
|
# @return ascp version (from execution)
|
228
|
-
def install_sdk(
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
292
|
+
def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
|
293
|
+
url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
|
294
|
+
folder = sdk_folder if folder.nil?
|
295
|
+
subfolder_lambda = block
|
296
|
+
if subfolder_lambda.nil?
|
297
|
+
subfolder_lambda = ->(name) do
|
298
|
+
if SDK_ARCHIVE_FOLDERS.any?{|i|name.include?(i)}
|
299
|
+
'/'
|
300
|
+
elsif name.end_with?(EXT_RUBY_PROTOBUF)
|
301
|
+
RB_SDK_SUBFOLDER
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
if url.start_with?('file:')
|
234
306
|
# require specific file scheme: the path part is "relative", or absolute if there are 4 slash
|
235
|
-
raise 'use format: file:///<path>' unless
|
236
|
-
|
307
|
+
raise 'use format: file:///<path>' unless url.start_with?(FILE_SCHEME_PREFIX)
|
308
|
+
sdk_archive_path = url[FILE_SCHEME_PREFIX.length..-1]
|
309
|
+
delete_archive = false
|
237
310
|
else
|
238
|
-
|
311
|
+
sdk_archive_path = File.join(Dir.tmpdir, File.basename(url))
|
312
|
+
Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
|
313
|
+
delete_archive = true
|
239
314
|
end
|
240
315
|
# rename old install
|
241
|
-
if !Dir.empty?(
|
316
|
+
if backup && !Dir.empty?(folder)
|
242
317
|
Log.log.warn('Previous install exists, renaming folder.')
|
243
|
-
File.rename(
|
318
|
+
File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
|
244
319
|
# TODO: delete old archives ?
|
245
320
|
end
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
dest_folder = sdk_folder if entry.name.include?(arch_filter)
|
254
|
-
# ruby adapters
|
255
|
-
dest_folder = sdk_ruby_folder if entry.name.end_with?(EXT_RUBY_PROTOBUF)
|
256
|
-
next if dest_folder.nil?
|
257
|
-
File.open(File.join(dest_folder, File.basename(entry.name)), 'wb') do |output_stream|
|
258
|
-
IO.copy_stream(entry.get_input_stream, output_stream)
|
259
|
-
end
|
321
|
+
extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
|
322
|
+
subfolder = subfolder_lambda.call(entry_name)
|
323
|
+
next if subfolder.nil?
|
324
|
+
dest_folder = File.join(folder, subfolder)
|
325
|
+
FileUtils.mkdir_p(dest_folder)
|
326
|
+
File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
|
327
|
+
IO.copy_stream(entry_stream, output_stream)
|
260
328
|
end
|
261
329
|
end
|
262
|
-
File.unlink(
|
330
|
+
File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
|
331
|
+
return unless with_exe
|
263
332
|
# ensure license file are generated so that ascp invocation for version works
|
264
333
|
path(:aspera_license)
|
265
334
|
path(:aspera_conf)
|
266
|
-
|
267
|
-
|
268
|
-
raise "No #{
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
335
|
+
sdk_ascp_file = Products.ascp_filename
|
336
|
+
sdk_ascp_path = File.join(folder, sdk_ascp_file)
|
337
|
+
raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
|
338
|
+
EXE_FILES.each do |exe_sym|
|
339
|
+
exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
|
340
|
+
Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
|
341
|
+
end
|
342
|
+
sdk_ascp_version = get_ascp_version(sdk_ascp_path)
|
343
|
+
sdk_daemon_path = transferd_filepath
|
344
|
+
Log.log.warn{"No #{sdk_daemon_path} in SDK archive"} unless File.exist?(sdk_daemon_path)
|
345
|
+
Environment.restrict_file_access(sdk_daemon_path, mode: 0o755) if File.exist?(sdk_daemon_path)
|
346
|
+
transferd_version = get_exe_version(sdk_daemon_path, 'version')
|
347
|
+
sdk_name = 'IBM Aspera Transfer SDK'
|
348
|
+
sdk_version = transferd_version || sdk_ascp_version
|
349
|
+
File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
|
350
|
+
return sdk_name, sdk_version
|
279
351
|
end
|
280
352
|
|
281
353
|
private
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/assert'
|
4
|
+
|
3
5
|
module Aspera
|
4
6
|
module Ascp
|
5
7
|
# processing of ascp management port events
|
@@ -188,7 +190,7 @@ module Aspera
|
|
188
190
|
# empty line is separator to end event information
|
189
191
|
MGT_FRAME_SEPARATOR = ''
|
190
192
|
# fields description for JSON generation
|
191
|
-
#
|
193
|
+
# cspell: disable
|
192
194
|
INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
|
193
195
|
DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
|
194
196
|
ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
|
@@ -219,6 +221,9 @@ module Aspera
|
|
219
221
|
end
|
220
222
|
attr_reader :last_event
|
221
223
|
|
224
|
+
# process line of mgt port event
|
225
|
+
# @param line [String] line of mgt port event
|
226
|
+
# @return [Hash] event hash or nil if event is not yet complete
|
222
227
|
def process_line(line)
|
223
228
|
# Log.log.debug{"line=[#{line}]"}
|
224
229
|
case line
|
@@ -234,8 +239,7 @@ module Aspera
|
|
234
239
|
@last_event = @event_build
|
235
240
|
@event_build = nil
|
236
241
|
return @last_event
|
237
|
-
else
|
238
|
-
raise "mgt port: unexpected line: [#{line}]"
|
242
|
+
else Aspera.error_unexpected_value(line){'mgt port'}
|
239
243
|
end
|
240
244
|
return nil
|
241
245
|
end
|
data/lib/aspera/ascp/products.rb
CHANGED
@@ -56,6 +56,18 @@ module Aspera
|
|
56
56
|
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
57
57
|
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
58
58
|
sub_bin: File.join('Contents', 'Resources')
|
59
|
+
}, {
|
60
|
+
expected: CONNECT,
|
61
|
+
app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
|
62
|
+
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
63
|
+
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
64
|
+
sub_bin: File.join('Contents', 'Resources')
|
65
|
+
}, {
|
66
|
+
expected: CONNECT,
|
67
|
+
app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
|
68
|
+
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
69
|
+
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
70
|
+
sub_bin: File.join('Contents', 'Resources')
|
59
71
|
}, {
|
60
72
|
expected: CLI_V1,
|
61
73
|
app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
|
@@ -126,7 +138,7 @@ module Aspera
|
|
126
138
|
|
127
139
|
# filename for ascp with optional extension (Windows)
|
128
140
|
def ascp_filename
|
129
|
-
return
|
141
|
+
return "ascp#{Environment.exe_extension}"
|
130
142
|
end
|
131
143
|
|
132
144
|
# @return folder paths for specified applications
|
@@ -150,12 +162,6 @@ module Aspera
|
|
150
162
|
end
|
151
163
|
raise "no connect uri file found in #{folder}"
|
152
164
|
end
|
153
|
-
|
154
|
-
# @ return path to configuration file of aspera CLI
|
155
|
-
# def cli_conf_file
|
156
|
-
# connect = folders(PRODUCT_CLI_V1)
|
157
|
-
# return File.join(connect[:app_root], BIN_SUBFOLDER, '.aspera_cli_conf')
|
158
|
-
# end
|
159
165
|
end
|
160
166
|
end
|
161
167
|
end
|
data/lib/aspera/assert.rb
CHANGED
@@ -8,13 +8,13 @@ module Aspera
|
|
8
8
|
end
|
9
9
|
class << self
|
10
10
|
# the block is executed in the context of the Aspera module
|
11
|
-
def assert(assertion, info = nil,
|
11
|
+
def assert(assertion, info = nil, exception_class: AssertError)
|
12
12
|
raise InternalError, 'bad assert: both info and block given' unless info.nil? || !block_given?
|
13
13
|
return if assertion
|
14
14
|
message = 'assertion failed'
|
15
15
|
info = yield if block_given?
|
16
16
|
message = "#{message}: #{info}" if info
|
17
|
-
message = "#{message}: #{caller(
|
17
|
+
message = "#{message}: #{caller.find{|call|!call.start_with?(__FILE__)}}"
|
18
18
|
raise exception_class, message
|
19
19
|
end
|
20
20
|
|
@@ -22,13 +22,18 @@ module Aspera
|
|
22
22
|
# @param value [Object] the value to check
|
23
23
|
# @param type [Class] the expected type
|
24
24
|
def assert_type(value, type, exception_class: AssertError)
|
25
|
-
assert(value.is_a?(type),
|
25
|
+
assert(value.is_a?(type), exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
|
26
26
|
end
|
27
27
|
|
28
28
|
# assert that value is one of the given values
|
29
|
+
# @param value value to check
|
30
|
+
# @param values accepted values
|
31
|
+
# @param exception_class exception in case of no match
|
29
32
|
def assert_values(value, values, exception_class: AssertError)
|
30
|
-
assert(values.include?(value),
|
31
|
-
|
33
|
+
assert(values.include?(value), exception_class: exception_class) do
|
34
|
+
val_list = values.inspect
|
35
|
+
val_list = "one of #{val_list}" if values.is_a?(Array)
|
36
|
+
"#{block_given? ? "#{yield}: " : nil}expecting #{val_list}, but have #{value.inspect}"
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|
data/lib/aspera/cli/formatter.rb
CHANGED
@@ -101,8 +101,9 @@ module Aspera
|
|
101
101
|
DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
|
102
102
|
# user output levels
|
103
103
|
DISPLAY_LEVELS = %i[info data error].freeze
|
104
|
+
FIELD_VALUE_HEADINGS = %i[key value].freeze
|
104
105
|
|
105
|
-
private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR
|
106
|
+
private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR, :FIELD_VALUE_HEADINGS
|
106
107
|
# prefix to display error messages in user messages (terminal)
|
107
108
|
ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
|
108
109
|
WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
|
@@ -116,7 +117,7 @@ module Aspera
|
|
116
117
|
|
117
118
|
def tick(yes)
|
118
119
|
result =
|
119
|
-
if Environment.terminal_supports_unicode?
|
120
|
+
if Environment.instance.terminal_supports_unicode?
|
120
121
|
if yes
|
121
122
|
"\u2713"
|
122
123
|
else
|
@@ -150,16 +151,9 @@ module Aspera
|
|
150
151
|
end
|
151
152
|
|
152
153
|
# Highlight special values
|
153
|
-
def special_format(what
|
154
|
-
result =
|
155
|
-
|
156
|
-
result = if %w[null empty].any?{|s|what.include?(s)}
|
157
|
-
result.dim
|
158
|
-
else
|
159
|
-
result.reverse_color
|
160
|
-
end
|
161
|
-
end
|
162
|
-
return result
|
154
|
+
def special_format(what)
|
155
|
+
result = "<#{what}>"
|
156
|
+
return %w[null empty].any?{|s|what.include?(s)} ? result.dim : result.reverse_color
|
163
157
|
end
|
164
158
|
|
165
159
|
# call this after REST calls if several api calls are expected
|
@@ -173,6 +167,11 @@ module Aspera
|
|
173
167
|
@spinner.spin
|
174
168
|
end
|
175
169
|
|
170
|
+
def long_operation_terminated
|
171
|
+
@spinner&.stop
|
172
|
+
@spinner = nil
|
173
|
+
end
|
174
|
+
|
176
175
|
# options are: format, output, display, fields, select, table_style, flat_hash, transpose_single
|
177
176
|
def option_handler(option_symbol, operation, value=nil)
|
178
177
|
Aspera.assert_values(operation, %i[set get])
|
@@ -198,12 +197,11 @@ module Aspera
|
|
198
197
|
end
|
199
198
|
|
200
199
|
def declare_options(options)
|
201
|
-
default_table_style = if Environment.terminal_supports_unicode?
|
200
|
+
default_table_style = if Environment.instance.terminal_supports_unicode?
|
202
201
|
{border: :unicode_round}
|
203
202
|
else
|
204
203
|
{}
|
205
204
|
end
|
206
|
-
|
207
205
|
options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
|
208
206
|
options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
|
209
207
|
options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
|
@@ -213,8 +211,9 @@ module Aspera
|
|
213
211
|
default: SpecialValues::DEF)
|
214
212
|
options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
|
215
213
|
options.declare(:table_style, 'Table display style', types: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
|
216
|
-
options.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
|
217
|
-
options.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
|
214
|
+
options.declare(:flat_hash, '(Table) Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
|
215
|
+
options.declare(:transpose_single, '(Table) Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
|
216
|
+
options.declare(:multi_table, '(Table) Each element of a table are displayed as a table', values: :bool, handler: {o: self, m: :option_handler}, default: false)
|
218
217
|
options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
|
219
218
|
options.declare(:image, 'Options for image display', types: Hash, handler: {o: self, m: :option_handler}, default: {})
|
220
219
|
end
|
@@ -326,37 +325,52 @@ module Aspera
|
|
326
325
|
display_message(:info, special_format('empty')) if @options[:format].eql?(:table)
|
327
326
|
return
|
328
327
|
end
|
328
|
+
# if table has only one element, and only one field, display the value
|
329
329
|
if object_array.length == 1 && fields.length == 1
|
330
330
|
display_message(:data, object_array.first[fields.first])
|
331
331
|
return
|
332
332
|
end
|
333
|
+
single_transposed = @options[:transpose_single] && object_array.length == 1
|
333
334
|
# Special case if only one row (it could be object_list or single_object)
|
334
|
-
if
|
335
|
-
new_columns = %i[key value]
|
335
|
+
if single_transposed
|
336
336
|
single = object_array.first
|
337
|
-
object_array = fields.map { |i|
|
338
|
-
fields =
|
337
|
+
object_array = fields.map { |i| FIELD_VALUE_HEADINGS.zip([i, single[i]]).to_h }
|
338
|
+
fields = FIELD_VALUE_HEADINGS
|
339
339
|
end
|
340
340
|
Log.log.debug{Log.dump(:object_array, object_array)}
|
341
341
|
# convert data to string, and keep only display fields
|
342
342
|
final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
|
343
|
+
# remove empty rows
|
344
|
+
final_table_rows.select!{|i| !(i.is_a?(Hash) && i.empty?)}
|
343
345
|
# here : fields : list of column names
|
344
346
|
case @options[:format]
|
345
347
|
when :table
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
348
|
+
if @options[:multi_table] && !single_transposed
|
349
|
+
final_table_rows.each do |row|
|
350
|
+
Log.log.debug{Log.dump(:row, row)}
|
351
|
+
display_message(:data, Terminal::Table.new(
|
352
|
+
headings: FIELD_VALUE_HEADINGS,
|
353
|
+
rows: fields.zip(row),
|
354
|
+
style: @options[:table_style]&.symbolize_keys))
|
355
|
+
end
|
356
|
+
else
|
357
|
+
# display the table !
|
358
|
+
display_message(:data, Terminal::Table.new(
|
359
|
+
headings: fields,
|
360
|
+
rows: final_table_rows,
|
361
|
+
style: @options[:table_style]&.symbolize_keys))
|
362
|
+
end
|
351
363
|
when :csv
|
352
364
|
display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
|
365
|
+
else
|
366
|
+
raise "not expected: #{@options[:format]}"
|
353
367
|
end
|
354
368
|
end
|
355
369
|
|
356
370
|
# @return text suitable to display an image from url
|
357
371
|
def status_image(blob)
|
358
372
|
begin
|
359
|
-
raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::
|
373
|
+
raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::RFC2396_PARSER.make_regexp}\z/)
|
360
374
|
# it's a url
|
361
375
|
url = blob
|
362
376
|
unless Environment.instance.url_method.eql?(:text)
|
@@ -452,6 +466,8 @@ module Aspera
|
|
452
466
|
else
|
453
467
|
raise "unknown data type: #{type}"
|
454
468
|
end
|
469
|
+
else
|
470
|
+
raise "not expected: #{@options[:format]}"
|
455
471
|
end
|
456
472
|
end
|
457
473
|
end
|
data/lib/aspera/cli/hints.rb
CHANGED
@@ -4,6 +4,7 @@ require 'aspera/transfer/error'
|
|
4
4
|
require 'aspera/rest'
|
5
5
|
require 'aspera/log'
|
6
6
|
require 'aspera/assert'
|
7
|
+
require 'aspera/cli/info'
|
7
8
|
require 'net/ssh'
|
8
9
|
require 'openssl'
|
9
10
|
|
@@ -18,7 +19,7 @@ module Aspera
|
|
18
19
|
match: 'Remote host is not who we expected',
|
19
20
|
remediation: [
|
20
21
|
'For this specific error, refer to:',
|
21
|
-
"#{SRC_URL}#error-remote-host-is-not-who-we-expected",
|
22
|
+
"#{Info::SRC_URL}#error-remote-host-is-not-who-we-expected",
|
22
23
|
'Add this to arguments:',
|
23
24
|
%q{--ts=@json:'{"sshfp":null}'"}
|
24
25
|
]
|
data/lib/aspera/cli/info.rb
CHANGED
@@ -2,15 +2,17 @@
|
|
2
2
|
|
3
3
|
module Aspera
|
4
4
|
module Cli
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
5
|
+
module Info
|
6
|
+
# name of command line tool, also used as foldername where config is stored
|
7
|
+
CMD_NAME = 'ascli'
|
8
|
+
# name of the containing gem, same as in <gem name>.gemspec
|
9
|
+
GEM_NAME = 'aspera-cli'
|
10
|
+
DOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
|
11
|
+
GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
|
12
|
+
SRC_URL = 'https://github.com/IBM/aspera-cli'
|
13
|
+
# set this to warn in advance when minimum required ruby version will increase
|
14
|
+
# see also required_ruby_version in gemspec file
|
15
|
+
RUBY_FUTURE_MINIMUM_VERSION = '3.0'
|
16
|
+
end
|
15
17
|
end
|
16
18
|
end
|