aspera-cli 4.25.3 → 4.26.0.pre
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 +39 -6
- data/CONTRIBUTING.md +119 -111
- data/README.md +9 -7
- data/lib/aspera/agent/direct.rb +10 -8
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/alee.rb +1 -0
- data/lib/aspera/api/aoc.rb +13 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +5 -0
- data/lib/aspera/api/faspex.rb +15 -2
- data/lib/aspera/api/httpgw.rb +2 -0
- data/lib/aspera/api/node.rb +82 -29
- data/lib/aspera/ascp/installation.rb +9 -10
- data/lib/aspera/cli/error.rb +8 -0
- data/lib/aspera/cli/formatter.rb +27 -11
- data/lib/aspera/cli/info.rb +2 -1
- data/lib/aspera/cli/main.rb +30 -12
- data/lib/aspera/cli/manager.rb +43 -31
- data/lib/aspera/cli/plugins/aoc.rb +7 -5
- data/lib/aspera/cli/plugins/base.rb +2 -79
- data/lib/aspera/cli/plugins/config.rb +2 -1
- data/lib/aspera/cli/plugins/faspex.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +51 -51
- data/lib/aspera/cli/plugins/node.rb +9 -14
- data/lib/aspera/cli/plugins/shares.rb +4 -2
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/transfer_agent.rb +3 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +2 -1
- data/lib/aspera/dot_container.rb +10 -10
- data/lib/aspera/log.rb +1 -1
- data/lib/aspera/markdown.rb +1 -1
- data/lib/aspera/persistency_folder.rb +1 -1
- data/lib/aspera/rest.rb +34 -49
- data/lib/aspera/rest_list.rb +116 -0
- data/lib/aspera/sync/operations.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +8 -8
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/yaml.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +0 -0
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -7,11 +7,15 @@ require 'aspera/hash_ext'
|
|
|
7
7
|
require 'aspera/data_repository'
|
|
8
8
|
require 'aspera/transfer/spec'
|
|
9
9
|
require 'aspera/api/node'
|
|
10
|
+
require 'aspera/rest_list'
|
|
10
11
|
require 'base64'
|
|
11
12
|
|
|
12
13
|
module Aspera
|
|
13
14
|
module Api
|
|
15
|
+
# Aspera on Cloud API client
|
|
14
16
|
class AoC < Rest
|
|
17
|
+
include RestList
|
|
18
|
+
|
|
15
19
|
PRODUCT_NAME = 'Aspera on Cloud'
|
|
16
20
|
# use default workspace if it is set, else none
|
|
17
21
|
DEFAULT_WORKSPACE = ''
|
|
@@ -144,12 +148,11 @@ module Aspera
|
|
|
144
148
|
|
|
145
149
|
# Call `block` with same query using paging and response information.
|
|
146
150
|
# Block must return a 2 element `Array` with data and http response
|
|
147
|
-
# @param query [Hash]
|
|
148
|
-
# @param progress [nil, Object] Uses methods: `long_operation_running` and `long_operation_terminated`
|
|
151
|
+
# @param query [Hash] Additional query parameters
|
|
149
152
|
# @return [Hash] Items and total number of items
|
|
150
153
|
# @option return [Array<Hash>] :items The list of items
|
|
151
154
|
# @option return [Integer] :total The total number of items
|
|
152
|
-
def call_paging(query: {}
|
|
155
|
+
def call_paging(query: {})
|
|
153
156
|
Aspera.assert_type(query, Hash){'query'}
|
|
154
157
|
Aspera.assert(block_given?)
|
|
155
158
|
# set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
|
|
@@ -177,9 +180,9 @@ module Aspera
|
|
|
177
180
|
break if !max_items.nil? && item_list.count >= max_items
|
|
178
181
|
break if !max_pages.nil? && page_count >= max_pages
|
|
179
182
|
break if total_count&.<=(item_list.count)
|
|
180
|
-
|
|
183
|
+
RestParameters.instance.spinner_cb.call("#{item_list.count} / #{total_count}") unless total_count.eql?(item_list.count.to_s)
|
|
181
184
|
end
|
|
182
|
-
|
|
185
|
+
RestParameters.instance.spinner_cb.call(action: :success)
|
|
183
186
|
item_list = item_list[0..max_items - 1] if !max_items.nil? && item_list.count > max_items
|
|
184
187
|
return {items: item_list, total: total_count}
|
|
185
188
|
end
|
|
@@ -210,7 +213,7 @@ module Aspera
|
|
|
210
213
|
when 'upload' then %w[mkdir write]
|
|
211
214
|
when Array
|
|
212
215
|
Aspera.assert_array_all(levels, String){'access_levels'}
|
|
213
|
-
levels.each{ |level| Aspera.
|
|
216
|
+
levels.each{ |level| Aspera.assert_values(level, Node::ACCESS_LEVELS){'access_level'}}
|
|
214
217
|
levels
|
|
215
218
|
else Aspera.error_unexpected_value(levels){"access_levels must be a list of #{Node::ACCESS_LEVELS.join(', ')} or one of edit, preview, download, upload"}
|
|
216
219
|
end
|
|
@@ -232,8 +235,7 @@ module Aspera
|
|
|
232
235
|
username: nil,
|
|
233
236
|
password: nil,
|
|
234
237
|
workspace: nil,
|
|
235
|
-
secret_finder: nil
|
|
236
|
-
progress_disp: nil
|
|
238
|
+
secret_finder: nil
|
|
237
239
|
)
|
|
238
240
|
# Test here because link may set url
|
|
239
241
|
Aspera.assert(url, 'Missing mandatory option: url', type: ParameterError)
|
|
@@ -244,7 +246,6 @@ module Aspera
|
|
|
244
246
|
# key: access key
|
|
245
247
|
# value: associated secret
|
|
246
248
|
@secret_finder = secret_finder
|
|
247
|
-
@progress_disp = progress_disp
|
|
248
249
|
@workspace_name = workspace
|
|
249
250
|
@cache_user_info = nil
|
|
250
251
|
@cache_url_token_info = nil
|
|
@@ -309,7 +310,7 @@ module Aspera
|
|
|
309
310
|
# @param query [nil, Hash] Additional query
|
|
310
311
|
# @return [Hash] {items: , total: }
|
|
311
312
|
def read_with_paging(subpath, query = nil)
|
|
312
|
-
return self.class.call_paging(query: query
|
|
313
|
+
return self.class.call_paging(query: query) do |paged_query|
|
|
313
314
|
read(subpath, query: paged_query, ret: :both)
|
|
314
315
|
end
|
|
315
316
|
end
|
|
@@ -495,7 +496,7 @@ module Aspera
|
|
|
495
496
|
# in that case, the name is resolved and replaced with {type: , id: }
|
|
496
497
|
# @param package_data [Hash] The whole package creation payload
|
|
497
498
|
# @param rcpt_lst_field [String] The field in structure, i.e. recipients or bcc_recipients
|
|
498
|
-
# @param new_user_option [Hash]
|
|
499
|
+
# @param new_user_option [Hash] Additional fields for contact creation
|
|
499
500
|
# @return nil, `package_data` is modified
|
|
500
501
|
def resolve_package_recipients(package_data, rcpt_lst_field, new_user_option)
|
|
501
502
|
return unless package_data.key?(rcpt_lst_field)
|
|
@@ -513,7 +514,7 @@ module Aspera
|
|
|
513
514
|
# email: user, else dropbox
|
|
514
515
|
entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
|
|
515
516
|
begin
|
|
516
|
-
full_recipient_info = lookup_by_name(entity_type, short_recipient_info, query: {'
|
|
517
|
+
full_recipient_info = lookup_by_name(entity_type, short_recipient_info, query: {'workspace_id' => ws_id})
|
|
517
518
|
rescue EntityNotFound
|
|
518
519
|
# dropboxes cannot be created on the fly
|
|
519
520
|
Aspera.assert_values(entity_type, 'contacts', type: Error){"No such shared inbox in workspace #{ws_id}"}
|
data/lib/aspera/api/ats.rb
CHANGED
data/lib/aspera/api/cos_node.rb
CHANGED
|
@@ -7,9 +7,11 @@ require 'xmlsimple'
|
|
|
7
7
|
|
|
8
8
|
module Aspera
|
|
9
9
|
module Api
|
|
10
|
+
# Cloud Object Storage Node API Client
|
|
10
11
|
class CosNode < Node
|
|
11
12
|
IBM_CLOUD_TOKEN_URL = 'https://iam.cloud.ibm.com/identity'
|
|
12
13
|
TOKEN_FIELD = 'delegated_refresh_token'
|
|
14
|
+
FASP_INFO_KEYS = %w[ATSEndpoint AccessKey].freeze
|
|
13
15
|
class << self
|
|
14
16
|
def parameters_from_svc_credentials(service_credentials, bucket_region)
|
|
15
17
|
# check necessary contents
|
|
@@ -60,6 +62,9 @@ module Aspera
|
|
|
60
62
|
).body
|
|
61
63
|
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
|
62
64
|
Log.dump(:ats_info, ats_info)
|
|
65
|
+
Aspera.assert_hash_all(ats_info, String, nil){'ats_info'}
|
|
66
|
+
Aspera.assert((FASP_INFO_KEYS - ats_info.keys).empty?){'ats_info missing required keys'}
|
|
67
|
+
Aspera.assert_hash_all(ats_info['AccessKey'], String, String){'ats_info'}
|
|
63
68
|
@storage_credentials = {
|
|
64
69
|
'type' => 'token',
|
|
65
70
|
'token' => {TOKEN_FIELD => nil}
|
data/lib/aspera/api/faspex.rb
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/rest'
|
|
4
4
|
require 'aspera/oauth/base'
|
|
5
|
+
require 'aspera/rest_list'
|
|
5
6
|
require 'digest'
|
|
6
7
|
|
|
7
8
|
module Aspera
|
|
8
|
-
#
|
|
9
|
+
# OAuth for Faspex public link
|
|
9
10
|
class FaspexPubLink < OAuth::Base
|
|
10
11
|
class << self
|
|
11
12
|
attr_accessor :additional_info
|
|
@@ -55,7 +56,10 @@ module Aspera
|
|
|
55
56
|
end
|
|
56
57
|
OAuth::Factory.instance.register_token_creator(FaspexPubLink)
|
|
57
58
|
module Api
|
|
59
|
+
# Aspera Faspex 5 API Client
|
|
58
60
|
class Faspex < Aspera::Rest
|
|
61
|
+
include RestList
|
|
62
|
+
|
|
59
63
|
# endpoint for authentication API
|
|
60
64
|
PATH_AUTH = 'auth'
|
|
61
65
|
PATH_API_V5 = 'api/v5'
|
|
@@ -91,7 +95,7 @@ module Aspera
|
|
|
91
95
|
JOB_RUNNING = %w[queued working].freeze
|
|
92
96
|
PATH_STANDARD_ROOT = '/aspera/faspex'
|
|
93
97
|
PATH_API_DETECT = "#{PATH_API_V5}/#{PATH_HEALTH}"
|
|
94
|
-
|
|
98
|
+
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
|
95
99
|
HEADER_FASPEX_VERSION = 'X-IBM-Aspera'
|
|
96
100
|
EMAIL_NOTIF_LIST = %w[
|
|
97
101
|
welcome_email
|
|
@@ -136,6 +140,15 @@ module Aspera
|
|
|
136
140
|
end
|
|
137
141
|
attr_reader :pub_link_context
|
|
138
142
|
|
|
143
|
+
# @param url Faspex URL, can be a public link
|
|
144
|
+
# @param auth Authentication method: :boot (token in header), :web (open browser), :jwt (client_id + private key), :public_link (context in URL)
|
|
145
|
+
# @param password For :boot auth, the token copied directly from browser in developer mode
|
|
146
|
+
# @param client_id For :web and :jwt auth, the client_id of web UI application
|
|
147
|
+
# @param client_secret For :web auth, the client_secret of web UI application (not needed for :jwt)
|
|
148
|
+
# @param redirect_uri For :web auth, the redirect_uri of web UI application (must be the same as in application configuration)
|
|
149
|
+
# @param username For :jwt auth, the username of the user to impersonate
|
|
150
|
+
# @param private_key For :jwt auth, the private key to sign JWT token
|
|
151
|
+
# @param passphrase For :jwt auth, the passphrase of the private key
|
|
139
152
|
def initialize(
|
|
140
153
|
url:,
|
|
141
154
|
auth:,
|
data/lib/aspera/api/httpgw.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'aspera/log'
|
|
|
4
4
|
require 'aspera/rest'
|
|
5
5
|
require 'aspera/transfer/faux_file'
|
|
6
6
|
require 'aspera/assert'
|
|
7
|
+
require 'net/protocol'
|
|
7
8
|
require 'securerandom'
|
|
8
9
|
require 'websocket'
|
|
9
10
|
require 'base64'
|
|
@@ -11,6 +12,7 @@ require 'json'
|
|
|
11
12
|
|
|
12
13
|
module Aspera
|
|
13
14
|
module Api
|
|
15
|
+
# Aspera HTTP Gateway API client
|
|
14
16
|
# Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
|
|
15
17
|
# ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
|
|
16
18
|
# https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
|
data/lib/aspera/api/node.rb
CHANGED
|
@@ -6,16 +6,17 @@ require 'aspera/oauth'
|
|
|
6
6
|
require 'aspera/log'
|
|
7
7
|
require 'aspera/assert'
|
|
8
8
|
require 'aspera/environment'
|
|
9
|
-
require 'zlib'
|
|
10
9
|
require 'base64'
|
|
11
10
|
require 'openssl'
|
|
12
11
|
require 'pathname'
|
|
12
|
+
require 'zlib'
|
|
13
13
|
require 'net/ssh/buffer'
|
|
14
14
|
|
|
15
15
|
module Aspera
|
|
16
16
|
module Api
|
|
17
|
-
#
|
|
18
|
-
|
|
17
|
+
# Aspera Node API client
|
|
18
|
+
# with gen4 extensions (access keys)
|
|
19
|
+
class Node < Rest
|
|
19
20
|
# Format of node scope : node.<access key>:<scope>
|
|
20
21
|
module Scope
|
|
21
22
|
# Node sub-scopes
|
|
@@ -42,9 +43,10 @@ module Aspera
|
|
|
42
43
|
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
43
44
|
# Special HTTP Headers
|
|
44
45
|
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
45
|
-
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
46
46
|
HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
|
|
47
|
+
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
47
48
|
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
|
49
|
+
HEADER_ACCEPT_VERSION = 'Accept-Version'
|
|
48
50
|
# / in cloud
|
|
49
51
|
PATH_SEPARATOR = '/'
|
|
50
52
|
|
|
@@ -52,22 +54,36 @@ module Aspera
|
|
|
52
54
|
OAuth::Factory.instance.register_decoder(lambda{ |token| Node.decode_bearer_token(token)})
|
|
53
55
|
|
|
54
56
|
# Class instance variable, access with accessors on class
|
|
55
|
-
@
|
|
56
|
-
@use_node_cache = true
|
|
57
|
-
|
|
58
|
-
class << self
|
|
57
|
+
@api_options = {
|
|
59
58
|
# Set to false to read transfer parameters from download_setup
|
|
60
|
-
|
|
59
|
+
standard_ports: true,
|
|
61
60
|
# Set to false to bypass cache in redis
|
|
62
|
-
|
|
61
|
+
cache: true,
|
|
62
|
+
accept_v4: true
|
|
63
|
+
}
|
|
64
|
+
OPTIONS = @api_options.keys.freeze
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
attr_reader :api_options
|
|
63
68
|
attr_reader :use_dynamic_key
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
def api_options=(h)
|
|
71
|
+
Aspera.assert_type(h, Hash)
|
|
72
|
+
h.each do |k, v|
|
|
73
|
+
Aspera.assert(@api_options.key?(k.to_sym)){"unknown api option: #{k} (#{OPTIONS.join(', ')})"}
|
|
74
|
+
Aspera.assert_type(v, TrueClass, FalseClass){"api options value for #{k} should be boolean"}
|
|
75
|
+
@api_options[k.to_sym] = v
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Adds cache control header for node API /files/:id
|
|
80
|
+
# as globally specified to read request
|
|
81
|
+
# Use like this: read(..., headers: add_cache_control)
|
|
82
|
+
# @param headers [Hash] optional initial headers to add to
|
|
83
|
+
# @return [Hash] headers with cache control header added if needed
|
|
84
|
+
def add_cache_control(headers = {})
|
|
85
|
+
headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless api_options[:cache]
|
|
86
|
+
headers
|
|
71
87
|
end
|
|
72
88
|
|
|
73
89
|
# Set private key to be used
|
|
@@ -180,8 +196,8 @@ module Aspera
|
|
|
180
196
|
Aspera.assert(!access_key.nil?)
|
|
181
197
|
end
|
|
182
198
|
return {
|
|
183
|
-
|
|
184
|
-
'Authorization'
|
|
199
|
+
HEADER_X_ASPERA_ACCESS_KEY => access_key,
|
|
200
|
+
'Authorization' => bearer_auth
|
|
185
201
|
}
|
|
186
202
|
end
|
|
187
203
|
end
|
|
@@ -248,6 +264,50 @@ module Aspera
|
|
|
248
264
|
return false
|
|
249
265
|
end
|
|
250
266
|
|
|
267
|
+
# Read folder content, with pagination management for gen4, not recursive
|
|
268
|
+
# if `Accept-Version: 4.0` is not specified:
|
|
269
|
+
# if `page` and `per_page` are not specified, then all entries are returned.
|
|
270
|
+
# if either `page` or `per_page` is specified, then both are required, else 400
|
|
271
|
+
# if `Accept-Version: 4.0` is specified:
|
|
272
|
+
# those queries are not available: page (not mentioned), sort, min_size, max_size, min_modified_time, max_modified_time, target_id, target_node_id, files_prefetch_count, page, name_iglob : either ignored or result in API error 400.
|
|
273
|
+
# query include is accepted, but seems to do nothing as access_levels and recursive_counts are already included in results.
|
|
274
|
+
# query `iteration_token` is accepted and allows to get paginated results, with `X-Aspera-Next-Iteration-Token` header in response to get next page token. `X-Aspera-Total-Count` header gives total count of entries.
|
|
275
|
+
def read_folder_content(file_id, query = nil, exception: true, path: nil)
|
|
276
|
+
folder_items = []
|
|
277
|
+
begin
|
|
278
|
+
query ||= {}
|
|
279
|
+
headers = self.class.add_cache_control
|
|
280
|
+
use_v4 = self.class.api_options[:accept_v4]
|
|
281
|
+
return read("files/#{file_id}/files", query, headers: headers) unless use_v4 || query.key?('page') || query.key?('per_page')
|
|
282
|
+
if use_v4
|
|
283
|
+
headers[HEADER_ACCEPT_VERSION] = '4.0'
|
|
284
|
+
query['per_page'] = 1000 unless query.key?('per_page')
|
|
285
|
+
elsif query.key?('per_page') && !query.key?('page')
|
|
286
|
+
query['page'] = 0
|
|
287
|
+
end
|
|
288
|
+
loop do
|
|
289
|
+
RestParameters.instance.spinner_cb.call(folder_items.count)
|
|
290
|
+
data, http = read("files/#{file_id}/files", query, headers: headers, ret: :both)
|
|
291
|
+
folder_items.concat(data)
|
|
292
|
+
if use_v4
|
|
293
|
+
iteration_token = http[HEADER_X_NEXT_ITER_TOKEN]
|
|
294
|
+
break if iteration_token.nil? || iteration_token.empty?
|
|
295
|
+
query['iteration_token'] = iteration_token
|
|
296
|
+
else
|
|
297
|
+
break if data['item_count'].eql?(0)
|
|
298
|
+
query['offset'] += data['item_count']
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
rescue StandardError => e
|
|
302
|
+
raise e if exception
|
|
303
|
+
Log.log.warn{"#{path}: #{e.class} #{e.message}"}
|
|
304
|
+
Log.log.debug{(['Backtrace:'] + e.backtrace).join("\n")}
|
|
305
|
+
ensure
|
|
306
|
+
RestParameters.instance.spinner_cb.call(folder_items.count, action: :success)
|
|
307
|
+
end
|
|
308
|
+
folder_items
|
|
309
|
+
end
|
|
310
|
+
|
|
251
311
|
# Recursively browse in a folder (with non-recursive method)
|
|
252
312
|
# Entries of folders are processed if the processing method returns true
|
|
253
313
|
# Links are processed on the respective node
|
|
@@ -267,14 +327,7 @@ module Aspera
|
|
|
267
327
|
current_item = folders_to_explore.shift
|
|
268
328
|
Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
|
|
269
329
|
# Get folder content
|
|
270
|
-
folder_contents =
|
|
271
|
-
begin
|
|
272
|
-
# TODO: use header
|
|
273
|
-
read("files/#{current_item[:id]}/files", query, **self.class.cache_control)
|
|
274
|
-
rescue StandardError => e
|
|
275
|
-
Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
|
|
276
|
-
[]
|
|
277
|
-
end
|
|
330
|
+
folder_contents = read_folder_content(current_item[:id], query, exception: false, path: current_item[:path])
|
|
278
331
|
Log.dump(:folder_contents, folder_contents)
|
|
279
332
|
folder_contents.each do |entry|
|
|
280
333
|
if entry.key?('error')
|
|
@@ -309,7 +362,7 @@ module Aspera
|
|
|
309
362
|
# @param path [String] file or folder path (end with "/" is like setting process_last_link)
|
|
310
363
|
# @param process_last_link [Boolean] if true, follow the last link
|
|
311
364
|
# @return [Hash] Result data
|
|
312
|
-
# @option return [
|
|
365
|
+
# @option return [Rest] :api REST client instance
|
|
313
366
|
# @option return [String] :file_id File identifier
|
|
314
367
|
def resolve_api_fid(top_file_id, path, process_last_link = false)
|
|
315
368
|
Aspera.assert_type(top_file_id, String)
|
|
@@ -445,7 +498,7 @@ module Aspera
|
|
|
445
498
|
# Add application specific tags (AoC)
|
|
446
499
|
@app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: @app_info) unless @app_info.nil?
|
|
447
500
|
# Add remote host info
|
|
448
|
-
if self.class.
|
|
501
|
+
if self.class.api_options[:standard_ports]
|
|
449
502
|
# Get default TCP/UDP ports and transfer user
|
|
450
503
|
transfer_spec.merge!(Transfer::Spec::AK_TSPEC_BASE)
|
|
451
504
|
# By default: same address as node API
|
|
@@ -456,7 +509,7 @@ module Aspera
|
|
|
456
509
|
# Get the transfer user from info on access key
|
|
457
510
|
transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
|
|
458
511
|
# Get settings from name.value array to hash key.value
|
|
459
|
-
settings = info['settings']&.
|
|
512
|
+
settings = info['settings']&.to_h{ |i| [i['name'], i['value']]}
|
|
460
513
|
# Check WSS ports
|
|
461
514
|
Transfer::Spec::WSS_FIELDS.each do |i|
|
|
462
515
|
transfer_spec[i] = settings[i] if settings.key?(i)
|
|
@@ -83,16 +83,15 @@ module Aspera
|
|
|
83
83
|
|
|
84
84
|
# @return [Hash] with key = file name (String), and value = path to file
|
|
85
85
|
def file_paths
|
|
86
|
-
return SDK_FILES.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
end
|
|
86
|
+
return SDK_FILES.to_h do |v|
|
|
87
|
+
[v.to_s, begin
|
|
88
|
+
path(v)
|
|
89
|
+
rescue Errno::ENOENT => e
|
|
90
|
+
e.message.gsub(/.*assertion failed: /, '').gsub(/\): .*/, ')')
|
|
91
|
+
rescue => e
|
|
92
|
+
e.message
|
|
93
|
+
end]
|
|
94
|
+
end
|
|
96
95
|
end
|
|
97
96
|
|
|
98
97
|
# TODO: if using another product than SDK, should use files from there
|
data/lib/aspera/cli/error.rb
CHANGED
|
@@ -9,7 +9,15 @@ module Aspera
|
|
|
9
9
|
class MissingArgument < Error; end
|
|
10
10
|
class NoSuchElement < Error; end
|
|
11
11
|
|
|
12
|
+
# Raised when a lookup for a specific entity fails to return exactly one result.
|
|
13
|
+
#
|
|
14
|
+
# Provides a formatted message indicating whether the entity was missing
|
|
15
|
+
# or if multiple matches were found (ambiguity).
|
|
12
16
|
class BadIdentifier < Error
|
|
17
|
+
# @param res_type [String] The type of entity being looked up (e.g., 'user').
|
|
18
|
+
# @param res_id [String] The value of the identifier that failed.
|
|
19
|
+
# @param field [String] The name of the field used for lookup (defaults to 'identifier').
|
|
20
|
+
# @param count [Integer] The number of matches found (0 for not found, >1 for ambiguous).
|
|
13
21
|
def initialize(res_type, res_id, field: 'identifier', count: 0)
|
|
14
22
|
msg = count.eql?(0) ? 'not found' : "found #{count}"
|
|
15
23
|
super("#{res_type} with #{field}=#{res_id}: #{msg}")
|
data/lib/aspera/cli/formatter.rb
CHANGED
|
@@ -128,20 +128,34 @@ module Aspera
|
|
|
128
128
|
@spinner = nil
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
def long_operation_running(title = '')
|
|
131
|
+
def long_operation(title = nil, action: :spin)
|
|
133
132
|
return unless Environment.terminal?
|
|
134
|
-
if
|
|
133
|
+
return if %i[error data].include?(@options[:display])
|
|
134
|
+
|
|
135
|
+
# Handle the "delayed start" state
|
|
136
|
+
return @spinner = :starting if action == :spin && @spinner.nil?
|
|
137
|
+
|
|
138
|
+
# Cleanup if we try to stop a spinner that never actually started
|
|
139
|
+
@spinner = nil if action != :spin && @spinner == :starting
|
|
140
|
+
return if @spinner.nil?
|
|
141
|
+
|
|
142
|
+
# Initialize the real TTY object if it's currently just the :starting symbol
|
|
143
|
+
if @spinner == :starting
|
|
135
144
|
@spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
|
|
145
|
+
@spinner.update(title: '')
|
|
136
146
|
@spinner.start
|
|
137
147
|
end
|
|
138
|
-
@spinner.update(title: title)
|
|
139
|
-
@spinner.spin
|
|
140
|
-
end
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
@spinner.update(title: title) if title
|
|
150
|
+
|
|
151
|
+
case action
|
|
152
|
+
when :spin
|
|
153
|
+
@spinner.spin
|
|
154
|
+
when :success, :fail
|
|
155
|
+
action == :success ? @spinner.success : @spinner.error
|
|
156
|
+
@spinner.stop
|
|
157
|
+
@spinner = nil
|
|
158
|
+
end
|
|
145
159
|
end
|
|
146
160
|
|
|
147
161
|
def declare_options(options)
|
|
@@ -150,9 +164,9 @@ module Aspera
|
|
|
150
164
|
else
|
|
151
165
|
{}
|
|
152
166
|
end
|
|
167
|
+
options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
|
|
153
168
|
options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
|
|
154
169
|
options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
|
|
155
|
-
options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
|
|
156
170
|
options.declare(
|
|
157
171
|
:fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
|
|
158
172
|
allowed: [String, Array, Regexp, Proc],
|
|
@@ -178,6 +192,8 @@ module Aspera
|
|
|
178
192
|
@options[option_symbol] = value
|
|
179
193
|
# special handling of some options
|
|
180
194
|
case option_symbol
|
|
195
|
+
when :format
|
|
196
|
+
@options[:display] = value.eql?(:table) ? :info : :data
|
|
181
197
|
when :output
|
|
182
198
|
$stdout = if value.eql?('-')
|
|
183
199
|
STDOUT # rubocop:disable Style/GlobalStdStream
|
|
@@ -197,7 +213,7 @@ module Aspera
|
|
|
197
213
|
nil
|
|
198
214
|
end
|
|
199
215
|
|
|
200
|
-
#
|
|
216
|
+
# Main output method
|
|
201
217
|
# data: for requested data, not displayed if level==error
|
|
202
218
|
# info: additional info, displayed if level==info
|
|
203
219
|
# error: always displayed on stderr
|
data/lib/aspera/cli/info.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Aspera
|
|
|
7
7
|
CMD_NAME = 'ascli'
|
|
8
8
|
# Name of the containing gem, same as in <gem name>.gemspec
|
|
9
9
|
GEM_NAME = 'aspera-cli'
|
|
10
|
-
DOC_URL =
|
|
10
|
+
DOC_URL = 'https://ibm.biz/ascli-doc'
|
|
11
|
+
RUBYDOC_URL = "https://www.rubydoc.info/gems/#{GEM_NAME}"
|
|
11
12
|
GEM_URL = "https://rubygems.org/gems/#{GEM_NAME}"
|
|
12
13
|
SRC_URL = 'https://github.com/IBM/aspera-cli'
|
|
13
14
|
CONTAINER = 'docker.io/martinlaurent/ascli'
|
data/lib/aspera/cli/main.rb
CHANGED
|
@@ -12,18 +12,34 @@ require 'aspera/cli/hints'
|
|
|
12
12
|
require 'aspera/secret_hider'
|
|
13
13
|
require 'aspera/log'
|
|
14
14
|
require 'aspera/assert'
|
|
15
|
+
require 'net/ssh/errors'
|
|
16
|
+
require 'openssl'
|
|
15
17
|
|
|
16
18
|
module Aspera
|
|
17
19
|
module Cli
|
|
18
20
|
# Global objects shared with plugins
|
|
19
21
|
class Context
|
|
22
|
+
# @!attribute [rw] options
|
|
23
|
+
# @return [Manager] the command line options manager
|
|
24
|
+
# @!attribute [rw] transfer
|
|
25
|
+
# @return [TransferAgent] the transfer agent, used by transfer plugins
|
|
26
|
+
# @!attribute [rw] config
|
|
27
|
+
# @return [Plugins::Config] the configuration plugin, used by plugins to get configuration values and presets
|
|
28
|
+
# @!attribute [rw] formatter
|
|
29
|
+
# @return [Formatter] the formatter, used by plugins to display results and messages
|
|
30
|
+
# @!attribute [rw] persistency
|
|
31
|
+
# @return [Object] # whatever the type is
|
|
32
|
+
# @!attribute [rw] man_header
|
|
33
|
+
# @return [Boolean] whether to display the manual header in plugin help
|
|
20
34
|
MEMBERS = %i[options transfer config formatter persistency man_header].freeze
|
|
21
35
|
attr_accessor(*MEMBERS)
|
|
22
36
|
|
|
37
|
+
# Initialize all members to nil, so that they are defined and can be validated later
|
|
23
38
|
def initialize
|
|
24
|
-
|
|
39
|
+
MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
|
|
25
40
|
end
|
|
26
41
|
|
|
42
|
+
# Validate that all members are set, raise exception if not
|
|
27
43
|
def validate
|
|
28
44
|
MEMBERS.each do |i|
|
|
29
45
|
Aspera.assert(instance_variable_defined?(:"@#{i}"))
|
|
@@ -35,7 +51,7 @@ module Aspera
|
|
|
35
51
|
transfer.eql?(:only_manual)
|
|
36
52
|
end
|
|
37
53
|
|
|
38
|
-
def only_manual
|
|
54
|
+
def only_manual!
|
|
39
55
|
@transfer = :only_manual
|
|
40
56
|
end
|
|
41
57
|
end
|
|
@@ -89,13 +105,15 @@ module Aspera
|
|
|
89
105
|
status_table.each do |item|
|
|
90
106
|
worst = TransferAgent.session_status(item[STATUS_FIELD])
|
|
91
107
|
global_status = worst unless worst.eql?(:success)
|
|
92
|
-
item[STATUS_FIELD] = item[STATUS_FIELD].
|
|
108
|
+
item[STATUS_FIELD] = item[STATUS_FIELD].join(',')
|
|
93
109
|
end
|
|
94
110
|
raise global_status unless global_status.eql?(:success)
|
|
95
111
|
return result_object_list(status_table)
|
|
96
112
|
end
|
|
97
113
|
|
|
98
114
|
# Display image for that URL or directly blob
|
|
115
|
+
#
|
|
116
|
+
# @param url_or_blob [String] URL or blob to display as image
|
|
99
117
|
def result_image(url_or_blob)
|
|
100
118
|
return {type: :image, data: url_or_blob}
|
|
101
119
|
end
|
|
@@ -111,6 +129,9 @@ module Aspera
|
|
|
111
129
|
end
|
|
112
130
|
|
|
113
131
|
# A list of values
|
|
132
|
+
#
|
|
133
|
+
# @param data [Array] The list of values
|
|
134
|
+
# @param name [String] The name of the list (used for display)
|
|
114
135
|
def result_value_list(data, name: 'id')
|
|
115
136
|
Aspera.assert_type(data, Array)
|
|
116
137
|
Aspera.assert_type(name, String)
|
|
@@ -155,7 +176,9 @@ module Aspera
|
|
|
155
176
|
execute_command = true
|
|
156
177
|
# Catch exceptions
|
|
157
178
|
begin
|
|
158
|
-
|
|
179
|
+
init_agents_and_options
|
|
180
|
+
# Find plugins, shall be after parse! ?
|
|
181
|
+
Plugins::Factory.instance.add_plugins_from_lookup_folders
|
|
159
182
|
# Help requested without command ? (plugins must be known here)
|
|
160
183
|
show_usage if @option_help && @context.options.command_or_arg_empty?
|
|
161
184
|
generate_bash_completion if @bash_completion
|
|
@@ -253,17 +276,11 @@ module Aspera
|
|
|
253
276
|
return
|
|
254
277
|
end
|
|
255
278
|
|
|
256
|
-
def init_agents_options_plugins
|
|
257
|
-
init_agents_and_options
|
|
258
|
-
# Find plugins, shall be after parse! ?
|
|
259
|
-
Plugins::Factory.instance.add_plugins_from_lookup_folders
|
|
260
|
-
end
|
|
261
|
-
|
|
262
279
|
def show_usage(all: true, exit: true)
|
|
263
280
|
# Display main plugin options (+config)
|
|
264
281
|
@context.formatter.display_message(:error, @context.options.parser)
|
|
265
282
|
if all
|
|
266
|
-
@context.only_manual
|
|
283
|
+
@context.only_manual!
|
|
267
284
|
# List plugins that have a "require" field, i.e. all but main plugin
|
|
268
285
|
Plugins::Factory.instance.plugin_list.each do |plugin_name_sym|
|
|
269
286
|
# Config was already included in the global options
|
|
@@ -283,6 +300,7 @@ module Aspera
|
|
|
283
300
|
|
|
284
301
|
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
|
285
302
|
def init_agents_and_options
|
|
303
|
+
@context.man_header = true
|
|
286
304
|
# Create formatter, in case there is an exception, it is used to display.
|
|
287
305
|
@context.formatter = Formatter.new
|
|
288
306
|
# Create command line manager with arguments
|
|
@@ -336,7 +354,7 @@ module Aspera
|
|
|
336
354
|
OPTIONS
|
|
337
355
|
#{t}Options begin with a '-' (minus), and value is provided on command line.
|
|
338
356
|
#{t}Special values are supported beginning with special prefix @pfx:, where pfx is one of:
|
|
339
|
-
#{t}#{ExtendedValue.instance.modifiers.
|
|
357
|
+
#{t}#{ExtendedValue.instance.modifiers.join(', ')}
|
|
340
358
|
#{t}Dates format is 'DD-MM-YY HH:MM:SS', or 'now' or '-<num>h'
|
|
341
359
|
|
|
342
360
|
ARGS
|