aspera-cli 4.23.0 → 4.24.1
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 +37 -1
- data/CONTRIBUTING.md +86 -29
- data/README.md +2109 -1300
- data/bin/ascli +2 -1
- data/bin/asession +4 -4
- data/lib/aspera/agent/base.rb +4 -0
- data/lib/aspera/agent/connect.rb +20 -18
- data/lib/aspera/agent/desktop.rb +14 -11
- data/lib/aspera/agent/direct.rb +39 -31
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +9 -11
- data/lib/aspera/agent/transferd.rb +18 -11
- data/lib/aspera/api/aoc.rb +44 -31
- data/lib/aspera/api/cos_node.rb +7 -5
- data/lib/aspera/api/httpgw.rb +15 -18
- data/lib/aspera/api/node.rb +104 -22
- data/lib/aspera/ascmd.rb +22 -16
- data/lib/aspera/ascp/installation.rb +37 -40
- data/lib/aspera/ascp/management.rb +5 -4
- data/lib/aspera/assert.rb +54 -23
- data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
- data/lib/aspera/cli/error.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +28 -29
- data/lib/aspera/cli/formatter.rb +191 -168
- data/lib/aspera/cli/hints.rb +29 -3
- data/lib/aspera/cli/main.rb +138 -107
- data/lib/aspera/cli/manager.rb +50 -30
- data/lib/aspera/cli/plugin.rb +148 -77
- data/lib/aspera/cli/plugin_factory.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +189 -70
- data/lib/aspera/cli/plugins/ats.rb +15 -13
- data/lib/aspera/cli/plugins/config.rb +100 -214
- data/lib/aspera/cli/plugins/console.rb +49 -18
- data/lib/aspera/cli/plugins/cos.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +45 -51
- data/lib/aspera/cli/plugins/faspex5.rb +164 -165
- data/lib/aspera/cli/plugins/faspio.rb +6 -5
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +144 -162
- data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
- data/lib/aspera/cli/plugins/preview.rb +26 -29
- data/lib/aspera/cli/plugins/server.rb +28 -28
- data/lib/aspera/cli/plugins/shares.rb +40 -28
- data/lib/aspera/cli/sync_actions.rb +101 -80
- data/lib/aspera/cli/transfer_agent.rb +51 -50
- data/lib/aspera/cli/transfer_progress.rb +29 -20
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +157 -0
- data/lib/aspera/colors.rb +13 -8
- data/lib/aspera/command_line_builder.rb +28 -22
- data/lib/aspera/command_line_converter.rb +31 -0
- data/lib/aspera/environment.rb +145 -101
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +3 -2
- data/lib/aspera/hash_ext.rb +1 -1
- data/lib/aspera/id_generator.rb +10 -10
- data/lib/aspera/keychain/base.rb +18 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -12
- data/lib/aspera/keychain/factory.rb +9 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
- data/lib/aspera/keychain/macos_security.rb +13 -13
- data/lib/aspera/log.rb +91 -19
- data/lib/aspera/nagios.rb +5 -6
- data/lib/aspera/node_simulator.rb +12 -7
- data/lib/aspera/oauth/base.rb +5 -3
- data/lib/aspera/oauth/factory.rb +24 -18
- data/lib/aspera/oauth/jwt.rb +13 -1
- data/lib/aspera/oauth/url_json.rb +3 -3
- data/lib/aspera/oauth/web.rb +5 -3
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -3
- data/lib/aspera/preview/generator.rb +25 -12
- data/lib/aspera/preview/terminal.rb +10 -7
- data/lib/aspera/preview/utils.rb +11 -9
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +8 -6
- data/lib/aspera/proxy_auto_config.rb +1 -1
- data/lib/aspera/rest.rb +29 -22
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +46 -40
- data/lib/aspera/ssh.rb +13 -3
- data/lib/aspera/sync/args.schema.yaml +102 -0
- data/lib/aspera/sync/conf.schema.yaml +701 -0
- data/lib/aspera/sync/database.rb +83 -0
- data/lib/aspera/sync/operations.rb +296 -0
- data/lib/aspera/temp_file_manager.rb +3 -2
- data/lib/aspera/transfer/error.rb +1 -1
- data/lib/aspera/transfer/error_info.rb +1 -2
- data/lib/aspera/transfer/faux_file.rb +11 -10
- data/lib/aspera/transfer/parameters.rb +6 -5
- data/lib/aspera/transfer/spec.rb +15 -1
- data/lib/aspera/transfer/spec.schema.yaml +316 -293
- data/lib/aspera/transfer/spec_doc.rb +34 -16
- data/lib/aspera/transfer/uri.rb +5 -5
- data/lib/aspera/uri_reader.rb +14 -10
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +15 -13
- metadata.gz.sig +0 -0
- data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
- data/lib/aspera/transfer/convert.rb +0 -29
- data/lib/aspera/transfer/sync.rb +0 -232
- data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
- data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
data/lib/aspera/api/aoc.rb
CHANGED
@@ -61,7 +61,7 @@ module Aspera
|
|
61
61
|
# class static methods
|
62
62
|
class << self
|
63
63
|
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
64
|
-
def get_client_info(client_name=nil)
|
64
|
+
def get_client_info(client_name = nil)
|
65
65
|
client_key = client_name.nil? ? GLOBAL_CLIENT_APPS.first : client_name.to_sym
|
66
66
|
return client_key, DataRepository.instance.item(client_key)
|
67
67
|
end
|
@@ -82,23 +82,25 @@ module Aspera
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def saas_url?(url)
|
85
|
-
url.
|
85
|
+
URI.parse(url).host&.end_with?(".#{SAAS_DOMAIN_PROD}")
|
86
|
+
rescue URI::InvalidURIError
|
87
|
+
false
|
86
88
|
end
|
87
89
|
|
88
90
|
# @param url [String] URL of AoC public link
|
89
91
|
# @return [Hash] information about public link, or nil if not a public link
|
90
92
|
def link_info(url)
|
91
93
|
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET')[:http].uri
|
92
|
-
Log.
|
94
|
+
Log.dump(:final_uri, final_uri, level: :trace1)
|
93
95
|
org_domain = split_org_domain(final_uri)
|
94
96
|
if (m = final_uri.path.match(%r{/oauth2/([^/]+)/login$}))
|
95
97
|
org_domain[:organization] = m[1] if org_domain[:organization].nil?
|
96
98
|
else
|
97
99
|
Log.log.debug{"path=#{final_uri.path} does not end with /login"}
|
98
100
|
end
|
99
|
-
raise 'AoC shall redirect to login page with a query' if final_uri.query.nil?
|
101
|
+
raise Error, 'AoC shall redirect to login page with a query' if final_uri.query.nil?
|
100
102
|
query = Rest.query_to_h(final_uri.query)
|
101
|
-
Log.
|
103
|
+
Log.dump(:query, query, level: :trace1)
|
102
104
|
# is that a public link ?
|
103
105
|
if query.key?('token')
|
104
106
|
Log.log.warn{"Unknown pub link path: #{final_uri.path}"} unless PUBLIC_LINK_PATHS.include?(final_uri.path)
|
@@ -109,7 +111,7 @@ module Aspera
|
|
109
111
|
token: query['token']
|
110
112
|
}
|
111
113
|
end
|
112
|
-
if query
|
114
|
+
if query.key?('state')
|
113
115
|
# can be a private link
|
114
116
|
state_uri = URI.parse(query['state'])
|
115
117
|
if state_uri.query && query['redirect_uri']
|
@@ -132,7 +134,7 @@ module Aspera
|
|
132
134
|
end
|
133
135
|
end
|
134
136
|
end
|
135
|
-
Log.
|
137
|
+
Log.dump(:org_domain, org_domain)
|
136
138
|
return {
|
137
139
|
instance_domain: org_domain[:domain],
|
138
140
|
organization: org_domain[:organization]
|
@@ -166,7 +168,7 @@ module Aspera
|
|
166
168
|
}
|
167
169
|
# analyze type of url
|
168
170
|
url_info = AoC.link_info(url)
|
169
|
-
Log.
|
171
|
+
Log.dump(:url_info, url_info)
|
170
172
|
@private_link = url_info[:private_link]
|
171
173
|
auth_params[:grant_method] = if url_info.key?(:token)
|
172
174
|
:url_json
|
@@ -212,7 +214,7 @@ module Aspera
|
|
212
214
|
end
|
213
215
|
|
214
216
|
def public_link
|
215
|
-
return
|
217
|
+
return unless auth_params[:grant_method].eql?(:url_json)
|
216
218
|
return @cache_url_token_info unless @cache_url_token_info.nil?
|
217
219
|
# TODO: can there be several in list ?
|
218
220
|
@cache_url_token_info = read('url_tokens').first
|
@@ -245,12 +247,12 @@ module Aspera
|
|
245
247
|
end
|
246
248
|
|
247
249
|
def workspace
|
248
|
-
|
250
|
+
Aspera.assert(!@workspace_info.nil?){'AoC workspace context is not set'}
|
249
251
|
@workspace_info
|
250
252
|
end
|
251
253
|
|
252
254
|
def home
|
253
|
-
|
255
|
+
Aspera.assert(!@home_info.nil?){'AoC home context is not set'}
|
254
256
|
@home_info
|
255
257
|
end
|
256
258
|
|
@@ -294,8 +296,8 @@ module Aspera
|
|
294
296
|
name: ws_info['name']
|
295
297
|
}
|
296
298
|
end
|
297
|
-
Log.
|
298
|
-
return
|
299
|
+
Log.dump(:context, @workspace_info)
|
300
|
+
return unless application.eql?(:files)
|
299
301
|
@home_info =
|
300
302
|
if !public_link.nil?
|
301
303
|
assert_public_link_types(['view_shared_file'])
|
@@ -322,7 +324,7 @@ module Aspera
|
|
322
324
|
}
|
323
325
|
end
|
324
326
|
raise "Cannot get user's home node id, check your default workspace or specify one" if @home_info[:node_id].to_s.empty?
|
325
|
-
Log.
|
327
|
+
Log.dump(:context, @home_info)
|
326
328
|
end
|
327
329
|
|
328
330
|
# @param node_id [String] identifier of node in AoC
|
@@ -334,9 +336,7 @@ module Aspera
|
|
334
336
|
def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::SCOPE_USER, package_info: nil)
|
335
337
|
Aspera.assert_type(node_id, String)
|
336
338
|
node_info = read("nodes/#{node_id}")
|
337
|
-
if workspace_name.nil? && !workspace_id.nil?
|
338
|
-
workspace_name = read("workspaces/#{workspace_id}")['name']
|
339
|
-
end
|
339
|
+
workspace_name = read("workspaces/#{workspace_id}")['name'] if workspace_name.nil? && !workspace_id.nil?
|
340
340
|
app_info = {
|
341
341
|
api: self, # for callback
|
342
342
|
app: package_info.nil? ? FILES_APP : PACKAGES_APP,
|
@@ -345,7 +345,7 @@ module Aspera
|
|
345
345
|
workspace_name: workspace_name
|
346
346
|
}
|
347
347
|
if PACKAGES_APP.eql?(app_info[:app])
|
348
|
-
|
348
|
+
Aspera.assert(!package_info.nil?){'package info required'}
|
349
349
|
app_info[:package_id] = package_info['id']
|
350
350
|
app_info[:package_name] = package_info['name']
|
351
351
|
end
|
@@ -383,7 +383,7 @@ module Aspera
|
|
383
383
|
Aspera.assert(pkg_data.key?('metadata')){"package requires metadata: #{meta_schema}"}
|
384
384
|
pkg_meta = pkg_data['metadata']
|
385
385
|
Aspera.assert_type(pkg_meta, Array){'metadata'}
|
386
|
-
Log.
|
386
|
+
Log.dump(:metadata, pkg_meta)
|
387
387
|
pkg_meta.each do |field|
|
388
388
|
Aspera.assert_type(field, Hash){'metadata field'}
|
389
389
|
Aspera.assert(field.key?('name')){'metadata field must have name'}
|
@@ -443,7 +443,7 @@ module Aspera
|
|
443
443
|
end
|
444
444
|
# replace with resolved elements
|
445
445
|
package_data[recipient_list_field] = resolved_list
|
446
|
-
return
|
446
|
+
return
|
447
447
|
end
|
448
448
|
|
449
449
|
# CLI allows simplified format for metadata: transform if necessary for API
|
@@ -461,7 +461,7 @@ module Aspera
|
|
461
461
|
pkg_data['metadata'] = api_meta
|
462
462
|
else Aspera.error_unexpected_value(pkg_meta.class)
|
463
463
|
end
|
464
|
-
return
|
464
|
+
return
|
465
465
|
end
|
466
466
|
|
467
467
|
# create a package
|
@@ -480,18 +480,24 @@ module Aspera
|
|
480
480
|
|
481
481
|
validate_metadata(package_data) if validate_meta
|
482
482
|
|
483
|
+
# tell AoC what to expect in package: 1 transfer (can also be done after transfer)
|
484
|
+
# TODO: if multi session was used we should probably tell
|
485
|
+
# also, currently no "multi-source" , i.e. only from client-side files, unless "node" agent is used
|
486
|
+
# `single_source` is required to allow web UI to ask for CSEAR password on download, see API doc
|
487
|
+
package_data.merge!({
|
488
|
+
'single_source' => true,
|
489
|
+
'sent' => true,
|
490
|
+
'transfers_expected' => 1
|
491
|
+
})
|
492
|
+
|
483
493
|
# create a new package container
|
484
494
|
created_package = create('packages', package_data)
|
485
495
|
|
486
496
|
package_node_api = node_api_from(
|
487
497
|
node_id: created_package['node_id'],
|
488
498
|
workspace_id: created_package['workspace_id'],
|
489
|
-
package_info: created_package
|
490
|
-
|
491
|
-
# tell AoC what to expect in package: 1 transfer (can also be done after transfer)
|
492
|
-
# TODO: if multi session was used we should probably tell
|
493
|
-
# also, currently no "multi-source" , i.e. only from client-side files, unless "node" agent is used
|
494
|
-
update("packages/#{created_package['id']}", {'sent' => true, 'transfers_expected' => 1})
|
499
|
+
package_info: created_package
|
500
|
+
)
|
495
501
|
|
496
502
|
return {
|
497
503
|
spec: package_node_api.transfer_spec_gen4(created_package['contents_file_id'], Transfer::Spec::DIRECTION_SEND),
|
@@ -510,8 +516,10 @@ module Aspera
|
|
510
516
|
transfer_spec.deep_merge!({
|
511
517
|
'tags' => {
|
512
518
|
Transfer::Spec::TAG_RESERVED => {
|
519
|
+
'app' => app_info[:app],
|
513
520
|
'usage_id' => "aspera.files.workspace.#{app_info[:workspace_id]}", # activity tracking
|
514
521
|
'files' => {
|
522
|
+
'node_id' => app_info[:node_info]['id'],
|
515
523
|
'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
|
516
524
|
'workspace_name' => app_info[:workspace_name], # activity tracking
|
517
525
|
'workspace_id' => app_info[:workspace_id]
|
@@ -530,8 +538,15 @@ module Aspera
|
|
530
538
|
case app_info[:app]
|
531
539
|
when FILES_APP
|
532
540
|
file_id = transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['node']['file_id']
|
533
|
-
transfer_spec.deep_merge!({
|
534
|
-
|
541
|
+
transfer_spec.deep_merge!({
|
542
|
+
'tags' => {
|
543
|
+
Transfer::Spec::TAG_RESERVED => {
|
544
|
+
'files' => {
|
545
|
+
'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"
|
546
|
+
}
|
547
|
+
}
|
548
|
+
}
|
549
|
+
}) unless transfer_spec.key?('remote_access_key')
|
535
550
|
when PACKAGES_APP
|
536
551
|
transfer_spec.deep_merge!({
|
537
552
|
'tags' => {
|
@@ -545,8 +560,6 @@ module Aspera
|
|
545
560
|
}
|
546
561
|
})
|
547
562
|
end
|
548
|
-
transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
|
549
|
-
transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['app'] = app_info[:app]
|
550
563
|
end
|
551
564
|
|
552
565
|
# Callback from Plugins::Node
|
data/lib/aspera/api/cos_node.rb
CHANGED
@@ -14,13 +14,13 @@ module Aspera
|
|
14
14
|
def parameters_from_svc_credentials(service_credentials, bucket_region)
|
15
15
|
# check necessary contents
|
16
16
|
Aspera.assert_type(service_credentials, Hash){'service_credentials'}
|
17
|
-
|
17
|
+
Log.dump(:service_credentials, service_credentials)
|
18
18
|
%w[apikey resource_instance_id endpoints].each do |field|
|
19
19
|
Aspera.assert(service_credentials.key?(field)){"service_credentials must have a field: #{field}"}
|
20
20
|
end
|
21
21
|
# read endpoints from service provided in service credentials
|
22
22
|
endpoints = Aspera::Rest.new(base_url: service_credentials['endpoints']).read('')
|
23
|
-
|
23
|
+
Log.dump(:endpoints, endpoints)
|
24
24
|
endpoint = endpoints.dig('service-endpoints', 'regional', bucket_region, 'public', bucket_region)
|
25
25
|
raise "no such region: #{bucket_region}" if endpoint.nil?
|
26
26
|
return {
|
@@ -48,7 +48,8 @@ module Aspera
|
|
48
48
|
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
49
49
|
response_type: 'cloud_iam',
|
50
50
|
apikey: @api_key
|
51
|
-
}
|
51
|
+
}
|
52
|
+
)
|
52
53
|
# read FASP connection information for bucket
|
53
54
|
xml_result_text = s3_api.call(
|
54
55
|
operation: 'GET',
|
@@ -57,7 +58,7 @@ module Aspera
|
|
57
58
|
query: {'faspConnectionInfo' => nil}
|
58
59
|
)[:http].body
|
59
60
|
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
60
|
-
|
61
|
+
Log.dump(:ats_info, ats_info)
|
61
62
|
@storage_credentials = {
|
62
63
|
'type' => 'token',
|
63
64
|
'token' => {TOKEN_FIELD => nil}
|
@@ -67,7 +68,8 @@ module Aspera
|
|
67
68
|
auth: {
|
68
69
|
type: :basic,
|
69
70
|
username: ats_info['AccessKey']['Id'],
|
70
|
-
password: ats_info['AccessKey']['Secret']
|
71
|
+
password: ats_info['AccessKey']['Secret']
|
72
|
+
},
|
71
73
|
add_tspec: {'tags'=>{Transfer::Spec::TAG_RESERVED=>{'node'=>{'storage_credentials'=>@storage_credentials}}}}
|
72
74
|
)
|
73
75
|
# update storage_credentials AND Rest params
|
data/lib/aspera/api/httpgw.rb
CHANGED
@@ -67,9 +67,7 @@ module Aspera
|
|
67
67
|
@shared_info[:read_exception].nil? &&
|
68
68
|
(((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
|
69
69
|
((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
|
70
|
-
if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
|
71
|
-
Log.log.trace1{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"}
|
72
|
-
end
|
70
|
+
Log.log.trace1{"#{LOG_WS_SEND}#{'timeout'.blue}: #{@shared_info[:count]}"} if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
|
73
71
|
end
|
74
72
|
end
|
75
73
|
end
|
@@ -110,7 +108,7 @@ module Aspera
|
|
110
108
|
Log.log.debug{"#{LOG_WS_RECV}read thread started"}
|
111
109
|
frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
|
112
110
|
until @ws_io.eof?
|
113
|
-
begin
|
111
|
+
begin
|
114
112
|
# ready byte by byte until frame is ready
|
115
113
|
# blocking read
|
116
114
|
byte = @ws_io.read(1)
|
@@ -138,16 +136,16 @@ module Aspera
|
|
138
136
|
def upload(transfer_spec)
|
139
137
|
# identify this session uniquely
|
140
138
|
session_id = SecureRandom.uuid
|
141
|
-
@notify_cb&.call(:
|
139
|
+
@notify_cb&.call(:sessions_init, info: 'starting')
|
142
140
|
# process files to send, modify `paths` in transfer_spec
|
143
141
|
files_to_send = process_upload_list(transfer_spec)
|
144
142
|
# total size of all files is last element
|
145
143
|
total_bytes_to_transfer = files_to_send.pop
|
146
|
-
Log.
|
147
|
-
Log.
|
144
|
+
Log.dump(:modified_tspec, transfer_spec, level: :trace1)
|
145
|
+
Log.dump(:files_to_send, files_to_send, level: :trace1)
|
148
146
|
# TODO: check that this is available in endpoints: @api_info['endpoints']
|
149
147
|
upload_url = File.join(@gw_root_url, @upload_version, 'upload')
|
150
|
-
@notify_cb&.call(:
|
148
|
+
@notify_cb&.call(:sessions_init, info: 'connecting wss')
|
151
149
|
# open web socket to end point (equivalent to Net::HTTP.start)
|
152
150
|
http_session = Rest.start_http_session(upload_url)
|
153
151
|
# get the underlying socket i/o
|
@@ -233,7 +231,8 @@ module Aspera
|
|
233
231
|
end
|
234
232
|
# throttling may have skipped last one
|
235
233
|
@notify_cb&.call(:transfer, session_id: session_id, info: session_sent_bytes)
|
236
|
-
@notify_cb&.call(:
|
234
|
+
@notify_cb&.call(:session_end, session_id: session_id)
|
235
|
+
@notify_cb&.call(:end)
|
237
236
|
ws_send(ws_type: :close, data: nil)
|
238
237
|
Log.log.debug("Finished upload, waiting for end of #{THR_RECV} thread.")
|
239
238
|
@ws_read_thread.join
|
@@ -254,9 +253,7 @@ module Aspera
|
|
254
253
|
# by default it is the name of first file
|
255
254
|
download_name = File.basename(default_file_name, '.*')
|
256
255
|
# add indication of number of files if there is more than one
|
257
|
-
if transfer_spec['paths'].length > 1
|
258
|
-
download_name += " #{transfer_spec['paths'].length} Files"
|
259
|
-
end
|
256
|
+
download_name += " #{transfer_spec['paths'].length} Files" if transfer_spec['paths'].length > 1
|
260
257
|
transfer_spec['download_name'] = download_name
|
261
258
|
end
|
262
259
|
# start transfer session on httpgw
|
@@ -265,7 +262,7 @@ module Aspera
|
|
265
262
|
file_name =
|
266
263
|
if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
|
267
264
|
# it is a zip file if zip is required or there is more than 1 file
|
268
|
-
transfer_spec['download_name']
|
265
|
+
"#{transfer_spec['download_name']}.zip"
|
269
266
|
else
|
270
267
|
# it is a plain file if we don't require zip and there is only one file
|
271
268
|
File.basename(default_file_name)
|
@@ -292,13 +289,13 @@ module Aspera
|
|
292
289
|
notify_cb: nil,
|
293
290
|
**opts
|
294
291
|
)
|
295
|
-
Log.
|
292
|
+
Log.dump(:gw_url, url)
|
296
293
|
# add scheme if missing
|
297
294
|
url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
|
298
|
-
raise 'GW URL shall be with scheme https' unless url.start_with?('https://')
|
295
|
+
raise Error, 'GW URL shall be with scheme https' unless url.start_with?('https://')
|
299
296
|
# remove trailing slash and version (o=only once) if present
|
300
297
|
# TODO: issue warning ?
|
301
|
-
url = url.
|
298
|
+
url = url.chomp('/').gsub(%r{/#{API_V1}$}o, '')
|
302
299
|
# assume GW is always under specific path (TODO: remove this ?)
|
303
300
|
url = File.join(url, DEFAULT_BASE_PATH) unless url.end_with?(DEFAULT_BASE_PATH)
|
304
301
|
@gw_root_url = url
|
@@ -309,7 +306,7 @@ module Aspera
|
|
309
306
|
@notify_cb = notify_cb
|
310
307
|
# get API info
|
311
308
|
@api_info = read('info').freeze
|
312
|
-
Log.
|
309
|
+
Log.dump(:api_info, @api_info)
|
313
310
|
# web socket endpoint: by default use v2 (newer gateways), without base64 encoding
|
314
311
|
# is the latest supported? else revert to old api
|
315
312
|
if !@upload_version.eql?(API_V1)
|
@@ -332,7 +329,7 @@ module Aspera
|
|
332
329
|
# @return [Array] info on files to send
|
333
330
|
def process_upload_list(transfer_spec)
|
334
331
|
total_bytes_to_transfer = 0
|
335
|
-
source_prefix = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? transfer_spec['source_root']
|
332
|
+
source_prefix = transfer_spec.key?('source_root') && !transfer_spec['source_root'].empty? ? "#{transfer_spec['source_root']}/" : ''
|
336
333
|
files_to_send = []
|
337
334
|
transfer_spec['paths'].each do |one_path|
|
338
335
|
source_path = source_prefix + one_path['source']
|
data/lib/aspera/api/node.rb
CHANGED
@@ -9,6 +9,9 @@ require 'aspera/assert'
|
|
9
9
|
require 'aspera/environment'
|
10
10
|
require 'zlib'
|
11
11
|
require 'base64'
|
12
|
+
require 'openssl'
|
13
|
+
require 'pathname'
|
14
|
+
require 'net/ssh/buffer'
|
12
15
|
|
13
16
|
module Aspera
|
14
17
|
module Api
|
@@ -35,6 +38,7 @@ module Aspera
|
|
35
38
|
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
36
39
|
SCOPE_USER = 'user:all'
|
37
40
|
SCOPE_ADMIN = 'admin:all'
|
41
|
+
# / in cloud
|
38
42
|
PATH_SEPARATOR = '/'
|
39
43
|
|
40
44
|
# register node special token decoder
|
@@ -49,6 +53,35 @@ module Aspera
|
|
49
53
|
attr_accessor :use_standard_ports
|
50
54
|
# set to false to bypass cache in redis
|
51
55
|
attr_accessor :use_node_cache
|
56
|
+
attr_reader :use_dynamic_key
|
57
|
+
|
58
|
+
# set private key to be used
|
59
|
+
# @param pem_content [String] PEM encoded private key
|
60
|
+
def use_dynamic_key=(pem_content)
|
61
|
+
Aspera.assert_type(pem_content, String)
|
62
|
+
@dynamic_key = OpenSSL::PKey.read(pem_content)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Adds fields `public_keys` in provided Hash, if dynamic key is set.
|
66
|
+
# @param h [Hash] Hash to add public key to
|
67
|
+
def add_public_key(h)
|
68
|
+
if @dynamic_key
|
69
|
+
ssh_key = Net::SSH::Buffer.from(:key, @dynamic_key)
|
70
|
+
# get pub key in OpenSSH public key format (authorized_keys)
|
71
|
+
h['public_keys'] = [
|
72
|
+
ssh_key.read_string,
|
73
|
+
Base64.strict_encode64(ssh_key.to_s)
|
74
|
+
].join(' ')
|
75
|
+
end
|
76
|
+
return h
|
77
|
+
end
|
78
|
+
|
79
|
+
# Adds fields `ssh_private_key` in provided Hash, if dynamic key is set.
|
80
|
+
# @param h [Hash] Hash to add private key to
|
81
|
+
def add_private_key(h)
|
82
|
+
h['ssh_private_key'] = @dynamic_key.to_pem if @dynamic_key
|
83
|
+
return h
|
84
|
+
end
|
52
85
|
|
53
86
|
# For access keys: provide expression to match entry in folder
|
54
87
|
# @param match_expression one of supported types
|
@@ -60,7 +93,7 @@ module Aspera
|
|
60
93
|
when String
|
61
94
|
return ->(f){File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
|
62
95
|
when NilClass then return ->(_){true}
|
63
|
-
else Aspera.error_unexpected_value(match_expression.class.name,
|
96
|
+
else Aspera.error_unexpected_value(match_expression.class.name, type: Cli::BadArgument)
|
64
97
|
end
|
65
98
|
end
|
66
99
|
|
@@ -68,6 +101,13 @@ module Aspera
|
|
68
101
|
return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
|
69
102
|
end
|
70
103
|
|
104
|
+
# @return [Array] containing folder + inside folder/file
|
105
|
+
def split_folder(path)
|
106
|
+
folder = path.split(PATH_SEPARATOR)
|
107
|
+
inside = folder.pop
|
108
|
+
[folder.join(PATH_SEPARATOR), inside]
|
109
|
+
end
|
110
|
+
|
71
111
|
# node API scopes
|
72
112
|
def token_scope(access_key, scope)
|
73
113
|
return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
|
@@ -115,7 +155,7 @@ module Aspera
|
|
115
155
|
def bearer_headers(bearer_auth, access_key: nil)
|
116
156
|
# if username is not provided, use the access key from the token
|
117
157
|
if access_key.nil?
|
118
|
-
access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.
|
158
|
+
access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
|
119
159
|
Aspera.assert(!access_key.nil?)
|
120
160
|
end
|
121
161
|
return {
|
@@ -135,6 +175,7 @@ module Aspera
|
|
135
175
|
def initialize(app_info: nil, add_tspec: nil, **rest_args)
|
136
176
|
# init Rest
|
137
177
|
super(**rest_args)
|
178
|
+
@dynamic_key = nil
|
138
179
|
@app_info = app_info
|
139
180
|
# this is added to transfer spec, for instance to add tags (COS)
|
140
181
|
@add_tspec = add_tspec
|
@@ -150,14 +191,15 @@ module Aspera
|
|
150
191
|
end
|
151
192
|
|
152
193
|
# Call node API, possibly adding cache control header, as globally specified
|
153
|
-
def read_with_cache(subpath, query=nil)
|
194
|
+
def read_with_cache(subpath, query = nil)
|
154
195
|
headers = {'Accept' => Rest::MIME_JSON}
|
155
196
|
headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless self.class.use_node_cache
|
156
197
|
return call(
|
157
198
|
operation: 'GET',
|
158
199
|
subpath: subpath,
|
159
200
|
headers: headers,
|
160
|
-
query: query
|
201
|
+
query: query
|
202
|
+
)[:data]
|
161
203
|
end
|
162
204
|
|
163
205
|
# update transfer spec with special additional tags
|
@@ -173,10 +215,11 @@ module Aspera
|
|
173
215
|
return @app_info[:api].node_api_from(
|
174
216
|
node_id: node_id,
|
175
217
|
workspace_id: @app_info[:workspace_id],
|
176
|
-
workspace_name: @app_info[:workspace_name]
|
218
|
+
workspace_name: @app_info[:workspace_name]
|
219
|
+
)
|
177
220
|
end
|
178
221
|
Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
|
179
|
-
return
|
222
|
+
return
|
180
223
|
end
|
181
224
|
|
182
225
|
# Check if a link entry in folder has target information
|
@@ -201,12 +244,12 @@ module Aspera
|
|
201
244
|
# @param state [Object] state object sent to processing method
|
202
245
|
# @param top_file_id [String] file id to start at (default = access key root file id)
|
203
246
|
# @param top_file_path [String] path of top folder (default = /)
|
204
|
-
def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/'
|
247
|
+
def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
|
205
248
|
Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
|
206
249
|
Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
|
207
250
|
# start at top folder
|
208
251
|
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
209
|
-
Log.
|
252
|
+
Log.dump(:folders_to_explore, folders_to_explore)
|
210
253
|
until folders_to_explore.empty?
|
211
254
|
# consume first in job list
|
212
255
|
current_item = folders_to_explore.shift
|
@@ -220,12 +263,10 @@ module Aspera
|
|
220
263
|
Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
|
221
264
|
[]
|
222
265
|
end
|
223
|
-
Log.
|
266
|
+
Log.dump(:folder_contents, folder_contents)
|
224
267
|
folder_contents.each do |entry|
|
225
268
|
if entry.key?('error')
|
226
|
-
if entry['error'].is_a?(Hash) && entry['error'].key?('user_message')
|
227
|
-
Log.log.error(entry['error']['user_message'])
|
228
|
-
end
|
269
|
+
Log.log.error(entry['error']['user_message']) if entry['error'].is_a?(Hash) && entry['error'].key?('user_message')
|
229
270
|
next
|
230
271
|
end
|
231
272
|
current_path = File.join(current_item[:path], entry['name'])
|
@@ -242,7 +283,8 @@ module Aspera
|
|
242
283
|
method_sym: method_sym,
|
243
284
|
state: state,
|
244
285
|
top_file_id: entry['target_id'],
|
245
|
-
top_file_path: current_path
|
286
|
+
top_file_path: current_path
|
287
|
+
)
|
246
288
|
end
|
247
289
|
end
|
248
290
|
end
|
@@ -255,7 +297,7 @@ module Aspera
|
|
255
297
|
# @param path [String] file or folder path (end with "/" is like setting process_last_link)
|
256
298
|
# @param process_last_link [Boolean] if true, follow the last link
|
257
299
|
# @return [Hash] {.api,.file_id}
|
258
|
-
def resolve_api_fid(top_file_id, path, process_last_link=false)
|
300
|
+
def resolve_api_fid(top_file_id, path, process_last_link = false)
|
259
301
|
Aspera.assert_type(top_file_id, String)
|
260
302
|
Aspera.assert_type(path, String)
|
261
303
|
process_last_link ||= path.end_with?(PATH_SEPARATOR)
|
@@ -268,6 +310,50 @@ module Aspera
|
|
268
310
|
return resolve_state[:result]
|
269
311
|
end
|
270
312
|
|
313
|
+
# Given a list of paths, finds a common root and list of sub-paths
|
314
|
+
# @param top_file_id [String] Root file id
|
315
|
+
# @param paths [Array(Hash)] List of paths
|
316
|
+
# @return [Array] size=2: apfid, paths (Array(Hash))
|
317
|
+
def resolve_api_fid_paths(top_file_id, paths)
|
318
|
+
Aspera.assert_type(paths, Array)
|
319
|
+
Aspera.assert(paths.size.positive?)
|
320
|
+
split_sources = paths.map{ |p| Pathname(p['source']).each_filename.to_a}
|
321
|
+
root = []
|
322
|
+
split_sources.map(&:size).min.times do |i|
|
323
|
+
parts = split_sources.map{ |s| s[i]}
|
324
|
+
break unless parts.uniq.size == 1
|
325
|
+
root << parts.first
|
326
|
+
end
|
327
|
+
source_folder = File.join(root)
|
328
|
+
source_paths = paths.each_with_index.map do |p, i|
|
329
|
+
m = {'source' => File.join(split_sources[i][root.size..])}
|
330
|
+
m['destination'] = p['destination'] if p.key?('destination')
|
331
|
+
m
|
332
|
+
end
|
333
|
+
apifid = resolve_api_fid(top_file_id, source_folder, true)
|
334
|
+
# If a single item
|
335
|
+
if source_paths.size.eql?(1)
|
336
|
+
# Get precise info in this element
|
337
|
+
file_info = apifid[:api].read("files/#{apifid[:file_id]}")
|
338
|
+
source_paths =
|
339
|
+
case file_info['type']
|
340
|
+
when 'file'
|
341
|
+
# if the single source is a file, we need to split into folder path and filename
|
342
|
+
src_dir_elements = source_folder.split(Api::Node::PATH_SEPARATOR)
|
343
|
+
filename = src_dir_elements.pop
|
344
|
+
apifid = resolve_api_fid(top_file_id, src_dir_elements.join(Api::Node::PATH_SEPARATOR), true)
|
345
|
+
# filename is the last one, source folder is what remains
|
346
|
+
[{'source' => filename}]
|
347
|
+
when 'link', 'folder'
|
348
|
+
# single source is 'folder' or 'link'
|
349
|
+
# TODO: add this ? , 'destination'=>file_info['name']
|
350
|
+
[{'source' => '.'}]
|
351
|
+
else Aspera.error_unexpected_value(file_info['type']){'source type'}
|
352
|
+
end
|
353
|
+
end
|
354
|
+
[apifid, source_paths]
|
355
|
+
end
|
356
|
+
|
271
357
|
def find_files(top_file_id, test_lambda)
|
272
358
|
Log.log.debug{"find_files: file id=#{top_file_id}"}
|
273
359
|
find_state = {found: [], test_lambda: test_lambda}
|
@@ -305,7 +391,7 @@ module Aspera
|
|
305
391
|
# @param file_id destination or source folder (id)
|
306
392
|
# @param direction one of Transfer::Spec::DIRECTION_SEND, Transfer::Spec::DIRECTION_RECEIVE
|
307
393
|
# @param ts_merge additional transfer spec to merge
|
308
|
-
def transfer_spec_gen4(file_id, direction, ts_merge=nil)
|
394
|
+
def transfer_spec_gen4(file_id, direction, ts_merge = nil)
|
309
395
|
ak_name = nil
|
310
396
|
ak_token = nil
|
311
397
|
case auth_params[:type]
|
@@ -347,9 +433,7 @@ module Aspera
|
|
347
433
|
# by default: same address as node API
|
348
434
|
transfer_spec['remote_host'] = URI.parse(base_url).host
|
349
435
|
# AoC allows specification of other url
|
350
|
-
if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
|
351
|
-
transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
|
352
|
-
end
|
436
|
+
transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url'] if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
|
353
437
|
info = read('info')
|
354
438
|
# get the transfer user from info on access key
|
355
439
|
transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
|
@@ -395,10 +479,8 @@ module Aspera
|
|
395
479
|
if state[:process_last_link]
|
396
480
|
# we found it
|
397
481
|
other_node = nil
|
398
|
-
if entry_has_link_information(entry)
|
399
|
-
|
400
|
-
end
|
401
|
-
raise 'Cannot resolve link' if other_node.nil?
|
482
|
+
other_node = node_id_to_node(entry['target_node_id']) if entry_has_link_information(entry)
|
483
|
+
raise Error, 'Cannot resolve link' if other_node.nil?
|
402
484
|
state[:result] = {api: other_node, file_id: entry['target_id']}
|
403
485
|
else
|
404
486
|
# we found it but we do not process the link
|