aspera-cli 4.27.2 → 4.27.4
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 +133 -0
- data/CONTRIBUTING.md +5 -2
- data/bin/ascli +3 -1
- data/docs/README.md +1006 -766
- data/lib/aspera/agent/base.rb +7 -2
- data/lib/aspera/agent/connect.rb +6 -8
- data/lib/aspera/agent/desktop.rb +2 -6
- data/lib/aspera/agent/direct.rb +52 -22
- data/lib/aspera/agent/node.rb +9 -8
- data/lib/aspera/agent/transferd.rb +2 -2
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +11 -7
- data/lib/aspera/api/httpgw.rb +38 -35
- data/lib/aspera/api/node.rb +39 -34
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +63 -28
- data/lib/aspera/ascp/management.rb +1 -0
- data/lib/aspera/assert.rb +4 -0
- data/lib/aspera/cli/ascp_actions.rb +20 -41
- data/lib/aspera/cli/async_transfer_store.rb +12 -11
- data/lib/aspera/cli/bootstrapper.rb +14 -16
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +215 -37
- data/lib/aspera/cli/command_spec.rb +104 -15
- data/lib/aspera/cli/completion/ascli.bash +12 -0
- data/lib/aspera/cli/completion/ascli.fish +16 -0
- data/lib/aspera/cli/completion/ascli.zsh +19 -0
- data/lib/aspera/cli/context.rb +3 -0
- data/lib/aspera/cli/deprecation.rb +37 -0
- data/lib/aspera/cli/extended_value.rb +6 -3
- data/lib/aspera/cli/formatter.rb +94 -80
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +22 -34
- data/lib/aspera/cli/info.rb +3 -0
- data/lib/aspera/cli/mcp_tool.rb +47 -83
- data/lib/aspera/cli/option_declarator.rb +33 -42
- data/lib/aspera/cli/option_registry.rb +69 -0
- data/lib/aspera/cli/option_types.rb +105 -0
- data/lib/aspera/cli/option_value.rb +278 -0
- data/lib/aspera/cli/options.schema.yaml +124 -15
- data/lib/aspera/cli/parser.rb +333 -862
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +545 -518
- data/lib/aspera/cli/plugins/ats.rb +59 -80
- data/lib/aspera/cli/plugins/base.rb +221 -265
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +263 -184
- data/lib/aspera/cli/plugins/console.rb +103 -39
- data/lib/aspera/cli/plugins/cos.rb +6 -23
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex5.rb +204 -182
- data/lib/aspera/cli/plugins/faspio.rb +6 -11
- data/lib/aspera/cli/plugins/httpgw.rb +8 -11
- data/lib/aspera/cli/plugins/mcp.rb +20 -55
- data/lib/aspera/cli/plugins/node.rb +300 -327
- data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
- data/lib/aspera/cli/plugins/preview.rb +96 -105
- data/lib/aspera/cli/plugins/server.rb +78 -53
- data/lib/aspera/cli/plugins/shares.rb +80 -131
- data/lib/aspera/cli/preset_actions.rb +44 -27
- data/lib/aspera/cli/preset_manager.rb +44 -19
- data/lib/aspera/cli/prompt.rb +36 -0
- data/lib/aspera/cli/result.rb +42 -36
- data/lib/aspera/cli/runner.rb +32 -59
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +51 -46
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +14 -9
- data/lib/aspera/cli/transfer_agent.rb +34 -38
- data/lib/aspera/cli/transfer_progress.rb +290 -55
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -3
- data/lib/aspera/coverage.rb +1 -1
- data/lib/aspera/environment.rb +35 -5
- data/lib/aspera/faspex_gw.rb +2 -1
- data/lib/aspera/faspex_postproc.rb +1 -0
- data/lib/aspera/graphql.rb +5 -5
- data/lib/aspera/json_rpc/client.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +2 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +47 -27
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_emulator.rb +759 -0
- data/lib/aspera/oauth/base.rb +37 -26
- data/lib/aspera/oauth/factory.rb +7 -3
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/json_credentials.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +4 -5
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_folder.rb +1 -3
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +11 -1
- data/lib/aspera/preview/options.schema.yaml +119 -0
- data/lib/aspera/preview/terminal.rb +4 -3
- data/lib/aspera/preview/utils.rb +9 -6
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/rainbow.rb +7 -0
- data/lib/aspera/rest/aspera_errors.rb +72 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +523 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +149 -0
- data/lib/aspera/rest/parameters.rb +55 -0
- data/lib/aspera/rest/util.rb +176 -0
- data/lib/aspera/rest.rb +7 -621
- data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
- 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 on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/reader.rb +12 -18
- data/lib/aspera/schema/registry.rb +23 -1
- data/lib/aspera/schema/validator.rb +92 -0
- data/lib/aspera/secret_hider.rb +36 -25
- data/lib/aspera/string_ext.rb +15 -0
- data/lib/aspera/temp_file_manager.rb +6 -5
- data/lib/aspera/transfer/parameters.rb +2 -0
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/uri_reader.rb +11 -11
- data/lib/aspera/web_auth/index.html +147 -0
- data/lib/aspera/web_auth/server.rb +81 -0
- data.tar.gz.sig +0 -0
- metadata +43 -9
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- data/lib/aspera/node_simulator.rb +0 -344
- data/lib/aspera/preview/options.rb +0 -45
- data/lib/aspera/rest_call_error.rb +0 -25
- data/lib/aspera/rest_error_analyzer.rb +0 -111
- data/lib/aspera/rest_errors_aspera.rb +0 -58
- data/lib/aspera/rest_list.rb +0 -136
- data/lib/aspera/web_auth.rb +0 -211
data/lib/aspera/api/httpgw.rb
CHANGED
|
@@ -9,8 +9,10 @@ require 'securerandom'
|
|
|
9
9
|
require 'websocket'
|
|
10
10
|
require 'base64'
|
|
11
11
|
require 'json'
|
|
12
|
+
require 'aspera/rainbow'
|
|
13
|
+
using Rainbow
|
|
12
14
|
|
|
13
|
-
#
|
|
15
|
+
# Throw exception on error, instead of error code
|
|
14
16
|
WebSocket.should_raise = true
|
|
15
17
|
|
|
16
18
|
module Aspera
|
|
@@ -29,7 +31,7 @@ module Aspera
|
|
|
29
31
|
# 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
|
|
30
32
|
# 2.. Binary File binary chunks "end upload" sent_general
|
|
31
33
|
# last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
|
|
32
|
-
class Httpgw < Aspera::Rest
|
|
34
|
+
class Httpgw < Aspera::Rest::Client
|
|
33
35
|
DEFAULT_BASE_PATH = '/aspera/http-gwy'
|
|
34
36
|
INFO_ENDPOINT = 'info'
|
|
35
37
|
MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
|
|
@@ -40,27 +42,24 @@ module Aspera
|
|
|
40
42
|
API_V1 = 'v1'
|
|
41
43
|
API_V2 = 'v2'
|
|
42
44
|
THR_RECV = 'recv'
|
|
43
|
-
LOG_WS_SEND = 'ws: send: '.red
|
|
44
|
-
LOG_WS_RECV = "ws: #{THR_RECV}: ".green
|
|
45
45
|
private_constant :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
# Send message on http gw web socket
|
|
48
|
+
# @param msg_type [String] type of message to send
|
|
49
|
+
# @param payload [Hash] Data to send
|
|
47
50
|
def ws_snd_json(msg_type, payload)
|
|
48
51
|
if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @upload_version.eql?(API_V2)
|
|
49
52
|
@shared_info[:count][:sent_v2_delimiter] += 1
|
|
50
53
|
else
|
|
51
54
|
@shared_info[:count][:sent_general] += 1
|
|
52
55
|
end
|
|
53
|
-
Log.
|
|
54
|
-
log_data = payload.dup
|
|
55
|
-
log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
|
|
56
|
-
"#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
|
|
57
|
-
end
|
|
56
|
+
Log.dump("#{log_ws_send} #{msg_type}", level: :trace1) { payload.dup.tap { |d| d[:data] = "[data #{d[:data].length} bytes]" if d.key?(:data) } }
|
|
58
57
|
ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
# send data on http gw web socket
|
|
62
61
|
def ws_send(ws_type:, data:)
|
|
63
|
-
Log.log.trace1 { "#{
|
|
62
|
+
Log.log.trace1 { "#{log_ws_send}sending: #{ws_type} (#{data&.length || 0} bytes)" }
|
|
64
63
|
@shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
|
|
65
64
|
frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
|
|
66
65
|
@ws_io.write(frame_generator.to_s)
|
|
@@ -72,12 +71,12 @@ module Aspera
|
|
|
72
71
|
@shared_info[:read_exception].nil? &&
|
|
73
72
|
(((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
|
|
74
73
|
((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
|
|
75
|
-
Log.log.trace1 { "#{
|
|
74
|
+
Log.log.trace1 { "#{log_ws_send}#{'timeout'.blue}: #{@shared_info[:count]}" } if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
|
|
76
75
|
end
|
|
77
76
|
end
|
|
78
77
|
end
|
|
79
78
|
raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
|
|
80
|
-
Log.log.trace2 { "#{
|
|
79
|
+
Log.log.trace2 { "#{log_ws_send}counts: #{@shared_info[:count]}" }
|
|
81
80
|
end
|
|
82
81
|
|
|
83
82
|
# Check header ourself and give precise error message, as websocket will only throw error without details
|
|
@@ -85,13 +84,13 @@ module Aspera
|
|
|
85
84
|
# @return [String] Response Header
|
|
86
85
|
def validated_ws_response_header(header)
|
|
87
86
|
first_line = header.split("\r\n").first
|
|
88
|
-
raise
|
|
87
|
+
raise Rest::CallError.new({messages: ["Unexpected: #{first_line}", 'Expected: 101 Switching Protocols']}) unless first_line.split(/\s+/, 3)[1].eql?('101')
|
|
89
88
|
header
|
|
90
89
|
end
|
|
91
90
|
|
|
92
91
|
# message processing for read thread
|
|
93
92
|
def process_received_message(message)
|
|
94
|
-
Log.log.debug { "#{
|
|
93
|
+
Log.log.debug { "#{log_ws_recv}message: [#{message}] (#{message.class})" }
|
|
95
94
|
if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
|
|
96
95
|
@shared_info[:mutex].synchronize do
|
|
97
96
|
@shared_info[:count][:received_general] += 1
|
|
@@ -117,23 +116,23 @@ module Aspera
|
|
|
117
116
|
end
|
|
118
117
|
end
|
|
119
118
|
|
|
120
|
-
#
|
|
119
|
+
# Main function of read thread
|
|
121
120
|
def process_read_thread
|
|
122
|
-
Log.log.debug { "#{
|
|
121
|
+
Log.log.debug { "#{log_ws_recv}read thread started" }
|
|
123
122
|
frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
|
|
124
123
|
until @ws_io.eof?
|
|
125
124
|
begin
|
|
126
125
|
# ready byte by byte until frame is ready
|
|
127
126
|
# blocking read
|
|
128
127
|
byte = @ws_io.read(1)
|
|
129
|
-
Log.log.trace2 { "#{
|
|
128
|
+
Log.log.trace2 { "#{log_ws_recv}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}" }
|
|
130
129
|
frame_parser << byte
|
|
131
130
|
frame_ok = frame_parser.next
|
|
132
131
|
next if frame_ok.nil?
|
|
133
132
|
process_received_message(frame_ok.data.to_s)
|
|
134
|
-
Log.log.trace2 { "#{
|
|
133
|
+
Log.log.trace2 { "#{log_ws_recv}counts: #{@shared_info[:count]}" }
|
|
135
134
|
rescue => e
|
|
136
|
-
Log.log.debug { "#{
|
|
135
|
+
Log.log.debug { "#{log_ws_recv}Exception: #{e}" }
|
|
137
136
|
@shared_info[:mutex].synchronize do
|
|
138
137
|
@shared_info[:read_exception] = e
|
|
139
138
|
@shared_info[:cond_var].signal
|
|
@@ -142,9 +141,9 @@ module Aspera
|
|
|
142
141
|
end
|
|
143
142
|
end
|
|
144
143
|
Log.log.debug do
|
|
145
|
-
"#{
|
|
144
|
+
"#{log_ws_recv}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
|
|
146
145
|
end unless @shared_info[:read_exception].nil?
|
|
147
|
-
Log.log.debug { "#{
|
|
146
|
+
Log.log.debug { "#{log_ws_recv}read thread stopped (ws eof=#{@ws_io.eof?})" }
|
|
148
147
|
end
|
|
149
148
|
|
|
150
149
|
def upload(transfer_spec)
|
|
@@ -170,7 +169,7 @@ module Aspera
|
|
|
170
169
|
# Get whole HTTP response header, Check and process
|
|
171
170
|
# no need to check `finished?` or result of `<<` (true), as we give the whole header at once
|
|
172
171
|
@ws_handshake << validated_ws_response_header(@ws_io.readuntil("\r\n\r\n"))
|
|
173
|
-
Log.log.debug { "#{
|
|
172
|
+
Log.log.debug { "#{log_ws_send}handshake success" }
|
|
174
173
|
# data shared between main thread and read thread
|
|
175
174
|
@shared_info = {
|
|
176
175
|
read_exception: nil, # error message if any in callback
|
|
@@ -224,7 +223,7 @@ module Aspera
|
|
|
224
223
|
# send once, before data, at beginning
|
|
225
224
|
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
|
|
226
225
|
ws_send(ws_type: :binary, data: slice_bin_data)
|
|
227
|
-
Log.log.trace1 { "#{
|
|
226
|
+
Log.log.trace1 { "#{log_ws_send}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}" }
|
|
228
227
|
# send once, after data, at end
|
|
229
228
|
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(last_slice)
|
|
230
229
|
end
|
|
@@ -255,12 +254,11 @@ module Aspera
|
|
|
255
254
|
# session no more used
|
|
256
255
|
@ws_io = nil
|
|
257
256
|
http_session&.finish
|
|
258
|
-
@notify_cb&.call(:end)
|
|
259
257
|
end
|
|
260
258
|
|
|
261
259
|
def download(transfer_spec)
|
|
262
260
|
transfer_spec['source_root'] ||= '/'
|
|
263
|
-
default_file_name = transfer_spec['paths'].first
|
|
261
|
+
default_file_name = transfer_spec['paths'].first&.fetch('source')
|
|
264
262
|
source_is_folder = %w[. /].include?(default_file_name)
|
|
265
263
|
default_file_name = 'http_download' if source_is_folder
|
|
266
264
|
transfer_spec['zip_required'] ||= source_is_folder || transfer_spec['paths'].length > 1
|
|
@@ -283,17 +281,20 @@ module Aspera
|
|
|
283
281
|
# it is a plain file if we don't require zip and there is only one file
|
|
284
282
|
File.basename(default_file_name)
|
|
285
283
|
end
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
call(
|
|
285
|
+
operation: 'GET',
|
|
286
|
+
subpath: "download/#{transfer_uuid}",
|
|
287
|
+
save_to: File.join(transfer_spec['destination_root'], file_name)
|
|
288
|
+
)
|
|
288
289
|
end
|
|
289
290
|
|
|
290
291
|
def info
|
|
291
|
-
|
|
292
|
+
@api_info
|
|
292
293
|
end
|
|
293
294
|
|
|
294
295
|
# @return [String] the base url of the gateway
|
|
295
296
|
def base_url
|
|
296
|
-
|
|
297
|
+
@gw_root_url
|
|
297
298
|
end
|
|
298
299
|
|
|
299
300
|
# @param url [String] URL of the HTTP Gateway, without version
|
|
@@ -306,10 +307,10 @@ module Aspera
|
|
|
306
307
|
**opts
|
|
307
308
|
)
|
|
308
309
|
Log.dump(:gw_url, url)
|
|
309
|
-
#
|
|
310
|
+
# Add scheme if missing
|
|
310
311
|
url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
|
|
311
312
|
Aspera.assert(url.start_with?('https://'), type: Error) { 'GW URL shall be with scheme https' }
|
|
312
|
-
#
|
|
313
|
+
# Remove trailing slash and version (o=only once) if present
|
|
313
314
|
# TODO: issue warning ?
|
|
314
315
|
url = url.chomp('/').gsub(%r{/#{API_V1}$}o, '')
|
|
315
316
|
# assume GW is always under specific path (TODO: remove this ?)
|
|
@@ -339,6 +340,10 @@ module Aspera
|
|
|
339
340
|
|
|
340
341
|
private
|
|
341
342
|
|
|
343
|
+
# Log prefixes (evaluated on each call, as colors can be enabled or disabled by option)
|
|
344
|
+
def log_ws_send = 'ws: send: '.red
|
|
345
|
+
def log_ws_recv = "ws: #{THR_RECV}: ".green
|
|
346
|
+
|
|
342
347
|
# compute total size of files to upload (for progress)
|
|
343
348
|
# modify transfer spec to be suitable for HTTPGW
|
|
344
349
|
# @param transfer_spec [Hash] transfer specification
|
|
@@ -372,8 +377,7 @@ module Aspera
|
|
|
372
377
|
until folders_to_process.empty?
|
|
373
378
|
folder = folders_to_process.shift
|
|
374
379
|
# read all entries
|
|
375
|
-
Dir.
|
|
376
|
-
next if entry.eql?('.') || entry.eql?('..')
|
|
380
|
+
Dir.each_child(folder) do |entry|
|
|
377
381
|
entry_path = File.join(folder, entry)
|
|
378
382
|
if File.directory?(entry_path)
|
|
379
383
|
folders_to_process.push(entry_path)
|
|
@@ -396,7 +400,6 @@ module Aspera
|
|
|
396
400
|
end
|
|
397
401
|
transfer_spec['paths'] = files_to_send.map { |i| {'source' => i[:name]} }
|
|
398
402
|
files_to_send.push(total_bytes_to_transfer)
|
|
399
|
-
return files_to_send
|
|
400
403
|
end
|
|
401
404
|
end
|
|
402
405
|
end
|
data/lib/aspera/api/node.rb
CHANGED
|
@@ -12,6 +12,8 @@ require 'openssl'
|
|
|
12
12
|
require 'pathname'
|
|
13
13
|
require 'zlib'
|
|
14
14
|
require 'net/ssh/buffer'
|
|
15
|
+
require 'aspera/rainbow'
|
|
16
|
+
using Rainbow
|
|
15
17
|
|
|
16
18
|
module Aspera
|
|
17
19
|
module Api
|
|
@@ -33,7 +35,7 @@ module Aspera
|
|
|
33
35
|
|
|
34
36
|
# Aspera Node API client
|
|
35
37
|
# with gen4 extensions (access keys)
|
|
36
|
-
class Node < Rest
|
|
38
|
+
class Node < Rest::Client
|
|
37
39
|
# Format of node scope : node.<access key>:<scope>
|
|
38
40
|
module Scope
|
|
39
41
|
# Node sub-scopes
|
|
@@ -42,6 +44,26 @@ module Aspera
|
|
|
42
44
|
# Separator between node.AK and user:all
|
|
43
45
|
SEPARATOR = ':'
|
|
44
46
|
NODE_PREFIX = 'node.'
|
|
47
|
+
|
|
48
|
+
extend self
|
|
49
|
+
|
|
50
|
+
# Encode node API scope
|
|
51
|
+
# @param access_key [String] Access Key
|
|
52
|
+
# @param scope [String] internal scope
|
|
53
|
+
# @return [String] full node scope
|
|
54
|
+
def join(access_key:, scope:)
|
|
55
|
+
["#{NODE_PREFIX}#{access_key}", scope].join(SEPARATOR)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Decode node scope into access key and scope
|
|
59
|
+
# @param scope [String] scope string to split
|
|
60
|
+
# @return [Hash] decoded scope with access key and scope parts
|
|
61
|
+
def split(scope)
|
|
62
|
+
items = scope.split(Scope::SEPARATOR, 2)
|
|
63
|
+
Aspera.assert(items.length.eql?(2)) { "invalid scope: #{scope}" }
|
|
64
|
+
Aspera.assert(items[0].start_with?(NODE_PREFIX)) { "invalid scope: #{scope}" }
|
|
65
|
+
{access_key: items[0].delete_prefix(NODE_PREFIX), scope: items[1]}
|
|
66
|
+
end
|
|
45
67
|
end
|
|
46
68
|
# Delimiter in decoded node token
|
|
47
69
|
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
@@ -54,10 +76,10 @@ module Aspera
|
|
|
54
76
|
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
55
77
|
HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
|
|
56
78
|
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
|
79
|
+
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
57
80
|
HEADER_ACCEPT_VERSION = 'Accept-Version'
|
|
58
81
|
# / in cloud
|
|
59
82
|
PATH_SEPARATOR = '/'
|
|
60
|
-
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
61
83
|
|
|
62
84
|
# Register node special token decoder
|
|
63
85
|
OAuth::Factory.instance.register_decoder(lambda { |token| Node.decode_bearer_token(token) })
|
|
@@ -133,22 +155,6 @@ module Aspera
|
|
|
133
155
|
[folder.join(PATH_SEPARATOR), inside]
|
|
134
156
|
end
|
|
135
157
|
|
|
136
|
-
# Node API scopes
|
|
137
|
-
# @return [String] node scope
|
|
138
|
-
def token_scope(access_key, scope)
|
|
139
|
-
return [Scope::NODE_PREFIX, access_key, Scope::SEPARATOR, scope].join('')
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Decode node scope into access key and scope
|
|
143
|
-
# @param scope [String] scope string to decode
|
|
144
|
-
# @return [Hash] decoded scope with access key and scope parts
|
|
145
|
-
def decode_scope(scope)
|
|
146
|
-
items = scope.split(Scope::SEPARATOR, 2)
|
|
147
|
-
Aspera.assert(items.length.eql?(2)) { "invalid scope: #{scope}" }
|
|
148
|
-
Aspera.assert(items[0].start_with?(Scope::NODE_PREFIX)) { "invalid scope: #{scope}" }
|
|
149
|
-
return {access_key: items[0].delete_prefix(Scope::NODE_PREFIX), scope: items[1]}
|
|
150
|
-
end
|
|
151
|
-
|
|
152
158
|
# Create an Aspera Node bearer token
|
|
153
159
|
# @param access_key [String] Access key identifier
|
|
154
160
|
# @param payload [String] JSON payload to be included in the token
|
|
@@ -162,9 +168,8 @@ module Aspera
|
|
|
162
168
|
# Manage convenience parameters
|
|
163
169
|
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
164
170
|
payload.delete('_validity')
|
|
165
|
-
scope = payload
|
|
166
|
-
payload.
|
|
167
|
-
payload['scope'] ||= token_scope(access_key, scope)
|
|
171
|
+
scope = payload.delete('_scope') || Scope::USER
|
|
172
|
+
payload['scope'] ||= Scope.join(access_key: access_key, scope: scope)
|
|
168
173
|
payload['auth_type'] ||= 'access_key'
|
|
169
174
|
payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
|
|
170
175
|
payload_json = JSON.generate(payload)
|
|
@@ -185,7 +190,7 @@ module Aspera
|
|
|
185
190
|
def bearer_headers(bearer_auth, access_key: nil)
|
|
186
191
|
# If username is not provided, use the access key from the token
|
|
187
192
|
if access_key.nil?
|
|
188
|
-
access_key = Node.
|
|
193
|
+
access_key = Node::Scope.split(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
|
|
189
194
|
Aspera.assert(!access_key.nil?, 'access_key could not be determined from token')
|
|
190
195
|
end
|
|
191
196
|
return {
|
|
@@ -233,7 +238,7 @@ module Aspera
|
|
|
233
238
|
end
|
|
234
239
|
Log.log.warn { "Cannot resolve link with node id #{node_id}, no resolver" }
|
|
235
240
|
return
|
|
236
|
-
rescue
|
|
241
|
+
rescue Rest::CallError => e
|
|
237
242
|
Log.log.warn { "Cannot resolve link with node id #{node_id}: #{e.message}" }
|
|
238
243
|
return
|
|
239
244
|
end
|
|
@@ -287,7 +292,7 @@ module Aspera
|
|
|
287
292
|
query['page'] = 0
|
|
288
293
|
end
|
|
289
294
|
loop do
|
|
290
|
-
|
|
295
|
+
Rest::Parameters.instance.spinner_cb.call(folder_items.count)
|
|
291
296
|
data, http = read("files/#{file_id}/files", query, headers: headers, ret: :both)
|
|
292
297
|
folder_items.concat(data)
|
|
293
298
|
if use_v4
|
|
@@ -304,7 +309,7 @@ module Aspera
|
|
|
304
309
|
Log.log.warn { "#{path || file_id}: #{e.class} #{e.message}" }
|
|
305
310
|
Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
|
|
306
311
|
ensure
|
|
307
|
-
|
|
312
|
+
Rest::Parameters.instance.spinner_cb.call(folder_items.count, action: :success)
|
|
308
313
|
end
|
|
309
314
|
folder_items
|
|
310
315
|
end
|
|
@@ -326,7 +331,7 @@ module Aspera
|
|
|
326
331
|
until folders_to_explore.empty?
|
|
327
332
|
# Consume first in job list
|
|
328
333
|
current_item = folders_to_explore.shift
|
|
329
|
-
Log.log.debug { "Exploring #{current_item[:path]}".
|
|
334
|
+
Log.log.debug { "Exploring #{current_item[:path]}".bg(:green) }
|
|
330
335
|
# Get folder content
|
|
331
336
|
folder_contents = read_folder_content(current_item[:id], query, exception: false, path: current_item[:path])
|
|
332
337
|
Log.dump(:folder_contents, folder_contents)
|
|
@@ -452,7 +457,7 @@ module Aspera
|
|
|
452
457
|
create(
|
|
453
458
|
'files/download_setup',
|
|
454
459
|
{transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
|
|
455
|
-
)['transfer_specs'].first
|
|
460
|
+
)['transfer_specs'].first&.fetch('transfer_spec')
|
|
456
461
|
end
|
|
457
462
|
|
|
458
463
|
# Get generic part of transfer spec with transport parameters only
|
|
@@ -527,7 +532,7 @@ module Aspera
|
|
|
527
532
|
|
|
528
533
|
# Executes `GET` call in loop using `iteration_token` (`/ops/transfers`)
|
|
529
534
|
# @param iteration [Array] a single element array with the iteration token or nil
|
|
530
|
-
# @param call_args [Hash] additional arguments to pass to `Rest
|
|
535
|
+
# @param call_args [Hash] additional arguments to pass to `Rest::Client#call`
|
|
531
536
|
# @return [Array] list of items returned by the API call
|
|
532
537
|
def read_with_paging(subpath, query = nil, iteration: nil, **call_args)
|
|
533
538
|
Aspera.assert_type(iteration, Array, NilClass) { 'iteration' }
|
|
@@ -535,7 +540,7 @@ module Aspera
|
|
|
535
540
|
Aspera.assert(!call_args.key?(:query), ':query must not be in call_args (use query parameter)')
|
|
536
541
|
query = {} if query.nil?
|
|
537
542
|
query[:iteration_token] = iteration[0] unless iteration.nil? || iteration[0].nil?
|
|
538
|
-
max = query.delete(
|
|
543
|
+
max = query.delete(Rest::List::MAX_ITEMS)
|
|
539
544
|
# Return empty list immediately if max is 0
|
|
540
545
|
return [] if max&.zero?
|
|
541
546
|
item_list = []
|
|
@@ -551,7 +556,7 @@ module Aspera
|
|
|
551
556
|
break
|
|
552
557
|
end
|
|
553
558
|
# Update progress spinner
|
|
554
|
-
|
|
559
|
+
Rest::Parameters.instance.spinner_cb.call(item_list.length)
|
|
555
560
|
# Parse Link header according to RFC 8288 to extract next iteration token
|
|
556
561
|
next_url = LinkHeader.parse(http['Link']).find_href(rel: 'next')
|
|
557
562
|
next_iteration_token = nil
|
|
@@ -572,7 +577,7 @@ module Aspera
|
|
|
572
577
|
query[:iteration_token] = next_iteration_token
|
|
573
578
|
end
|
|
574
579
|
# Signal completion
|
|
575
|
-
|
|
580
|
+
Rest::Parameters.instance.spinner_cb.call(action: :success)
|
|
576
581
|
# save iteration token if needed
|
|
577
582
|
iteration[0] = query[:iteration_token] unless iteration.nil?
|
|
578
583
|
item_list
|
|
@@ -582,7 +587,7 @@ module Aspera
|
|
|
582
587
|
#
|
|
583
588
|
# @param subpath [String] API Path
|
|
584
589
|
# @param query [Hash, nil] Optional query parameters for the API request
|
|
585
|
-
# @param kwargs [Hash] Other parameters for Rest
|
|
590
|
+
# @param kwargs [Hash] Other parameters for Rest::Client#read
|
|
586
591
|
#
|
|
587
592
|
# @return [Array<Hash>] List of folder entries (files, folders, links)
|
|
588
593
|
def read_with_pages(subpath, query = nil, **kwargs)
|
|
@@ -592,7 +597,7 @@ module Aspera
|
|
|
592
597
|
query['page'] ||= 1
|
|
593
598
|
suffix = nil
|
|
594
599
|
loop do
|
|
595
|
-
|
|
600
|
+
Rest::Parameters.instance.spinner_cb.call("#{items.count}#{suffix}")
|
|
596
601
|
data, http = read(subpath, query, **kwargs, ret: :both)
|
|
597
602
|
items.concat(data)
|
|
598
603
|
break if data.length < query['per_page']
|
|
@@ -603,7 +608,7 @@ module Aspera
|
|
|
603
608
|
Log.log.warn { "#{e.class} #{e.message}" }
|
|
604
609
|
Log.log.debug { (['Backtrace:'] + e.backtrace).join("\n") }
|
|
605
610
|
ensure
|
|
606
|
-
|
|
611
|
+
Rest::Parameters.instance.spinner_cb.call(items.count, action: :success)
|
|
607
612
|
return items # rubocop:disable Lint/EnsureReturn
|
|
608
613
|
end
|
|
609
614
|
|
data/lib/aspera/ascmd.rb
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
# cspell:ignore ascmd smode errstr zstr zmode zuid zgid zctime zatime zmtime fcount dcount btype blist codeset lc_ctype ascmdtypes
|
|
4
4
|
require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
|
6
|
+
require 'aspera/rainbow'
|
|
7
|
+
using Rainbow
|
|
6
8
|
|
|
7
9
|
module Aspera
|
|
8
|
-
# Run
|
|
10
|
+
# Run `ascmd` commands using specified executor (usually, remotely on transfer node)
|
|
9
11
|
# Equivalent of SDK "command client"
|
|
10
12
|
# execute: "ascmd -h" to get syntax
|
|
11
13
|
# Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
|
|
@@ -42,6 +42,14 @@ module Aspera
|
|
|
42
42
|
# policy for product selection
|
|
43
43
|
FIRST_FOUND = 'FIRST'
|
|
44
44
|
|
|
45
|
+
class << self
|
|
46
|
+
# @param value [String, nil] value of option `sdk_folder`
|
|
47
|
+
# @return [Boolean] true if value selects a product instead of a folder
|
|
48
|
+
def product_selector?(value)
|
|
49
|
+
value.is_a?(String) && value.start_with?(USE_PRODUCT_PREFIX)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
# Loads YAML from cloud with locations of SDK archives for all platforms
|
|
46
54
|
# @return [Hash] location structure
|
|
47
55
|
def sdk_locations
|
|
@@ -55,30 +63,25 @@ module Aspera
|
|
|
55
63
|
end
|
|
56
64
|
end
|
|
57
65
|
|
|
58
|
-
# Set `ascp` executable "location"
|
|
66
|
+
# Set `ascp` executable "location" (option `sdk_folder`)
|
|
59
67
|
# It can be:
|
|
60
|
-
# - Full path to folder where `ascp` executable is located
|
|
61
|
-
# - "product:PRODUCT_NAME" to use ascp from named product
|
|
68
|
+
# - Full path to folder where `ascp` executable is located, also used as SDK folder (keys, conf)
|
|
69
|
+
# - "product:PRODUCT_NAME" to use ascp from named product, SDK folder stays the default one
|
|
62
70
|
# - "product:FIRST" to use ascp from first found product
|
|
71
|
+
# @param ascp_location [String] folder or product selector
|
|
72
|
+
# @return [nil]
|
|
63
73
|
def sdk_folder=(ascp_location)
|
|
64
74
|
Aspera.assert_type(ascp_location, String) { 'ascp_location' }
|
|
65
75
|
Aspera.assert(!ascp_location.empty?, 'ascp location cannot be empty: check your config file')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
File.dirname(pl[:ascp_path])
|
|
77
|
-
else
|
|
78
|
-
ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
|
|
79
|
-
end
|
|
80
|
-
Log.log.debug { "ascp_folder=#{folder}" }
|
|
81
|
-
Products::Transferd.sdk_directory = folder
|
|
76
|
+
@ascp_folder = nil
|
|
77
|
+
if self.class.product_selector?(ascp_location)
|
|
78
|
+
# Resolved on first use: product scan requires the SDK folder, which is set later
|
|
79
|
+
@ascp_product = ascp_location.delete_prefix(USE_PRODUCT_PREFIX)
|
|
80
|
+
Log.log.debug { "ascp_product=#{@ascp_product}" }
|
|
81
|
+
else
|
|
82
|
+
@ascp_product = nil
|
|
83
|
+
Products::Transferd.sdk_directory = ascp_location.include?('/ascp') ? File.dirname(ascp_location) : ascp_location
|
|
84
|
+
end
|
|
82
85
|
nil
|
|
83
86
|
end
|
|
84
87
|
|
|
@@ -114,8 +117,9 @@ module Aspera
|
|
|
114
117
|
case file_type
|
|
115
118
|
when *EXE_FILES
|
|
116
119
|
file_is_required = file_type.eql?(:ascp)
|
|
117
|
-
file =
|
|
118
|
-
|
|
120
|
+
file = sdk_exe_path(file_type)
|
|
121
|
+
# transferd always comes from SDK
|
|
122
|
+
file = File.join(product_ascp_folder, File.basename(file)) unless @ascp_product.nil? || file_type.eql?(:transferd)
|
|
119
123
|
when :ssh_private_dsa, :ssh_private_rsa
|
|
120
124
|
# assume last 3 letters are type
|
|
121
125
|
type = file_type.to_s[-3..].to_sym
|
|
@@ -282,7 +286,7 @@ module Aspera
|
|
|
282
286
|
end
|
|
283
287
|
end
|
|
284
288
|
|
|
285
|
-
# Downloads and extracts the SDK archive for the current platform into
|
|
289
|
+
# Downloads and extracts the SDK archive for the current platform into `folder`
|
|
286
290
|
# @param folder [String] Destination folder path
|
|
287
291
|
# @param url [nil, String] URL to SDK archive, if nil: default url for version
|
|
288
292
|
# @param version [nil, String] Specific version, if nil: latest version
|
|
@@ -306,9 +310,9 @@ module Aspera
|
|
|
306
310
|
if UriReader.file?(url)
|
|
307
311
|
archive_io.write(File.binread(UriReader.file_path(url)))
|
|
308
312
|
else
|
|
309
|
-
Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
|
|
310
|
-
archive_io.rewind
|
|
313
|
+
Rest::Client.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to: archive_io)
|
|
311
314
|
end
|
|
315
|
+
archive_io.rewind
|
|
312
316
|
extract_archive_files(url, archive_io) do |entry_name, entry_stream, link_target|
|
|
313
317
|
dest_folder = if block_given?
|
|
314
318
|
yield(entry_name)
|
|
@@ -385,10 +389,13 @@ module Aspera
|
|
|
385
389
|
folder = Products::Transferd.sdk_directory
|
|
386
390
|
download_sdk(folder: folder, url: url, version: version)
|
|
387
391
|
# Ensure necessary files are there, or generate them, restrict file access on SDK executables
|
|
392
|
+
# (executables of SDK, even if `ascp` of another product is selected)
|
|
388
393
|
SDK_FILES.each do |file_id_sym|
|
|
389
|
-
|
|
390
|
-
|
|
394
|
+
if EXE_FILES.include?(file_id_sym)
|
|
395
|
+
file_path = sdk_exe_path(file_id_sym)
|
|
391
396
|
Environment.restrict_file_access(file_path, mode: 0o755) if File.exist?(file_path)
|
|
397
|
+
else
|
|
398
|
+
path(file_id_sym)
|
|
392
399
|
end
|
|
393
400
|
end
|
|
394
401
|
# Generate meta data XML file in SDK if needed, based on actual binaries versions
|
|
@@ -399,8 +406,8 @@ module Aspera
|
|
|
399
406
|
sdk_name = meta_data.scan(%r{<name>(.*?)</name>}).flatten.first || 'unknown'
|
|
400
407
|
sdk_version = meta_data.scan(%r{<version>(.*?)</version>}).flatten.first || 'unknown'
|
|
401
408
|
else
|
|
402
|
-
sdk_ascp_version = get_ascp_version(
|
|
403
|
-
transferd_version = get_exe_version(
|
|
409
|
+
sdk_ascp_version = get_ascp_version(sdk_exe_path(:ascp))
|
|
410
|
+
transferd_version = get_exe_version(sdk_exe_path(:transferd), 'version')
|
|
404
411
|
sdk_name = 'IBM Aspera Transfer SDK'
|
|
405
412
|
sdk_version = transferd_version || sdk_ascp_version
|
|
406
413
|
File.write(meta_data_file, "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
|
|
@@ -435,6 +442,34 @@ module Aspera
|
|
|
435
442
|
# cache for installed products found
|
|
436
443
|
@found_products = nil
|
|
437
444
|
@transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
|
|
445
|
+
# product selected with option `sdk_folder`, or nil to use SDK folder
|
|
446
|
+
@ascp_product = nil
|
|
447
|
+
# cache for folder of `ascp` of selected product
|
|
448
|
+
@ascp_folder = nil
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# @param file_type [Symbol] one of EXE_FILES
|
|
452
|
+
# @return [String] path to executable in SDK folder
|
|
453
|
+
def sdk_exe_path(file_type)
|
|
454
|
+
file = Products::Transferd.transferd_path
|
|
455
|
+
file = File.join(File.dirname(file), Environment.instance.exe_file(file_type.to_s)) unless file_type.eql?(:transferd)
|
|
456
|
+
return file
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# @return [String] folder of `ascp` of product selected with option `sdk_folder`
|
|
460
|
+
def product_ascp_folder
|
|
461
|
+
if @ascp_folder.nil?
|
|
462
|
+
if @ascp_product.eql?(FIRST_FOUND)
|
|
463
|
+
pl = installed_products.first
|
|
464
|
+
Aspera.assert(!pl.nil?) { "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli config ascp install" }
|
|
465
|
+
else
|
|
466
|
+
pl = installed_products.find { |i| i[:name].eql?(@ascp_product) }
|
|
467
|
+
Aspera.assert(!pl.nil?) { "No such product installed: #{@ascp_product}" }
|
|
468
|
+
end
|
|
469
|
+
@ascp_folder = File.dirname(pl[:ascp_path])
|
|
470
|
+
Log.log.debug { "ascp_folder=#{@ascp_folder}" }
|
|
471
|
+
end
|
|
472
|
+
return @ascp_folder
|
|
438
473
|
end
|
|
439
474
|
|
|
440
475
|
public
|
data/lib/aspera/assert.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Aspera
|
|
|
17
17
|
class AssertError < Error
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Raised when a looked up entity is not found
|
|
21
|
+
class EntityNotFound < Error
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
class << self
|
|
21
25
|
# Replaces `raise` in assertion
|
|
22
26
|
# Allows sending exception, or just error log, when type is `:error`
|