aspera-cli 4.22.0 → 4.24.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 +405 -364
- data/CONTRIBUTING.md +86 -29
- data/README.md +1856 -961
- 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 +53 -43
- data/lib/aspera/api/cos_node.rb +7 -5
- data/lib/aspera/api/httpgw.rb +23 -22
- data/lib/aspera/api/node.rb +104 -22
- data/lib/aspera/ascmd.rb +35 -21
- data/lib/aspera/ascp/installation.rb +43 -43
- data/lib/aspera/ascp/management.rb +5 -4
- data/lib/aspera/assert.rb +55 -24
- 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 +38 -4
- data/lib/aspera/cli/main.rb +139 -108
- data/lib/aspera/cli/manager.rb +51 -31
- data/lib/aspera/cli/plugin.rb +149 -78
- data/lib/aspera/cli/plugin_factory.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +217 -88
- data/lib/aspera/cli/plugins/ats.rb +15 -13
- data/lib/aspera/cli/plugins/config.rb +105 -227
- 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 +162 -163
- 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 +233 -247
- 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 +29 -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 +55 -58
- data/lib/aspera/cli/transfer_progress.rb +29 -20
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +160 -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/data_repository.rb +1 -0
- data/lib/aspera/environment.rb +144 -100
- 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 +70 -20
- data/lib/aspera/nagios.rb +5 -6
- data/lib/aspera/node_simulator.rb +12 -7
- data/lib/aspera/oauth/base.rb +6 -2
- data/lib/aspera/oauth/factory.rb +25 -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 +43 -35
- data/lib/aspera/preview/generator.rb +26 -13
- data/lib/aspera/preview/terminal.rb +10 -7
- data/lib/aspera/preview/utils.rb +11 -9
- data/lib/aspera/products/connect.rb +2 -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 +46 -28
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +46 -40
- data/lib/aspera/ssh.rb +14 -4
- 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/{transfer/sync.rb → sync/operations.rb} +145 -68
- data/lib/aspera/temp_file_manager.rb +4 -2
- data/lib/aspera/timer_limiter.rb +7 -5
- 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 -15
- metadata.gz.sig +0 -0
- data/examples/dascli +0 -30
- data/examples/get_proto_file.rb +0 -8
- data/examples/proxy.pac +0 -60
- data/lib/aspera/transfer/convert.rb +0 -29
- data/lib/aspera/transfer/sync_instance.schema.yaml +0 -13
- data/lib/aspera/transfer/sync_session.schema.yaml +0 -79
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
|
data/lib/aspera/ascmd.rb
CHANGED
@@ -10,7 +10,7 @@ module Aspera
|
|
10
10
|
# execute: "ascmd -h" to get syntax
|
11
11
|
# Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
|
12
12
|
class AsCmd
|
13
|
-
# number of arguments for each operation
|
13
|
+
# number of arguments for each operation (to allow splitting into batches)
|
14
14
|
OPS_ARGS = {
|
15
15
|
cp: 2,
|
16
16
|
df: 0,
|
@@ -23,17 +23,23 @@ module Aspera
|
|
23
23
|
rm: 1
|
24
24
|
}.freeze
|
25
25
|
|
26
|
-
#
|
27
|
-
#
|
26
|
+
# Protocol is based on Type-Length-Value
|
27
|
+
# Type start at one, but array index start at zero
|
28
28
|
ENUM_START = 1
|
29
29
|
|
30
|
-
#
|
30
|
+
# Description of result structures (see ascmdtypes.h).
|
31
31
|
# Base types are big endian
|
32
32
|
# key = name of type
|
33
|
-
# index in array `fields` is the type
|
33
|
+
# index in array `fields` is: the numerical type of TLV - `ENUM_START`. i.e. add `ENUM_START` to index, to get `T`
|
34
34
|
# decoding always start at `result`
|
35
35
|
# some fields have special handling indicated by `special`
|
36
36
|
# field_list, list_tlv_list, list_tlv_restart are composed with a list of TLV
|
37
|
+
# decode:
|
38
|
+
# - :base : value
|
39
|
+
# - :buffer_list : an array of {btype,buffer}
|
40
|
+
# - :field_list : a hash, or array
|
41
|
+
# :check: ???
|
42
|
+
# rubocop:disable Layout/FirstHashElementLineBreak
|
37
43
|
TYPES_DESCR = {
|
38
44
|
result: {decode: :field_list,
|
39
45
|
fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :list_tlv_list}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
|
@@ -67,6 +73,7 @@ module Aspera
|
|
67
73
|
zstr: {decode: :base, unpack: 'Z*'},
|
68
74
|
blist: {decode: :buffer_list}
|
69
75
|
}.freeze
|
76
|
+
# rubocop:enable Layout/FirstHashElementLineBreak
|
70
77
|
|
71
78
|
private_constant :TYPES_DESCR, :ENUM_START, :OPS_ARGS
|
72
79
|
|
@@ -79,17 +86,25 @@ module Aspera
|
|
79
86
|
end
|
80
87
|
|
81
88
|
# execute an "as" command on a remote server
|
89
|
+
# Version 2 allows use of reverse proxy with multiple addresses.
|
82
90
|
# @param [Symbol] one of OPERATIONS
|
83
91
|
# @param [Array] parameters for "as" command
|
84
92
|
# @return result of command, type depends on command (bool, array, hash)
|
85
|
-
def execute_single(action_sym, arguments)
|
93
|
+
def execute_single(action_sym, arguments, version: 1, host: nil)
|
86
94
|
arguments = [] if arguments.nil?
|
87
95
|
Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
|
88
96
|
Aspera.assert_type(action_sym, Symbol)
|
89
97
|
Aspera.assert_type(arguments, Array)
|
90
98
|
Aspera.assert(arguments.all?(String), 'arguments must be strings')
|
99
|
+
remote_cmd = 'ascmd'
|
91
100
|
# lines of commands (String's)
|
92
101
|
command_lines = []
|
102
|
+
if version.eql?(2)
|
103
|
+
cmd = "as_session_init --protocol=#{version}"
|
104
|
+
cmd += " --host=#{host}" if host
|
105
|
+
command_lines.push(cmd)
|
106
|
+
remote_cmd += ' -V2'
|
107
|
+
end
|
93
108
|
# add "as_" command
|
94
109
|
main_command = "as_#{action_sym}"
|
95
110
|
arg_batches =
|
@@ -114,11 +129,11 @@ module Aspera
|
|
114
129
|
stdin_input = command_lines.join("\n")
|
115
130
|
Log.log.trace1{"execute_single:#{stdin_input}"}
|
116
131
|
# execute, get binary output
|
117
|
-
byte_buffer = @command_executor.execute(
|
118
|
-
|
132
|
+
byte_buffer = @command_executor.execute(remote_cmd, input: stdin_input).unpack('C*')
|
133
|
+
Aspera.assert(!byte_buffer.empty?){'empty answer from server'}
|
119
134
|
# get hash or table result
|
120
135
|
result = self.class.parse(byte_buffer, :result)
|
121
|
-
|
136
|
+
Aspera.assert(byte_buffer.empty?){'unparsed bytes remaining'}
|
122
137
|
# get and delete info,always present in results
|
123
138
|
system_info = result[:info]
|
124
139
|
result.delete(:info)
|
@@ -129,7 +144,7 @@ module Aspera
|
|
129
144
|
result[:dir].each do |file|
|
130
145
|
if file.key?(:smode)
|
131
146
|
# Converts the first character of the file mode (see 'man ls') into a type.
|
132
|
-
file[:type] = case file[:smode][0, 1]; when 'd' then:directory; when '-' then:file; when 'l' then:link; else; :other; end
|
147
|
+
file[:type] = case file[:smode][0, 1]; when 'd' then:directory; when '-' then:file; when 'l' then:link; else; :other; end
|
133
148
|
end
|
134
149
|
end
|
135
150
|
end
|
@@ -145,27 +160,26 @@ module Aspera
|
|
145
160
|
return result
|
146
161
|
end
|
147
162
|
|
148
|
-
# This exception is raised when
|
163
|
+
# This exception is raised when `ascmd` returns an error.
|
149
164
|
class Error < StandardError
|
150
|
-
|
151
|
-
|
152
|
-
|
165
|
+
# rubocop:disable Style/Semicolon
|
166
|
+
def initialize(errno, errstr, cmd, arguments); super(); @errno = errno; @errstr = errstr; @command = cmd; @arguments = arguments; end
|
153
167
|
def message; "ascmd: #{@errstr} (#{@errno})"; end
|
154
168
|
def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
|
169
|
+
# rubocop:enable Style/Semicolon
|
155
170
|
end
|
156
171
|
|
157
172
|
class << self
|
158
|
-
#
|
173
|
+
# Get description of structure's field, @param struct_name, @param typed_buffer provides field name
|
159
174
|
def field_description(struct_name, typed_buffer)
|
160
175
|
result = TYPES_DESCR[struct_name][:fields][typed_buffer[:btype] - ENUM_START]
|
161
176
|
raise "Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}" if result.nil?
|
162
177
|
return result
|
163
178
|
end
|
164
179
|
|
165
|
-
#
|
166
|
-
# @return a decoded type
|
167
|
-
|
168
|
-
def parse(buffer, type_name, indent_level=nil)
|
180
|
+
# Decodes the provided buffer as provided type name
|
181
|
+
# @return a decoded type: single, Array, or Hash
|
182
|
+
def parse(buffer, type_name, indent_level = nil)
|
169
183
|
indent_level = (indent_level || -1) + 1
|
170
184
|
type_descr = TYPES_DESCR[type_name]
|
171
185
|
raise "Unexpected type #{type_name}" if type_descr.nil?
|
@@ -174,7 +188,7 @@ module Aspera
|
|
174
188
|
case type_descr[:decode]
|
175
189
|
when :base
|
176
190
|
num_bytes = type_name.eql?(:zstr) ? buffer.length : type_descr[:size]
|
177
|
-
|
191
|
+
Aspera.assert(buffer.length >= num_bytes){'not enough bytes'}
|
178
192
|
byte_array = buffer.shift(num_bytes)
|
179
193
|
byte_array = [byte_array] unless byte_array.is_a?(Array)
|
180
194
|
result = byte_array.pack('C*').unpack1(type_descr[:unpack])
|
@@ -187,7 +201,7 @@ module Aspera
|
|
187
201
|
until buffer.empty?
|
188
202
|
btype = parse(buffer, :int8, indent_level)
|
189
203
|
length = parse(buffer, :int32, indent_level)
|
190
|
-
|
204
|
+
Aspera.assert(buffer.length >= length){'not enough bytes'}
|
191
205
|
value = buffer.shift(length)
|
192
206
|
result.push({btype: btype, buffer: value})
|
193
207
|
Log.log.trace1{"#{' .' * indent_level}:buffer_list[#{result.length - 1}] #{result.last}"}
|
@@ -32,6 +32,7 @@ module Aspera
|
|
32
32
|
# Installation.instance.ascp_path=""
|
33
33
|
class Installation
|
34
34
|
include Singleton
|
35
|
+
|
35
36
|
DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
|
36
37
|
<?xml version='1.0' encoding='UTF-8'?>
|
37
38
|
<CONF version="2">
|
@@ -43,12 +44,12 @@ module Aspera
|
|
43
44
|
</default>
|
44
45
|
</CONF>
|
45
46
|
END_OF_CONFIG_FILE
|
46
|
-
# all
|
47
|
-
EXE_FILES = %i[ascp ascp4 async].freeze
|
48
|
-
|
49
|
-
|
47
|
+
# all executable files from SDK
|
48
|
+
EXE_FILES = %i[ascp ascp4 async transferd].freeze
|
49
|
+
SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
|
50
|
+
TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
|
50
51
|
# filename for ascp with optional extension (Windows)
|
51
|
-
private_constant :DEFAULT_ASPERA_CONF, :
|
52
|
+
private_constant :DEFAULT_ASPERA_CONF, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
|
52
53
|
# options for SSH client private key
|
53
54
|
CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
|
54
55
|
|
@@ -98,7 +99,7 @@ module Aspera
|
|
98
99
|
|
99
100
|
# @return [Hash] with key = file name (String), and value = path to file
|
100
101
|
def file_paths
|
101
|
-
return
|
102
|
+
return SDK_FILES.each_with_object({}) do |v, m|
|
102
103
|
m[v.to_s] =
|
103
104
|
begin
|
104
105
|
path(v)
|
@@ -114,19 +115,23 @@ module Aspera
|
|
114
115
|
return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
|
115
116
|
end
|
116
117
|
|
117
|
-
#
|
118
|
+
# Get path of one resource file of currently activated product
|
118
119
|
# keys and certs are generated locally... (they are well known values, arch. independent)
|
120
|
+
# @param k [Symbol] key of the resource file
|
121
|
+
# @return [String, nil] Full path to the resource file or nil if not found
|
119
122
|
def path(k)
|
120
|
-
|
123
|
+
file_is_required = true
|
121
124
|
case k
|
122
125
|
when *EXE_FILES
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
126
|
+
file_is_required = k.eql?(:ascp)
|
127
|
+
file = if k.eql?(:transferd)
|
128
|
+
Products::Transferd.transferd_path
|
129
|
+
else
|
130
|
+
# ensure at least ascp is found
|
131
|
+
use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
|
132
|
+
# NOTE: that there might be a .exe at the end
|
133
|
+
@path_to_ascp.gsub('ascp', k.to_s)
|
134
|
+
end
|
130
135
|
when :ssh_private_dsa, :ssh_private_rsa
|
131
136
|
# assume last 3 letters are type
|
132
137
|
type = k.to_s[-3..-1].to_sym
|
@@ -149,8 +154,8 @@ module Aspera
|
|
149
154
|
file = k.eql?(:fallback_certificate) ? file_cert : file_key
|
150
155
|
else Aspera.error_unexpected_value(k)
|
151
156
|
end
|
152
|
-
return
|
153
|
-
Aspera.assert(File.exist?(file),
|
157
|
+
return unless file_is_required || File.exist?(file)
|
158
|
+
Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{k} not found (#{file})"}
|
154
159
|
return file
|
155
160
|
end
|
156
161
|
|
@@ -167,7 +172,7 @@ module Aspera
|
|
167
172
|
when :dsa_rsa, :rsa
|
168
173
|
types.to_s.split('_').map{ |i| Installation.instance.path("ssh_private_#{i}".to_sym)}
|
169
174
|
when :per_client
|
170
|
-
|
175
|
+
Aspera.error_not_implemented
|
171
176
|
end
|
172
177
|
end
|
173
178
|
|
@@ -178,8 +183,9 @@ module Aspera
|
|
178
183
|
|
179
184
|
# Check that specified path is ascp and get version
|
180
185
|
def get_exe_version(exe_path, vers_arg)
|
181
|
-
|
182
|
-
|
186
|
+
Aspera.assert_type(exe_path, String)
|
187
|
+
Aspera.assert_type(vers_arg, String)
|
188
|
+
return unless File.exist?(exe_path)
|
183
189
|
exe_version = nil
|
184
190
|
cmd_out = %x("#{exe_path}" #{vers_arg})
|
185
191
|
raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
|
@@ -196,6 +202,8 @@ module Aspera
|
|
196
202
|
last_line = ''
|
197
203
|
while (line = stderr.gets)
|
198
204
|
line.chomp!
|
205
|
+
# skip lines that may have accents
|
206
|
+
next unless line.valid_encoding?
|
199
207
|
last_line = line
|
200
208
|
case line
|
201
209
|
when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
|
@@ -211,12 +219,10 @@ module Aspera
|
|
211
219
|
data['product_name'] = Regexp.last_match(1)
|
212
220
|
data['product_version'] = Regexp.last_match(2)
|
213
221
|
when /^LOG Initializing FASP version ([^,]+),/
|
214
|
-
data['
|
222
|
+
data['ascp_version'] = Regexp.last_match(1)
|
215
223
|
end
|
216
224
|
end
|
217
|
-
if !thread.value.exitstatus.eql?(1) && !data.key?('root')
|
218
|
-
raise last_line
|
219
|
-
end
|
225
|
+
raise last_line if !thread.value.exitstatus.eql?(1) && !data.key?('root')
|
220
226
|
end
|
221
227
|
return data
|
222
228
|
end
|
@@ -226,9 +232,9 @@ module Aspera
|
|
226
232
|
data = {}
|
227
233
|
File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
|
228
234
|
if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
|
229
|
-
data['
|
235
|
+
data['ascp_openssl_dir'] = m[1]
|
230
236
|
elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
|
231
|
-
data['
|
237
|
+
data['ascp_openssl_version'] = m[1]
|
232
238
|
end
|
233
239
|
end if File.file?(ascp_path)
|
234
240
|
return data
|
@@ -245,7 +251,7 @@ module Aspera
|
|
245
251
|
# @return the url for download of SDK archive for the given platform and version
|
246
252
|
def sdk_url_for_platform(platform: nil, version: nil)
|
247
253
|
locations = sdk_locations
|
248
|
-
platform = Environment.architecture if platform.nil?
|
254
|
+
platform = Environment.instance.architecture if platform.nil?
|
249
255
|
locations = locations.select{ |l| l['platform'].eql?(platform)}
|
250
256
|
raise "No SDK for platform: #{platform}" if locations.empty?
|
251
257
|
version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
|
@@ -256,7 +262,7 @@ module Aspera
|
|
256
262
|
|
257
263
|
# @param &block called with entry information
|
258
264
|
def extract_archive_files(sdk_archive_path)
|
259
|
-
|
265
|
+
Aspera.assert(block_given?){'missing block'}
|
260
266
|
case sdk_archive_path
|
261
267
|
# Windows and Mac use zip
|
262
268
|
when /\.zip$/
|
@@ -327,21 +333,15 @@ module Aspera
|
|
327
333
|
end
|
328
334
|
end
|
329
335
|
return unless with_exe
|
330
|
-
# ensure
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
EXE_FILES.each do |exe_sym|
|
337
|
-
exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
|
338
|
-
Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
|
336
|
+
# ensure necessary files are there, or generate them
|
337
|
+
SDK_FILES.each do |file_id_sym|
|
338
|
+
file_path = path(file_id_sym)
|
339
|
+
if file_path && EXE_FILES.include?(file_id_sym)
|
340
|
+
Environment.restrict_file_access(file_path, mode: 0o755) if File.exist?(file_path)
|
341
|
+
end
|
339
342
|
end
|
340
|
-
sdk_ascp_version = get_ascp_version(
|
341
|
-
|
342
|
-
Log.log.warn{"No #{transferd_exe_path} in SDK archive"} unless File.exist?(transferd_exe_path)
|
343
|
-
Environment.restrict_file_access(transferd_exe_path, mode: 0o755) if File.exist?(transferd_exe_path)
|
344
|
-
transferd_version = get_exe_version(transferd_exe_path, 'version')
|
343
|
+
sdk_ascp_version = get_ascp_version(path(:ascp))
|
344
|
+
transferd_version = get_exe_version(path(:transferd), 'version')
|
345
345
|
sdk_name = 'IBM Aspera Transfer SDK'
|
346
346
|
sdk_version = transferd_version || sdk_ascp_version
|
347
347
|
File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
|
@@ -359,7 +359,7 @@ module Aspera
|
|
359
359
|
@path_to_ascp = nil
|
360
360
|
@sdk_dir = nil
|
361
361
|
@found_products = nil
|
362
|
-
@transferd_urls =
|
362
|
+
@transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
|
363
363
|
end
|
364
364
|
|
365
365
|
public
|
@@ -184,7 +184,8 @@ module Aspera
|
|
184
184
|
ChunkSize
|
185
185
|
PostTransferValidation
|
186
186
|
OverwritePolicyCap
|
187
|
-
ExtraCreatePolicy
|
187
|
+
ExtraCreatePolicy
|
188
|
+
]
|
188
189
|
# Management port start message
|
189
190
|
MGT_HEADER = 'FASPMGR 2'
|
190
191
|
# empty line is separator to end event information
|
@@ -252,17 +253,17 @@ module Aspera
|
|
252
253
|
# begin event
|
253
254
|
@event_build = {}
|
254
255
|
when /^([^:]+): (.*)$/
|
255
|
-
|
256
|
+
Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: data without header'}
|
256
257
|
# event field
|
257
258
|
@event_build[Regexp.last_match(1)] = Regexp.last_match(2)
|
258
259
|
when MGT_FRAME_SEPARATOR
|
259
|
-
|
260
|
+
Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: end frame without header'}
|
260
261
|
@last_event = @event_build
|
261
262
|
@event_build = nil
|
262
263
|
return @last_event
|
263
264
|
else Aspera.error_unexpected_value(line){'mgt port'}
|
264
265
|
end
|
265
|
-
return
|
266
|
+
return
|
266
267
|
end
|
267
268
|
end
|
268
269
|
end
|