aspera-cli 4.15.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/BUGS.md +29 -3
- data/CHANGELOG.md +375 -280
- data/CONTRIBUTING.md +71 -18
- data/README.md +1978 -1656
- data/bin/ascli +13 -31
- data/bin/asession +32 -22
- data/examples/dascli +2 -2
- data/lib/aspera/agent/alpha.rb +117 -0
- data/lib/aspera/agent/base.rb +61 -0
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
- data/lib/aspera/agent/trsdk.rb +188 -0
- 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 +47 -14
- data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
- data/lib/aspera/{fasp → ascp}/management.rb +14 -14
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +27 -14
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/main.rb +49 -29
- data/lib/aspera/cli/manager.rb +46 -36
- data/lib/aspera/cli/plugin.rb +34 -20
- 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 +168 -132
- data/lib/aspera/cli/plugins/ats.rb +33 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +250 -272
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +20 -19
- data/lib/aspera/cli/plugins/faspex.rb +71 -60
- data/lib/aspera/cli/plugins/faspex5.rb +212 -133
- data/lib/aspera/cli/plugins/node.rb +83 -75
- data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
- data/lib/aspera/cli/plugins/preview.rb +33 -31
- data/lib/aspera/cli/plugins/server.rb +33 -32
- data/lib/aspera/cli/plugins/shares.rb +39 -33
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +45 -25
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +16 -14
- data/lib/aspera/coverage.rb +21 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +5 -4
- data/lib/aspera/faspex_gw.rb +13 -11
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +4 -2
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +29 -22
- data/lib/aspera/log.rb +5 -4
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node_simulator.rb +213 -0
- 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 -328
- data/lib/aspera/open_application.rb +7 -7
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/preview/file_types.rb +53 -267
- data/lib/aspera/preview/generator.rb +7 -5
- data/lib/aspera/preview/terminal.rb +17 -7
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +187 -140
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +5 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +15 -8
- data/lib/aspera/temp_file_manager.rb +1 -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 +95 -120
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/transfer/sync.rb +273 -0
- data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
- data/lib/aspera/web_server_simple.rb +12 -3
- data.tar.gz.sig +0 -0
- metadata +92 -68
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -606
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -93
- data/lib/aspera/fasp/agent_aspera.rb +0 -126
- data/lib/aspera/fasp/agent_base.rb +0 -48
- data/lib/aspera/fasp/agent_trsdk.rb +0 -146
- data/lib/aspera/fasp/resume_policy.rb +0 -77
- data/lib/aspera/node.rb +0 -338
- data/lib/aspera/sync.rb +0 -219
data/lib/aspera/node.rb
DELETED
|
@@ -1,338 +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/environment'
|
|
9
|
-
require 'zlib'
|
|
10
|
-
require 'base64'
|
|
11
|
-
|
|
12
|
-
module Aspera
|
|
13
|
-
# Provides additional functions using node API with gen4 extensions (access keys)
|
|
14
|
-
class Node < Aspera::Rest
|
|
15
|
-
# permissions
|
|
16
|
-
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
17
|
-
# prefix for ruby code for filter (deprecated)
|
|
18
|
-
MATCH_EXEC_PREFIX = 'exec:'
|
|
19
|
-
MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
|
|
20
|
-
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
21
|
-
PATH_SEPARATOR = '/'
|
|
22
|
-
TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
|
|
23
|
-
SCOPE_USER = 'user:all'
|
|
24
|
-
SCOPE_ADMIN = 'admin:all'
|
|
25
|
-
SCOPE_PREFIX = 'node.'
|
|
26
|
-
SCOPE_SEPARATOR = ':'
|
|
27
|
-
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
28
|
-
BEARER_TOKEN_VALIDITY_DEFAULT = 86400
|
|
29
|
-
BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
|
|
30
|
-
|
|
31
|
-
# register node special token decoder
|
|
32
|
-
Oauth.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
|
|
33
|
-
|
|
34
|
-
# class instance variable, access with accessors on class
|
|
35
|
-
@use_standard_ports = true
|
|
36
|
-
|
|
37
|
-
class << self
|
|
38
|
-
attr_accessor :use_standard_ports
|
|
39
|
-
|
|
40
|
-
# For access keys: provide expression to match entry in folder
|
|
41
|
-
def file_matcher(match_expression)
|
|
42
|
-
case match_expression
|
|
43
|
-
when Proc then return match_expression
|
|
44
|
-
when Regexp then return ->(f){f['name'].match?(match_expression)}
|
|
45
|
-
when String
|
|
46
|
-
if match_expression.start_with?(MATCH_EXEC_PREFIX)
|
|
47
|
-
code = "->(f){#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
|
|
48
|
-
Log.log.warn{"Use of prefix #{MATCH_EXEC_PREFIX} is deprecated (4.15), instead use: @ruby:'#{code}'"}
|
|
49
|
-
return Environment.secure_eval(code, __FILE__, __LINE__)
|
|
50
|
-
end
|
|
51
|
-
return lambda{|f|File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
|
|
52
|
-
when NilClass then return ->(_){true}
|
|
53
|
-
else raise Cli::BadArgument, "Invalid match expression type: #{match_expression.class}"
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def file_matcher_from_argument(options)
|
|
58
|
-
return file_matcher(options.get_next_argument('filter', type: MATCH_TYPES, mandatory: false))
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# node API scopes
|
|
62
|
-
def token_scope(access_key, scope)
|
|
63
|
-
return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def decode_scope(scope)
|
|
67
|
-
items = scope.split(SCOPE_SEPARATOR, 2)
|
|
68
|
-
raise "invalid scope: #{scope}" unless items.length.eql?(2)
|
|
69
|
-
raise "invalid scope: #{scope}" unless items[0].start_with?(SCOPE_PREFIX)
|
|
70
|
-
return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Create an Aspera Node bearer token
|
|
74
|
-
# @param payload [String] JSON payload to be included in the token
|
|
75
|
-
# @param private_key [OpenSSL::PKey::RSA] Private key to sign the token
|
|
76
|
-
def bearer_token(access_key:, payload:, private_key:)
|
|
77
|
-
raise 'payload shall be Hash' unless payload.is_a?(Hash)
|
|
78
|
-
raise 'missing user_id' unless payload.key?('user_id')
|
|
79
|
-
raise 'user_id must be a String' unless payload['user_id'].is_a?(String)
|
|
80
|
-
raise 'user_id must not be empty' if payload['user_id'].empty?
|
|
81
|
-
raise 'private_key shall be OpenSSL::PKey::RSA' unless private_key.is_a?(OpenSSL::PKey::RSA)
|
|
82
|
-
# manage convenience parameters
|
|
83
|
-
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
84
|
-
payload.delete('_validity')
|
|
85
|
-
scope = payload['_scope'] || BEARER_TOKEN_SCOPE_DEFAULT
|
|
86
|
-
payload.delete('_scope')
|
|
87
|
-
payload['scope'] ||= token_scope(access_key, scope)
|
|
88
|
-
payload['auth_type'] ||= 'access_key'
|
|
89
|
-
payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
|
|
90
|
-
payload_json = JSON.generate(payload)
|
|
91
|
-
return Base64.strict_encode64(Zlib::Deflate.deflate([
|
|
92
|
-
payload_json,
|
|
93
|
-
SIGNATURE_DELIMITER,
|
|
94
|
-
Base64.strict_encode64(private_key.sign(OpenSSL::Digest.new('sha512'), payload_json)).scan(/.{1,60}/).join("\n"),
|
|
95
|
-
''
|
|
96
|
-
].join("\n")))
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def decode_bearer_token(token)
|
|
100
|
-
return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition(SIGNATURE_DELIMITER).first)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def bearer_headers(bearer_auth, access_key: nil)
|
|
104
|
-
# if username is not provided, use the access key from the token
|
|
105
|
-
if access_key.nil?
|
|
106
|
-
access_key = Aspera::Node.decode_scope(Aspera::Node.decode_bearer_token(Oauth.bearer_extract(bearer_auth))['scope'])[:access_key]
|
|
107
|
-
raise "internal error #{access_key}" if access_key.nil?
|
|
108
|
-
end
|
|
109
|
-
return {
|
|
110
|
-
Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => access_key,
|
|
111
|
-
'Authorization' => bearer_auth
|
|
112
|
-
}
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# fields in @app_info
|
|
117
|
-
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
118
|
-
# methods of @app_info[:api]
|
|
119
|
-
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
120
|
-
private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
121
|
-
|
|
122
|
-
attr_reader :app_info
|
|
123
|
-
|
|
124
|
-
# @param params [Hash] Rest parameters
|
|
125
|
-
# @param app_info [Hash,NilClass] special processing for AoC
|
|
126
|
-
def initialize(params:, app_info: nil, add_tspec: nil)
|
|
127
|
-
# init Rest
|
|
128
|
-
super(params)
|
|
129
|
-
@app_info = app_info
|
|
130
|
-
# this is added to transfer spec, for instance to add tags (COS)
|
|
131
|
-
@add_tspec = add_tspec
|
|
132
|
-
if !@app_info.nil?
|
|
133
|
-
REQUIRED_APP_INFO_FIELDS.each do |field|
|
|
134
|
-
raise "INTERNAL ERROR: app_info lacks field #{field}" unless @app_info.key?(field)
|
|
135
|
-
end
|
|
136
|
-
REQUIRED_APP_API_METHODS.each do |method|
|
|
137
|
-
raise "INTERNAL ERROR: #{@app_info[:api].class} lacks method #{method}" unless @app_info[:api].respond_to?(method)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# update transfer spec with special additional tags
|
|
143
|
-
def add_tspec_info(tspec)
|
|
144
|
-
tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
|
|
145
|
-
return tspec
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# @returns [Aspera::Node] a Node or nil
|
|
149
|
-
def node_id_to_node(node_id)
|
|
150
|
-
if !@app_info.nil?
|
|
151
|
-
return self if node_id.eql?(@app_info[:node_info]['id'])
|
|
152
|
-
return @app_info[:api].node_api_from(
|
|
153
|
-
node_id: node_id,
|
|
154
|
-
workspace_id: @app_info[:workspace_id],
|
|
155
|
-
workspace_name: @app_info[:workspace_name])
|
|
156
|
-
end
|
|
157
|
-
Log.log.warn{"cannot resolve link with node id #{node_id}"}
|
|
158
|
-
return nil
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Recursively browse in a folder (with non-recursive method)
|
|
162
|
-
# sub folders are processed if the processing method returns true
|
|
163
|
-
# @param state [Object] state object sent to processing method
|
|
164
|
-
# @param top_file_id [String] file id to start at (default = access key root file id)
|
|
165
|
-
# @param top_file_path [String] path of top folder (default = /)
|
|
166
|
-
# @param block [Proc] processing method, arguments: entry, path, state
|
|
167
|
-
def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
|
|
168
|
-
raise 'INTERNAL ERROR: top_file_path not set' if top_file_path.nil?
|
|
169
|
-
raise 'INTERNAL ERROR: Missing block' unless block
|
|
170
|
-
# start at top folder
|
|
171
|
-
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
|
172
|
-
Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
|
|
173
|
-
until folders_to_explore.empty?
|
|
174
|
-
current_item = folders_to_explore.shift
|
|
175
|
-
Log.log.debug{"searching #{current_item[:path]}".bg_green}
|
|
176
|
-
# get folder content
|
|
177
|
-
folder_contents =
|
|
178
|
-
begin
|
|
179
|
-
read("files/#{current_item[:id]}/files")[:data]
|
|
180
|
-
rescue StandardError => e
|
|
181
|
-
Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
|
|
182
|
-
[]
|
|
183
|
-
end
|
|
184
|
-
Log.log.debug{Log.dump(:folder_contents, folder_contents)}
|
|
185
|
-
folder_contents.each do |entry|
|
|
186
|
-
relative_path = File.join(current_item[:path], entry['name'])
|
|
187
|
-
Log.log.debug{"process_folder_tree checking #{relative_path}"}
|
|
188
|
-
# continue only if method returns true
|
|
189
|
-
next unless yield(entry, relative_path, state)
|
|
190
|
-
# entry type is file, folder or link
|
|
191
|
-
case entry['type']
|
|
192
|
-
when 'folder'
|
|
193
|
-
folders_to_explore.push({id: entry['id'], path: relative_path})
|
|
194
|
-
when 'link'
|
|
195
|
-
node_id_to_node(entry['target_node_id'])&.process_folder_tree(
|
|
196
|
-
state: state,
|
|
197
|
-
top_file_id: entry['target_id'],
|
|
198
|
-
top_file_path: relative_path,
|
|
199
|
-
&block)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end # process_folder_tree
|
|
204
|
-
|
|
205
|
-
# Navigate the path from given file id
|
|
206
|
-
# @param top_file_id [String] id initial file id
|
|
207
|
-
# @param path [String] file path
|
|
208
|
-
# @return [Hash] {.api,.file_id}
|
|
209
|
-
def resolve_api_fid(top_file_id, path)
|
|
210
|
-
raise 'file id shall be String' unless top_file_id.is_a?(String)
|
|
211
|
-
process_last_link = path.end_with?(PATH_SEPARATOR)
|
|
212
|
-
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
|
213
|
-
return {api: self, file_id: top_file_id} if path_elements.empty?
|
|
214
|
-
resolve_state = {path: path_elements, result: nil}
|
|
215
|
-
process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
|
|
216
|
-
# this block is called recursively for each entry in folder
|
|
217
|
-
# stop digging here if not in right path
|
|
218
|
-
next false unless entry['name'].eql?(state[:path].first)
|
|
219
|
-
# ok it matches, so we remove the match
|
|
220
|
-
state[:path].shift
|
|
221
|
-
case entry['type']
|
|
222
|
-
when 'file'
|
|
223
|
-
# file must be terminal
|
|
224
|
-
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
|
|
225
|
-
# it's terminal, we found it
|
|
226
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
227
|
-
next false
|
|
228
|
-
when 'folder'
|
|
229
|
-
if state[:path].empty?
|
|
230
|
-
# we found it
|
|
231
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
232
|
-
next false
|
|
233
|
-
end
|
|
234
|
-
when 'link'
|
|
235
|
-
if state[:path].empty?
|
|
236
|
-
if process_last_link
|
|
237
|
-
# we found it
|
|
238
|
-
other_node = node_id_to_node(entry['target_node_id'])
|
|
239
|
-
raise 'cannot resolve link' if other_node.nil?
|
|
240
|
-
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
241
|
-
else
|
|
242
|
-
# we found it but we do not process the link
|
|
243
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
244
|
-
end
|
|
245
|
-
next false
|
|
246
|
-
end
|
|
247
|
-
else
|
|
248
|
-
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
249
|
-
end
|
|
250
|
-
# continue to dig folder
|
|
251
|
-
next true
|
|
252
|
-
end
|
|
253
|
-
raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
|
|
254
|
-
return resolve_state[:result]
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def find_files(top_file_id, test_block)
|
|
258
|
-
Log.log.debug{"find_files: file id=#{top_file_id}"}
|
|
259
|
-
find_state = {found: [], test_block: test_block}
|
|
260
|
-
process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
|
|
261
|
-
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
262
|
-
# test all files deeply
|
|
263
|
-
true
|
|
264
|
-
end
|
|
265
|
-
return find_state[:found]
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def refreshed_transfer_token
|
|
269
|
-
return oauth_token(force_refresh: true)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Create transfer spec for gen4
|
|
273
|
-
def transfer_spec_gen4(file_id, direction, ts_merge=nil)
|
|
274
|
-
ak_name = nil
|
|
275
|
-
ak_token = nil
|
|
276
|
-
case params[:auth][:type]
|
|
277
|
-
when :basic
|
|
278
|
-
ak_name = params[:auth][:username]
|
|
279
|
-
raise 'ERROR: no secret in node object' unless params[:auth][:password]
|
|
280
|
-
ak_token = Rest.basic_token(params[:auth][:username], params[:auth][:password])
|
|
281
|
-
when :oauth2
|
|
282
|
-
ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
|
|
283
|
-
# TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
|
|
284
|
-
# get bearer token, possibly use cache
|
|
285
|
-
ak_token = oauth_token(force_refresh: false)
|
|
286
|
-
else raise "Unsupported auth method for node gen4: #{params[:auth][:type]}"
|
|
287
|
-
end
|
|
288
|
-
transfer_spec = {
|
|
289
|
-
'direction' => direction,
|
|
290
|
-
'token' => ak_token,
|
|
291
|
-
'tags' => {
|
|
292
|
-
Fasp::TransferSpec::TAG_RESERVED => {
|
|
293
|
-
'node' => {
|
|
294
|
-
'access_key' => ak_name,
|
|
295
|
-
'file_id' => file_id
|
|
296
|
-
} # node
|
|
297
|
-
} # aspera
|
|
298
|
-
} # tags
|
|
299
|
-
}
|
|
300
|
-
# add specials tags (cos)
|
|
301
|
-
add_tspec_info(transfer_spec)
|
|
302
|
-
transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
|
|
303
|
-
# add application specific tags (AoC)
|
|
304
|
-
app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
|
|
305
|
-
# add remote host info
|
|
306
|
-
if self.class.use_standard_ports
|
|
307
|
-
# get default TCP/UDP ports and transfer user
|
|
308
|
-
transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
|
|
309
|
-
# by default: same address as node API
|
|
310
|
-
transfer_spec['remote_host'] = URI.parse(params[:base_url]).host
|
|
311
|
-
# AoC allows specification of other url
|
|
312
|
-
if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
|
|
313
|
-
transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
|
|
314
|
-
end
|
|
315
|
-
info = read('info')[:data]
|
|
316
|
-
# get the transfer user from info on access key
|
|
317
|
-
transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
|
|
318
|
-
# get settings from name.value array to hash key.value
|
|
319
|
-
settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
|
|
320
|
-
# check WSS ports
|
|
321
|
-
%w[wss_enabled wss_port].each do |i|
|
|
322
|
-
transfer_spec[i] = settings[i] if settings.key?(i)
|
|
323
|
-
end if settings.is_a?(Hash)
|
|
324
|
-
else
|
|
325
|
-
# retrieve values from API (and keep a copy/cache)
|
|
326
|
-
@std_t_spec_cache ||= create(
|
|
327
|
-
'files/download_setup',
|
|
328
|
-
{transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
|
|
329
|
-
)[:data]['transfer_specs'].first['transfer_spec']
|
|
330
|
-
# copy some parts
|
|
331
|
-
TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
|
|
332
|
-
end
|
|
333
|
-
Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
|
|
334
|
-
unless transfer_spec['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
|
|
335
|
-
return transfer_spec
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
end
|
data/lib/aspera/sync.rb
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# cspell:words logdir bidi watchd cooloff asyncadmin
|
|
4
|
-
|
|
5
|
-
require 'aspera/command_line_builder'
|
|
6
|
-
require 'aspera/fasp/installation'
|
|
7
|
-
require 'aspera/log'
|
|
8
|
-
require 'json'
|
|
9
|
-
require 'base64'
|
|
10
|
-
require 'open3'
|
|
11
|
-
require 'English'
|
|
12
|
-
|
|
13
|
-
module Aspera
|
|
14
|
-
# builds command line arg for async
|
|
15
|
-
module Sync
|
|
16
|
-
# sync direction, default is push
|
|
17
|
-
DIRECTIONS = %i[push pull bidi].freeze
|
|
18
|
-
PARAMS_VX_INSTANCE =
|
|
19
|
-
{
|
|
20
|
-
'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
21
|
-
'watchd' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
22
|
-
'apply_local_docroot' => { cli: { type: :opt_without_arg}},
|
|
23
|
-
'quiet' => { cli: { type: :opt_without_arg}},
|
|
24
|
-
'ws_connect' => { cli: { type: :opt_without_arg}}
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
# map sync session parameters to transfer spec: sync -> ts, true if same
|
|
28
|
-
PARAMS_VX_SESSION =
|
|
29
|
-
{
|
|
30
|
-
'name' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
31
|
-
'local_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
32
|
-
'remote_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
33
|
-
'local_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
34
|
-
'remote_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
35
|
-
'host' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_host},
|
|
36
|
-
'user' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_user},
|
|
37
|
-
'private_key_paths' => { cli: { type: :opt_with_arg, switch: '--private-key-path'}, accepted_types: :array},
|
|
38
|
-
'direction' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
39
|
-
'checksum' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
40
|
-
'tags' => { cli: { type: :opt_with_arg, switch: '--tags64', convert: 'Aspera::Fasp::Parameters.convert_json64'},
|
|
41
|
-
accepted_types: :hash, ts: true},
|
|
42
|
-
'tcp_port' => { cli: { type: :opt_with_arg}, accepted_types: :int, ts: :ssh_port},
|
|
43
|
-
'rate_policy' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
44
|
-
'target_rate' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
45
|
-
'cooloff' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
|
46
|
-
'pending_max' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
|
47
|
-
'scan_intensity' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
|
48
|
-
'cipher' => { cli: { type: :opt_with_arg, convert: 'Aspera::Fasp::Parameters.convert_remove_hyphen'}, accepted_types: :string, ts: true},
|
|
49
|
-
'transfer_threads' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
|
50
|
-
'preserve_time' => { cli: { type: :opt_without_arg}, ts: :preserve_times},
|
|
51
|
-
'preserve_access_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
|
52
|
-
'preserve_modification_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
|
53
|
-
'preserve_uid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_uid},
|
|
54
|
-
'preserve_gid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_gid},
|
|
55
|
-
'create_dir' => { cli: { type: :opt_without_arg}, ts: true},
|
|
56
|
-
'reset' => { cli: { type: :opt_without_arg}},
|
|
57
|
-
# NOTE: only one env var, but multiple sessions... could be a problem
|
|
58
|
-
'remote_password' => { cli: { type: :envvar, variable: 'ASPERA_SCP_PASS'}, ts: true},
|
|
59
|
-
'cookie' => { cli: { type: :envvar, variable: 'ASPERA_SCP_COOKIE'}, ts: true},
|
|
60
|
-
'token' => { cli: { type: :envvar, variable: 'ASPERA_SCP_TOKEN'}, ts: true},
|
|
61
|
-
'license' => { cli: { type: :envvar, variable: 'ASPERA_SCP_LICENSE'}}
|
|
62
|
-
}.freeze
|
|
63
|
-
|
|
64
|
-
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_INSTANCE)
|
|
65
|
-
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_SESSION)
|
|
66
|
-
|
|
67
|
-
PARAMS_VX_KEYS = %w[instance sessions].freeze
|
|
68
|
-
|
|
69
|
-
# new API
|
|
70
|
-
TS_TO_PARAMS_V2 = {
|
|
71
|
-
'remote_host' => 'remote.host',
|
|
72
|
-
'remote_user' => 'remote.user',
|
|
73
|
-
'remote_password' => 'remote.pass',
|
|
74
|
-
'sshfp' => 'remote.fingerprint',
|
|
75
|
-
'ssh_port' => 'remote.port',
|
|
76
|
-
'wss_port' => 'remote.ws_port',
|
|
77
|
-
'proxy' => 'remote.proxy',
|
|
78
|
-
'token' => 'remote.token',
|
|
79
|
-
'tags' => 'tags'
|
|
80
|
-
}.freeze
|
|
81
|
-
|
|
82
|
-
ASYNC_EXECUTABLE = 'async'
|
|
83
|
-
ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
|
|
84
|
-
|
|
85
|
-
private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :TS_TO_PARAMS_V2, :ASYNC_EXECUTABLE, :ASYNC_ADMIN_EXECUTABLE
|
|
86
|
-
|
|
87
|
-
class << self
|
|
88
|
-
# Set remote_dir in sync parameters based on transfer spec
|
|
89
|
-
# @param params [Hash] sync parameters, old or new format
|
|
90
|
-
# @param remote_dir_key [String] key to update in above hash
|
|
91
|
-
# @param transfer_spec [Hash] transfer spec
|
|
92
|
-
def update_remote_dir(sync_params, remote_dir_key, transfer_spec)
|
|
93
|
-
if transfer_spec.dig(*%w[tags aspera node file_id])
|
|
94
|
-
# in AoC, use gen4
|
|
95
|
-
sync_params[remote_dir_key] = '/'
|
|
96
|
-
elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
|
|
97
|
-
# TODO : something more generic, independent of Shares
|
|
98
|
-
# in Shares, the actual folder on remote end is not always the same as the name of the share
|
|
99
|
-
actual_remote = transfer_spec['paths']&.first&.[]('source')
|
|
100
|
-
sync_params[remote_dir_key] = actual_remote if actual_remote
|
|
101
|
-
end
|
|
102
|
-
nil
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# @param sync_params [Hash] sync parameters, old or new format
|
|
106
|
-
# @param block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
|
|
107
|
-
def start(sync_params, &block)
|
|
108
|
-
raise 'Internal Error: sync_params parameter must be Hash' unless sync_params.is_a?(Hash)
|
|
109
|
-
env_args = {
|
|
110
|
-
args: [],
|
|
111
|
-
env: {}
|
|
112
|
-
}
|
|
113
|
-
if sync_params.key?('local')
|
|
114
|
-
# async native JSON format (v2)
|
|
115
|
-
raise StandardError, 'remote must be Hash' unless sync_params['remote'].is_a?(Hash)
|
|
116
|
-
if block
|
|
117
|
-
transfer_spec = yield((sync_params['direction'] || 'push').to_sym, sync_params['local']['path'], sync_params['remote']['path'])
|
|
118
|
-
# async native JSON format
|
|
119
|
-
raise StandardError, 'sync parameter "local" must be Hash' unless sync_params['local'].is_a?(Hash)
|
|
120
|
-
TS_TO_PARAMS_V2.each do |ts_param, sy_path|
|
|
121
|
-
next unless transfer_spec.key?(ts_param)
|
|
122
|
-
sy_dig = sy_path.split('.')
|
|
123
|
-
param = sy_dig.pop
|
|
124
|
-
hash = sy_dig.empty? ? sync_params : sync_params[sy_dig.first]
|
|
125
|
-
hash = sync_params[sy_dig.first] = {} if hash.nil?
|
|
126
|
-
hash[param] = transfer_spec[ts_param]
|
|
127
|
-
end
|
|
128
|
-
sync_params['remote']['connect_mode'] ||= sync_params['remote'].key?('ws_port') ? 'ws' : 'ssh'
|
|
129
|
-
sync_params['remote']['private_key_paths'] ||= Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
|
|
130
|
-
update_remote_dir(sync_params['remote'], 'path', transfer_spec)
|
|
131
|
-
end
|
|
132
|
-
env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
|
|
133
|
-
elsif sync_params.key?('sessions')
|
|
134
|
-
# ascli JSON format (v1)
|
|
135
|
-
if block
|
|
136
|
-
sync_params['sessions'].each do |session|
|
|
137
|
-
transfer_spec = yield((session['direction'] || 'push').to_sym, session['local_dir'], session['remote_dir'])
|
|
138
|
-
PARAMS_VX_SESSION.each do |async_param, behavior|
|
|
139
|
-
if behavior.key?(:ts)
|
|
140
|
-
tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
|
|
141
|
-
session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
session['private_key_paths'] = Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
|
|
145
|
-
update_remote_dir(session, 'remote_dir', transfer_spec)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
|
|
149
|
-
sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
|
|
150
|
-
raise StandardError, 'sessions key must be Array' unless sync_params['sessions'].is_a?(Array)
|
|
151
|
-
raise StandardError, 'sessions key requires at least one Hash' unless sync_params['sessions'].first.is_a?(Hash)
|
|
152
|
-
|
|
153
|
-
if sync_params.key?('instance')
|
|
154
|
-
raise StandardError, 'instance key must be Hash' unless sync_params['instance'].is_a?(Hash)
|
|
155
|
-
instance_builder = Aspera::CommandLineBuilder.new(sync_params['instance'], PARAMS_VX_INSTANCE)
|
|
156
|
-
instance_builder.process_params
|
|
157
|
-
instance_builder.add_env_args(env_args)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
sync_params['sessions'].each do |session_params|
|
|
161
|
-
raise StandardError, 'sessions must contain hashes' unless session_params.is_a?(Hash)
|
|
162
|
-
raise StandardError, 'session must contain at least name' unless session_params.key?('name')
|
|
163
|
-
session_builder = Aspera::CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
|
|
164
|
-
session_builder.process_params
|
|
165
|
-
session_builder.add_env_args(env_args)
|
|
166
|
-
end
|
|
167
|
-
else
|
|
168
|
-
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
|
169
|
-
end
|
|
170
|
-
Log.log.debug{Log.dump(:sync_params, sync_params)}
|
|
171
|
-
|
|
172
|
-
Log.log.debug{"execute: #{env_args[:env].map{|k, v| "#{k}=\"#{v}\""}.join(' ')} \"#{ASYNC_EXECUTABLE}\" \"#{env_args[:args].join('" "')}\""}
|
|
173
|
-
res = system(env_args[:env], [ASYNC_EXECUTABLE, ASYNC_EXECUTABLE], *env_args[:args])
|
|
174
|
-
Log.log.debug{"result=#{res}"}
|
|
175
|
-
case res
|
|
176
|
-
when true then return nil
|
|
177
|
-
when false then raise "failed: #{$CHILD_STATUS}"
|
|
178
|
-
when nil then raise "not started: #{$CHILD_STATUS}"
|
|
179
|
-
else raise 'internal error: unspecified case'
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def admin_status(sync_params, session_name)
|
|
184
|
-
command_line = [ASYNC_ADMIN_EXECUTABLE, '--quiet']
|
|
185
|
-
if sync_params.key?('local')
|
|
186
|
-
raise 'Missing session name' if sync_params['name'].nil?
|
|
187
|
-
raise 'Session not found' unless session_name.nil? || session_name.eql?(sync_params['name'])
|
|
188
|
-
command_line.push("--name=#{sync_params['name']}")
|
|
189
|
-
if sync_params.key?('local_db_dir')
|
|
190
|
-
command_line.push("--local-db-dir=#{sync_params['local_db_dir']}")
|
|
191
|
-
elsif sync_params.dig('local', 'path')
|
|
192
|
-
command_line.push("--local-dir=#{sync_params.dig('local', 'path')}")
|
|
193
|
-
else
|
|
194
|
-
raise 'Missing either local_db_dir or local.path'
|
|
195
|
-
end
|
|
196
|
-
elsif sync_params.key?('sessions')
|
|
197
|
-
session = session_name.nil? ? sync_params['sessions'].first : sync_params['sessions'].find{|s|s['name'].eql?(session_name)}
|
|
198
|
-
raise "Session #{session_name} not found in #{sync_params['sessions'].map{|s|s['name']}.join(',')}" if session.nil?
|
|
199
|
-
raise 'Missing session name' if session['name'].nil?
|
|
200
|
-
command_line.push("--name=#{session['name']}")
|
|
201
|
-
if session.key?('local_db_dir')
|
|
202
|
-
command_line.push("--local-db-dir=#{session['local_db_dir']}")
|
|
203
|
-
elsif session.key?('local_dir')
|
|
204
|
-
command_line.push("--local-dir=#{session['local_dir']}")
|
|
205
|
-
else
|
|
206
|
-
raise 'Missing either local_db_dir or local_dir'
|
|
207
|
-
end
|
|
208
|
-
else
|
|
209
|
-
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
|
210
|
-
end
|
|
211
|
-
Log.log.debug{"execute: #{command_line.join(' ')}"}
|
|
212
|
-
stdout, stderr, status = Open3.capture3(*command_line)
|
|
213
|
-
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
|
214
|
-
raise "Sync failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
|
215
|
-
return stdout.split("\n").each_with_object({}){|l, m|i = l.split(':', 2); m[i.first.lstrip] = i.last.lstrip} # rubocop:disable Style/Semicolon
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end # end Sync
|
|
219
|
-
end # end Aspera
|