aspera-cli 4.16.0 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +50 -19
- data/CONTRIBUTING.md +3 -1
- data/README.md +965 -793
- data/bin/asession +29 -21
- data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
- data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
- data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
- data/lib/aspera/api/aoc.rb +586 -0
- data/lib/aspera/api/ats.rb +46 -0
- data/lib/aspera/api/cos_node.rb +95 -0
- data/lib/aspera/api/node.rb +344 -0
- data/lib/aspera/ascmd.rb +46 -10
- data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
- data/lib/aspera/{fasp → ascp}/management.rb +3 -8
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +30 -30
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +1 -1
- data/lib/aspera/cli/formatter.rb +13 -13
- data/lib/aspera/cli/hints.rb +5 -5
- data/lib/aspera/cli/main.rb +35 -28
- data/lib/aspera/cli/manager.rb +25 -24
- data/lib/aspera/cli/plugin.rb +22 -15
- data/lib/aspera/cli/plugin_factory.rb +61 -0
- data/lib/aspera/cli/plugins/alee.rb +7 -7
- data/lib/aspera/cli/plugins/aoc.rb +83 -77
- data/lib/aspera/cli/plugins/ats.rb +32 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +169 -186
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +19 -18
- data/lib/aspera/cli/plugins/faspex.rb +61 -54
- data/lib/aspera/cli/plugins/faspex5.rb +150 -103
- data/lib/aspera/cli/plugins/node.rb +68 -73
- data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
- data/lib/aspera/cli/plugins/preview.rb +31 -31
- data/lib/aspera/cli/plugins/server.rb +31 -33
- data/lib/aspera/cli/plugins/shares.rb +13 -11
- data/lib/aspera/cli/sync_actions.rb +8 -8
- data/lib/aspera/cli/transfer_agent.rb +32 -19
- data/lib/aspera/cli/transfer_progress.rb +1 -1
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +14 -14
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +2 -3
- data/lib/aspera/faspex_gw.rb +5 -6
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/id_generator.rb +2 -2
- data/lib/aspera/json_rpc.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +6 -6
- data/lib/aspera/keychain/macos_security.rb +27 -22
- data/lib/aspera/log.rb +2 -2
- data/lib/aspera/nagios.rb +3 -3
- data/lib/aspera/node_simulator.rb +5 -6
- data/lib/aspera/oauth/base.rb +143 -0
- data/lib/aspera/oauth/factory.rb +124 -0
- data/lib/aspera/oauth/generic.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +51 -0
- data/lib/aspera/oauth/url_json.rb +31 -0
- data/lib/aspera/oauth/web.rb +50 -0
- data/lib/aspera/oauth.rb +5 -331
- data/lib/aspera/open_application.rb +7 -7
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/generator.rb +5 -5
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/preview/utils.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +4 -4
- data/lib/aspera/rest.rb +175 -144
- data/lib/aspera/rest_errors_aspera.rb +3 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/ssh.rb +6 -1
- data/lib/aspera/{fasp → transfer}/error.rb +3 -3
- data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
- data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
- data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
- data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
- data/lib/aspera/web_server_simple.rb +11 -3
- data.tar.gz.sig +0 -0
- metadata +36 -63
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -601
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -94
- data/lib/aspera/fasp/resume_policy.rb +0 -79
- data/lib/aspera/node.rb +0 -339
data/lib/aspera/cos_node.rb
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'aspera/log'
|
|
4
|
-
require 'aspera/assert'
|
|
5
|
-
require 'aspera/rest'
|
|
6
|
-
require 'aspera/oauth'
|
|
7
|
-
require 'xmlsimple'
|
|
8
|
-
|
|
9
|
-
module Aspera
|
|
10
|
-
class CosNode < Aspera::Node
|
|
11
|
-
class << self
|
|
12
|
-
def parameters_from_svc_credentials(service_credentials, bucket_region)
|
|
13
|
-
# check necessary contents
|
|
14
|
-
assert_type(service_credentials, Hash){'service_credentials'}
|
|
15
|
-
%w[apikey resource_instance_id endpoints].each do |field|
|
|
16
|
-
assert(service_credentials.key?(field)){"service_credentials must have a field: #{field}"}
|
|
17
|
-
end
|
|
18
|
-
Aspera::Log.dump('service_credentials', service_credentials)
|
|
19
|
-
# read endpoints from service provided in service credentials
|
|
20
|
-
endpoints = Aspera::Rest.new({base_url: service_credentials['endpoints']}).read('')[:data]
|
|
21
|
-
Aspera::Log.dump('endpoints', endpoints)
|
|
22
|
-
storage_endpoint = endpoints.dig('service-endpoints', 'regional', bucket_region, 'public', bucket_region)
|
|
23
|
-
raise "no such region: #{bucket_region}" if storage_endpoint.nil?
|
|
24
|
-
return {
|
|
25
|
-
instance_id: service_credentials['resource_instance_id'],
|
|
26
|
-
service_api_key: service_credentials['apikey'],
|
|
27
|
-
storage_endpoint: "https://#{storage_endpoint}"
|
|
28
|
-
}
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
IBM_CLOUD_TOKEN_URL = 'https://iam.cloud.ibm.com/identity'
|
|
32
|
-
TOKEN_FIELD = 'delegated_refresh_token'
|
|
33
|
-
|
|
34
|
-
def initialize(bucket_name, storage_endpoint, instance_id, api_key, auth_url= IBM_CLOUD_TOKEN_URL)
|
|
35
|
-
@auth_url = auth_url
|
|
36
|
-
@api_key = api_key
|
|
37
|
-
s3_api = Aspera::Rest.new({
|
|
38
|
-
base_url: storage_endpoint,
|
|
39
|
-
not_auth_codes: %w[401 403], # error codes when not authorized
|
|
40
|
-
headers: {'ibm-service-instance-id' => instance_id},
|
|
41
|
-
auth: {
|
|
42
|
-
type: :oauth2,
|
|
43
|
-
base_url: @auth_url,
|
|
44
|
-
grant_method: :generic,
|
|
45
|
-
generic: {
|
|
46
|
-
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
|
47
|
-
response_type: 'cloud_iam',
|
|
48
|
-
apikey: @api_key
|
|
49
|
-
}}})
|
|
50
|
-
# read FASP connection information for bucket
|
|
51
|
-
xml_result_text = s3_api.call(
|
|
52
|
-
operation: 'GET',
|
|
53
|
-
subpath: bucket_name,
|
|
54
|
-
headers: {'Accept' => 'application/xml'},
|
|
55
|
-
url_params: {'faspConnectionInfo' => nil}
|
|
56
|
-
)[:http].body
|
|
57
|
-
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
|
58
|
-
Aspera::Log.dump('ats_info', ats_info)
|
|
59
|
-
@storage_credentials = {
|
|
60
|
-
'type' => 'token',
|
|
61
|
-
'token' => {TOKEN_FIELD => nil}
|
|
62
|
-
}
|
|
63
|
-
super(
|
|
64
|
-
params: {
|
|
65
|
-
base_url: ats_info['ATSEndpoint'],
|
|
66
|
-
auth: {
|
|
67
|
-
type: :basic,
|
|
68
|
-
username: ats_info['AccessKey']['Id'],
|
|
69
|
-
password: ats_info['AccessKey']['Secret']}},
|
|
70
|
-
add_tspec: {'tags'=>{Fasp::TransferSpec::TAG_RESERVED=>{'node'=>{'storage_credentials'=>@storage_credentials}}}})
|
|
71
|
-
# update storage_credentials AND Rest params
|
|
72
|
-
generate_token
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# potentially call this if delegated token is expired
|
|
76
|
-
def generate_token
|
|
77
|
-
# OAuth API to get delegated token
|
|
78
|
-
delegated_oauth = Oauth.new({
|
|
79
|
-
type: :oauth2,
|
|
80
|
-
base_url: @auth_url,
|
|
81
|
-
token_field: TOKEN_FIELD,
|
|
82
|
-
grant_method: :generic,
|
|
83
|
-
generic: {
|
|
84
|
-
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
|
85
|
-
response_type: 'delegated_refresh_token',
|
|
86
|
-
apikey: @api_key,
|
|
87
|
-
receiver_client_ids: 'aspera_ats'
|
|
88
|
-
}})
|
|
89
|
-
# get delegated token to be placed in rest call header and in transfer tags
|
|
90
|
-
@storage_credentials['token'][TOKEN_FIELD] = Oauth.bearer_extract(delegated_oauth.get_authorization)
|
|
91
|
-
@params[:headers] = {'X-Aspera-Storage-Credentials' => JSON.generate(@storage_credentials)}
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'singleton'
|
|
4
|
-
require 'aspera/log'
|
|
5
|
-
require 'aspera/assert'
|
|
6
|
-
|
|
7
|
-
module Aspera
|
|
8
|
-
module Fasp
|
|
9
|
-
# implements a simple resume policy
|
|
10
|
-
class ResumePolicy
|
|
11
|
-
# list of supported parameters and default values
|
|
12
|
-
DEFAULTS = {
|
|
13
|
-
iter_max: 7,
|
|
14
|
-
sleep_initial: 2,
|
|
15
|
-
sleep_factor: 2,
|
|
16
|
-
sleep_max: 60
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
# @param params see DEFAULTS
|
|
20
|
-
def initialize(params=nil)
|
|
21
|
-
@parameters = DEFAULTS.dup
|
|
22
|
-
if !params.nil?
|
|
23
|
-
assert_type(params, Hash)
|
|
24
|
-
params.each do |k, v|
|
|
25
|
-
assert_values(k, DEFAULTS.keys){'resume parameter'}
|
|
26
|
-
assert_type(v, Integer){k}
|
|
27
|
-
@parameters[k] = v
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
Log.log.debug{"resume params=#{@parameters}"}
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# calls block a number of times (resumes) until success or limit reached
|
|
34
|
-
# this is re-entrant, one resumer can handle multiple transfers in //
|
|
35
|
-
def execute_with_resume
|
|
36
|
-
assert(block_given?)
|
|
37
|
-
# maximum of retry
|
|
38
|
-
remaining_resumes = @parameters[:iter_max]
|
|
39
|
-
sleep_seconds = @parameters[:sleep_initial]
|
|
40
|
-
Log.log.debug{"retries=#{remaining_resumes}"}
|
|
41
|
-
# try to send the file until ascp is successful
|
|
42
|
-
loop do
|
|
43
|
-
Log.log.debug('transfer starting')
|
|
44
|
-
begin
|
|
45
|
-
# call provided block
|
|
46
|
-
yield
|
|
47
|
-
# exit retry loop if success
|
|
48
|
-
break
|
|
49
|
-
rescue Fasp::Error => e
|
|
50
|
-
Log.log.warn{"An error occurred during transfer: #{e.message}"}
|
|
51
|
-
# failure in ascp
|
|
52
|
-
if e.retryable?
|
|
53
|
-
# exit if we exceed the max number of retry
|
|
54
|
-
raise Fasp::Error, "Maximum number of retry reached (#{@parameters[:iter_max]})" if remaining_resumes <= 0
|
|
55
|
-
else
|
|
56
|
-
# give one chance only to non retryable errors
|
|
57
|
-
unless remaining_resumes.eql?(@parameters[:iter_max])
|
|
58
|
-
Log.log.error('non-retryable error'.red.blink)
|
|
59
|
-
raise e
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# take this retry in account
|
|
65
|
-
remaining_resumes -= 1
|
|
66
|
-
Log.log.warn{"Resuming in #{sleep_seconds} seconds (retry left:#{remaining_resumes})"}
|
|
67
|
-
|
|
68
|
-
# wait a bit before retrying, maybe network condition will be better
|
|
69
|
-
sleep(sleep_seconds)
|
|
70
|
-
|
|
71
|
-
# increase retry period
|
|
72
|
-
sleep_seconds *= @parameters[:sleep_factor]
|
|
73
|
-
# cap value
|
|
74
|
-
sleep_seconds = @parameters[:sleep_max] if sleep_seconds > @parameters[:sleep_max]
|
|
75
|
-
end # loop
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/aspera/node.rb
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'aspera/cli/error'
|
|
4
|
-
require 'aspera/fasp/transfer_spec'
|
|
5
|
-
require 'aspera/rest'
|
|
6
|
-
require 'aspera/oauth'
|
|
7
|
-
require 'aspera/log'
|
|
8
|
-
require 'aspera/assert'
|
|
9
|
-
require 'aspera/environment'
|
|
10
|
-
require 'zlib'
|
|
11
|
-
require 'base64'
|
|
12
|
-
|
|
13
|
-
module Aspera
|
|
14
|
-
# Provides additional functions using node API with gen4 extensions (access keys)
|
|
15
|
-
class Node < Aspera::Rest
|
|
16
|
-
# permissions
|
|
17
|
-
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
18
|
-
# prefix for ruby code for filter (deprecated)
|
|
19
|
-
MATCH_EXEC_PREFIX = 'exec:'
|
|
20
|
-
MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
|
|
21
|
-
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
22
|
-
PATH_SEPARATOR = '/'
|
|
23
|
-
TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
|
|
24
|
-
SCOPE_USER = 'user:all'
|
|
25
|
-
SCOPE_ADMIN = 'admin:all'
|
|
26
|
-
SCOPE_PREFIX = 'node.'
|
|
27
|
-
SCOPE_SEPARATOR = ':'
|
|
28
|
-
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
29
|
-
BEARER_TOKEN_VALIDITY_DEFAULT = 86400
|
|
30
|
-
BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
|
|
31
|
-
|
|
32
|
-
# register node special token decoder
|
|
33
|
-
Oauth.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
|
|
34
|
-
|
|
35
|
-
# class instance variable, access with accessors on class
|
|
36
|
-
@use_standard_ports = true
|
|
37
|
-
|
|
38
|
-
class << self
|
|
39
|
-
attr_accessor :use_standard_ports
|
|
40
|
-
|
|
41
|
-
# For access keys: provide expression to match entry in folder
|
|
42
|
-
def file_matcher(match_expression)
|
|
43
|
-
case match_expression
|
|
44
|
-
when Proc then return match_expression
|
|
45
|
-
when Regexp then return ->(f){f['name'].match?(match_expression)}
|
|
46
|
-
when String
|
|
47
|
-
if match_expression.start_with?(MATCH_EXEC_PREFIX)
|
|
48
|
-
code = "->(f){#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
|
|
49
|
-
Log.log.warn{"Use of prefix #{MATCH_EXEC_PREFIX} is deprecated (4.15), instead use: @ruby:'#{code}'"}
|
|
50
|
-
return Environment.secure_eval(code, __FILE__, __LINE__)
|
|
51
|
-
end
|
|
52
|
-
return lambda{|f|File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
|
|
53
|
-
when NilClass then return ->(_){true}
|
|
54
|
-
else error_unexpected_value(match_expression.class.name, exception_class: Cli::BadArgument)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def file_matcher_from_argument(options)
|
|
59
|
-
return file_matcher(options.get_next_argument('filter', type: MATCH_TYPES, mandatory: false))
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# node API scopes
|
|
63
|
-
def token_scope(access_key, scope)
|
|
64
|
-
return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def decode_scope(scope)
|
|
68
|
-
items = scope.split(SCOPE_SEPARATOR, 2)
|
|
69
|
-
assert(items.length.eql?(2)){"invalid scope: #{scope}"}
|
|
70
|
-
assert(items[0].start_with?(SCOPE_PREFIX)){"invalid scope: #{scope}"}
|
|
71
|
-
return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Create an Aspera Node bearer token
|
|
75
|
-
# @param payload [String] JSON payload to be included in the token
|
|
76
|
-
# @param private_key [OpenSSL::PKey::RSA] Private key to sign the token
|
|
77
|
-
def bearer_token(access_key:, payload:, private_key:)
|
|
78
|
-
assert_type(payload, Hash)
|
|
79
|
-
assert(payload.key?('user_id'))
|
|
80
|
-
assert_type(payload['user_id'], String)
|
|
81
|
-
assert(!payload['user_id'].empty?)
|
|
82
|
-
assert_type(private_key, OpenSSL::PKey::RSA)
|
|
83
|
-
# manage convenience parameters
|
|
84
|
-
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
85
|
-
payload.delete('_validity')
|
|
86
|
-
scope = payload['_scope'] || BEARER_TOKEN_SCOPE_DEFAULT
|
|
87
|
-
payload.delete('_scope')
|
|
88
|
-
payload['scope'] ||= token_scope(access_key, scope)
|
|
89
|
-
payload['auth_type'] ||= 'access_key'
|
|
90
|
-
payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
|
|
91
|
-
payload_json = JSON.generate(payload)
|
|
92
|
-
return Base64.strict_encode64(Zlib::Deflate.deflate([
|
|
93
|
-
payload_json,
|
|
94
|
-
SIGNATURE_DELIMITER,
|
|
95
|
-
Base64.strict_encode64(private_key.sign(OpenSSL::Digest.new('sha512'), payload_json)).scan(/.{1,60}/).join("\n"),
|
|
96
|
-
''
|
|
97
|
-
].join("\n")))
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def decode_bearer_token(token)
|
|
101
|
-
return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition(SIGNATURE_DELIMITER).first)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def bearer_headers(bearer_auth, access_key: nil)
|
|
105
|
-
# if username is not provided, use the access key from the token
|
|
106
|
-
if access_key.nil?
|
|
107
|
-
access_key = Aspera::Node.decode_scope(Aspera::Node.decode_bearer_token(Oauth.bearer_extract(bearer_auth))['scope'])[:access_key]
|
|
108
|
-
assert(!access_key.nil?)
|
|
109
|
-
end
|
|
110
|
-
return {
|
|
111
|
-
Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => access_key,
|
|
112
|
-
'Authorization' => bearer_auth
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# fields in @app_info
|
|
118
|
-
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
119
|
-
# methods of @app_info[:api]
|
|
120
|
-
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
121
|
-
private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
122
|
-
|
|
123
|
-
attr_reader :app_info
|
|
124
|
-
|
|
125
|
-
# @param params [Hash] Rest parameters
|
|
126
|
-
# @param app_info [Hash,NilClass] special processing for AoC
|
|
127
|
-
def initialize(params:, app_info: nil, add_tspec: nil)
|
|
128
|
-
# init Rest
|
|
129
|
-
super(params)
|
|
130
|
-
@app_info = app_info
|
|
131
|
-
# this is added to transfer spec, for instance to add tags (COS)
|
|
132
|
-
@add_tspec = add_tspec
|
|
133
|
-
if !@app_info.nil?
|
|
134
|
-
REQUIRED_APP_INFO_FIELDS.each do |field|
|
|
135
|
-
assert(@app_info.key?(field)){"app_info lacks field #{field}"}
|
|
136
|
-
end
|
|
137
|
-
REQUIRED_APP_API_METHODS.each do |method|
|
|
138
|
-
assert(@app_info[:api].respond_to?(method)){"#{@app_info[:api].class} lacks method #{method}"}
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# update transfer spec with special additional tags
|
|
144
|
-
def add_tspec_info(tspec)
|
|
145
|
-
tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
|
|
146
|
-
return tspec
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# @returns [Aspera::Node] a Node or nil
|
|
150
|
-
def node_id_to_node(node_id)
|
|
151
|
-
if !@app_info.nil?
|
|
152
|
-
return self if node_id.eql?(@app_info[:node_info]['id'])
|
|
153
|
-
return @app_info[:api].node_api_from(
|
|
154
|
-
node_id: node_id,
|
|
155
|
-
workspace_id: @app_info[:workspace_id],
|
|
156
|
-
workspace_name: @app_info[:workspace_name])
|
|
157
|
-
end
|
|
158
|
-
Log.log.warn{"cannot resolve link with node id #{node_id}"}
|
|
159
|
-
return nil
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Recursively browse in a folder (with non-recursive method)
|
|
163
|
-
# sub folders are processed if the processing method returns true
|
|
164
|
-
# @param state [Object] state object sent to processing method
|
|
165
|
-
# @param top_file_id [String] file id to start at (default = access key root file id)
|
|
166
|
-
# @param top_file_path [String] path of top folder (default = /)
|
|
167
|
-
# @param block [Proc] processing method, arguments: entry, path, state
|
|
168
|
-
def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
|
|
169
|
-
assert(!top_file_path.nil?){'top_file_path not set'}
|
|
170
|
-
assert(block){'Missing block'}
|
|
171
|
-
# start at top folder
|
|
172
|
-
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
|
173
|
-
Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
|
|
174
|
-
until folders_to_explore.empty?
|
|
175
|
-
current_item = folders_to_explore.shift
|
|
176
|
-
Log.log.debug{"searching #{current_item[:path]}".bg_green}
|
|
177
|
-
# get folder content
|
|
178
|
-
folder_contents =
|
|
179
|
-
begin
|
|
180
|
-
read("files/#{current_item[:id]}/files")[:data]
|
|
181
|
-
rescue StandardError => e
|
|
182
|
-
Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
|
|
183
|
-
[]
|
|
184
|
-
end
|
|
185
|
-
Log.log.debug{Log.dump(:folder_contents, folder_contents)}
|
|
186
|
-
folder_contents.each do |entry|
|
|
187
|
-
relative_path = File.join(current_item[:path], entry['name'])
|
|
188
|
-
Log.log.debug{"process_folder_tree checking #{relative_path}"}
|
|
189
|
-
# continue only if method returns true
|
|
190
|
-
next unless yield(entry, relative_path, state)
|
|
191
|
-
# entry type is file, folder or link
|
|
192
|
-
case entry['type']
|
|
193
|
-
when 'folder'
|
|
194
|
-
folders_to_explore.push({id: entry['id'], path: relative_path})
|
|
195
|
-
when 'link'
|
|
196
|
-
node_id_to_node(entry['target_node_id'])&.process_folder_tree(
|
|
197
|
-
state: state,
|
|
198
|
-
top_file_id: entry['target_id'],
|
|
199
|
-
top_file_path: relative_path,
|
|
200
|
-
&block)
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end # process_folder_tree
|
|
205
|
-
|
|
206
|
-
# Navigate the path from given file id
|
|
207
|
-
# @param top_file_id [String] id initial file id
|
|
208
|
-
# @param path [String] file path
|
|
209
|
-
# @return [Hash] {.api,.file_id}
|
|
210
|
-
def resolve_api_fid(top_file_id, path)
|
|
211
|
-
assert_type(top_file_id, String)
|
|
212
|
-
process_last_link = path.end_with?(PATH_SEPARATOR)
|
|
213
|
-
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
|
214
|
-
return {api: self, file_id: top_file_id} if path_elements.empty?
|
|
215
|
-
resolve_state = {path: path_elements, result: nil}
|
|
216
|
-
process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
|
|
217
|
-
# this block is called recursively for each entry in folder
|
|
218
|
-
# stop digging here if not in right path
|
|
219
|
-
next false unless entry['name'].eql?(state[:path].first)
|
|
220
|
-
# ok it matches, so we remove the match
|
|
221
|
-
state[:path].shift
|
|
222
|
-
case entry['type']
|
|
223
|
-
when 'file'
|
|
224
|
-
# file must be terminal
|
|
225
|
-
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
|
|
226
|
-
# it's terminal, we found it
|
|
227
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
228
|
-
next false
|
|
229
|
-
when 'folder'
|
|
230
|
-
if state[:path].empty?
|
|
231
|
-
# we found it
|
|
232
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
233
|
-
next false
|
|
234
|
-
end
|
|
235
|
-
when 'link'
|
|
236
|
-
if state[:path].empty?
|
|
237
|
-
if process_last_link
|
|
238
|
-
# we found it
|
|
239
|
-
other_node = node_id_to_node(entry['target_node_id'])
|
|
240
|
-
raise 'cannot resolve link' if other_node.nil?
|
|
241
|
-
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
242
|
-
else
|
|
243
|
-
# we found it but we do not process the link
|
|
244
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
245
|
-
end
|
|
246
|
-
next false
|
|
247
|
-
end
|
|
248
|
-
else
|
|
249
|
-
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
250
|
-
end
|
|
251
|
-
# continue to dig folder
|
|
252
|
-
next true
|
|
253
|
-
end
|
|
254
|
-
raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
|
|
255
|
-
return resolve_state[:result]
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def find_files(top_file_id, test_block)
|
|
259
|
-
Log.log.debug{"find_files: file id=#{top_file_id}"}
|
|
260
|
-
find_state = {found: [], test_block: test_block}
|
|
261
|
-
process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
|
|
262
|
-
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
263
|
-
# test all files deeply
|
|
264
|
-
true
|
|
265
|
-
end
|
|
266
|
-
return find_state[:found]
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def refreshed_transfer_token
|
|
270
|
-
return oauth_token(force_refresh: true)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Create transfer spec for gen4
|
|
274
|
-
def transfer_spec_gen4(file_id, direction, ts_merge=nil)
|
|
275
|
-
ak_name = nil
|
|
276
|
-
ak_token = nil
|
|
277
|
-
case params[:auth][:type]
|
|
278
|
-
when :basic
|
|
279
|
-
ak_name = params[:auth][:username]
|
|
280
|
-
assert(params[:auth][:password]){'no secret in node object'}
|
|
281
|
-
ak_token = Rest.basic_token(params[:auth][:username], params[:auth][:password])
|
|
282
|
-
when :oauth2
|
|
283
|
-
ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
|
|
284
|
-
# TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
|
|
285
|
-
# get bearer token, possibly use cache
|
|
286
|
-
ak_token = oauth_token(force_refresh: false)
|
|
287
|
-
else error_unexpected_value(params[:auth][:type])
|
|
288
|
-
end
|
|
289
|
-
transfer_spec = {
|
|
290
|
-
'direction' => direction,
|
|
291
|
-
'token' => ak_token,
|
|
292
|
-
'tags' => {
|
|
293
|
-
Fasp::TransferSpec::TAG_RESERVED => {
|
|
294
|
-
'node' => {
|
|
295
|
-
'access_key' => ak_name,
|
|
296
|
-
'file_id' => file_id
|
|
297
|
-
} # node
|
|
298
|
-
} # aspera
|
|
299
|
-
} # tags
|
|
300
|
-
}
|
|
301
|
-
# add specials tags (cos)
|
|
302
|
-
add_tspec_info(transfer_spec)
|
|
303
|
-
transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
|
|
304
|
-
# add application specific tags (AoC)
|
|
305
|
-
app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
|
|
306
|
-
# add remote host info
|
|
307
|
-
if self.class.use_standard_ports
|
|
308
|
-
# get default TCP/UDP ports and transfer user
|
|
309
|
-
transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
|
|
310
|
-
# by default: same address as node API
|
|
311
|
-
transfer_spec['remote_host'] = URI.parse(params[:base_url]).host
|
|
312
|
-
# AoC allows specification of other url
|
|
313
|
-
if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
|
|
314
|
-
transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
|
|
315
|
-
end
|
|
316
|
-
info = read('info')[:data]
|
|
317
|
-
# get the transfer user from info on access key
|
|
318
|
-
transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
|
|
319
|
-
# get settings from name.value array to hash key.value
|
|
320
|
-
settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
|
|
321
|
-
# check WSS ports
|
|
322
|
-
%w[wss_enabled wss_port].each do |i|
|
|
323
|
-
transfer_spec[i] = settings[i] if settings.key?(i)
|
|
324
|
-
end if settings.is_a?(Hash)
|
|
325
|
-
else
|
|
326
|
-
# retrieve values from API (and keep a copy/cache)
|
|
327
|
-
@std_t_spec_cache ||= create(
|
|
328
|
-
'files/download_setup',
|
|
329
|
-
{transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
|
|
330
|
-
)[:data]['transfer_specs'].first['transfer_spec']
|
|
331
|
-
# copy some parts
|
|
332
|
-
TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
|
|
333
|
-
end
|
|
334
|
-
Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
|
|
335
|
-
unless transfer_spec['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
|
|
336
|
-
return transfer_spec
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
end
|