aspera-cli 4.19.0 → 4.20.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 +20 -0
- data/CONTRIBUTING.md +16 -4
- data/README.md +344 -164
- data/bin/asession +26 -19
- data/examples/build_exec +65 -76
- data/examples/build_exec_rubyc +40 -0
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +2 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +23 -24
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +126 -97
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +15 -10
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +99 -42
- data/lib/aspera/ascp/management.rb +3 -2
- data/lib/aspera/ascp/products.rb +12 -0
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +27 -17
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +5 -0
- data/lib/aspera/cli/plugin.rb +15 -29
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +53 -45
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +153 -95
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +5 -9
- data/lib/aspera/cli/plugins/shares.rb +2 -2
- data/lib/aspera/cli/sync_actions.rb +2 -2
- data/lib/aspera/cli/transfer_agent.rb +12 -14
- data/lib/aspera/cli/transfer_progress.rb +35 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +34 -18
- data/lib/aspera/faspex_gw.rb +2 -2
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +3 -4
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +3 -3
- data/lib/aspera/oauth/url_json.rb +1 -2
- data/lib/aspera/oauth/web.rb +5 -2
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/transfer/faux_file.rb +4 -4
- data/lib/aspera/transfer/parameters.rb +14 -16
- data/lib/aspera/transfer/spec.rb +12 -12
- data/lib/aspera/transfer/sync.rb +1 -5
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -17
- data/lib/aspera/web_server_simple.rb +4 -3
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +24 -5
- metadata.gz.sig +0 -0
data/lib/aspera/api/node.rb
CHANGED
@@ -33,6 +33,8 @@ module Aspera
|
|
33
33
|
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
34
34
|
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
35
35
|
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
36
|
+
HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
|
37
|
+
HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
36
38
|
SCOPE_USER = 'user:all'
|
37
39
|
SCOPE_ADMIN = 'admin:all'
|
38
40
|
PATH_SEPARATOR = '/'
|
@@ -42,9 +44,17 @@ module Aspera
|
|
42
44
|
|
43
45
|
# class instance variable, access with accessors on class
|
44
46
|
@use_standard_ports = true
|
47
|
+
@use_node_cache = true
|
45
48
|
|
46
49
|
class << self
|
47
50
|
attr_accessor :use_standard_ports
|
51
|
+
attr_accessor :use_node_cache
|
52
|
+
|
53
|
+
def cache_control_headers
|
54
|
+
h = {'Accept' => 'application/json'}
|
55
|
+
h[HEADER_X_CACHE_CONTROL] = 'no-cache' unless use_node_cache
|
56
|
+
h
|
57
|
+
end
|
48
58
|
|
49
59
|
# For access keys: provide expression to match entry in folder
|
50
60
|
def file_matcher(match_expression)
|
@@ -146,6 +156,11 @@ module Aspera
|
|
146
156
|
end
|
147
157
|
end
|
148
158
|
|
159
|
+
# Call node API, possibly adding cache control header, as globally specified
|
160
|
+
def read_with_cache(subpath, query=nil)
|
161
|
+
return call(operation: 'GET', subpath: subpath, headers: self.class.cache_control_headers, query: query)[:data]
|
162
|
+
end
|
163
|
+
|
149
164
|
# update transfer spec with special additional tags
|
150
165
|
def add_tspec_info(tspec)
|
151
166
|
tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
|
@@ -171,7 +186,7 @@ module Aspera
|
|
171
186
|
def entry_has_link_information(entry)
|
172
187
|
# if target information is missing in folder, try to get it on entry
|
173
188
|
if entry['target_node_id'].nil? || entry['target_id'].nil?
|
174
|
-
link_entry = read("files/#{entry['id']}")
|
189
|
+
link_entry = read("files/#{entry['id']}")
|
175
190
|
entry['target_node_id'] = link_entry['target_node_id']
|
176
191
|
entry['target_id'] = link_entry['target_id']
|
177
192
|
end
|
@@ -200,13 +215,19 @@ module Aspera
|
|
200
215
|
# get folder content
|
201
216
|
folder_contents =
|
202
217
|
begin
|
203
|
-
read("files/#{current_item[:id]}/files")
|
218
|
+
read("files/#{current_item[:id]}/files")
|
204
219
|
rescue StandardError => e
|
205
220
|
Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
|
206
221
|
[]
|
207
222
|
end
|
208
223
|
Log.log.debug{Log.dump(:folder_contents, folder_contents)}
|
209
224
|
folder_contents.each do |entry|
|
225
|
+
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
|
229
|
+
next
|
230
|
+
end
|
210
231
|
relative_path = File.join(current_item[:path], entry['name'])
|
211
232
|
Log.log.debug{"process_folder_tree: checking #{relative_path}"}
|
212
233
|
# call block, continue only if method returns true
|
@@ -228,16 +249,16 @@ module Aspera
|
|
228
249
|
end
|
229
250
|
end
|
230
251
|
|
231
|
-
# Navigate the path from given file id
|
252
|
+
# Navigate the path from given file id on current node, and return the node and file id of target.
|
253
|
+
# If the path ends with a "/" or process_last_link is true then if the last item in path is a link, it is followed.
|
232
254
|
# @param top_file_id [String] id initial file id
|
233
|
-
# @param path [String]
|
255
|
+
# @param path [String] file or folder path (end with "/" is like setting process_last_link)
|
256
|
+
# @param process_last_link [Boolean] if true, follow the last link
|
234
257
|
# @return [Hash] {.api,.file_id}
|
235
|
-
def resolve_api_fid(top_file_id, path)
|
258
|
+
def resolve_api_fid(top_file_id, path, process_last_link=false)
|
236
259
|
Aspera.assert_type(top_file_id, String)
|
237
260
|
Aspera.assert_type(path, String)
|
238
|
-
|
239
|
-
process_last_link = path.end_with?(PATH_SEPARATOR)
|
240
|
-
# keep only non-empty elements
|
261
|
+
process_last_link ||= path.end_with?(PATH_SEPARATOR)
|
241
262
|
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
242
263
|
return {api: self, file_id: top_file_id} if path_elements.empty?
|
243
264
|
resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
|
@@ -265,7 +286,7 @@ module Aspera
|
|
265
286
|
full_spec = create(
|
266
287
|
'files/download_setup',
|
267
288
|
{transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
|
268
|
-
)[
|
289
|
+
)['transfer_specs'].first['transfer_spec']
|
269
290
|
# set available fields
|
270
291
|
@std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
|
271
292
|
h[i] = full_spec[i] if full_spec.key?(i)
|
@@ -317,13 +338,13 @@ module Aspera
|
|
317
338
|
if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
|
318
339
|
transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
|
319
340
|
end
|
320
|
-
info = read('info')
|
341
|
+
info = read('info')
|
321
342
|
# get the transfer user from info on access key
|
322
343
|
transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
|
323
344
|
# get settings from name.value array to hash key.value
|
324
345
|
settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
|
325
346
|
# check WSS ports
|
326
|
-
|
347
|
+
Transfer::Spec::WSS_FIELDS.each do |i|
|
327
348
|
transfer_spec[i] = settings[i] if settings.key?(i)
|
328
349
|
end if settings.is_a?(Hash)
|
329
350
|
else
|
@@ -380,7 +401,7 @@ module Aspera
|
|
380
401
|
return true
|
381
402
|
end
|
382
403
|
|
383
|
-
def process_find_files(entry,
|
404
|
+
def process_find_files(entry, path, state)
|
384
405
|
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
385
406
|
# test all files deeply
|
386
407
|
return true
|
data/lib/aspera/ascmd.rb
CHANGED
@@ -22,7 +22,54 @@ module Aspera
|
|
22
22
|
mv: 2,
|
23
23
|
rm: 1
|
24
24
|
}.freeze
|
25
|
-
|
25
|
+
|
26
|
+
# protocol is based on Type-Length-Value
|
27
|
+
# type start at one, but array index start at zero
|
28
|
+
ENUM_START = 1
|
29
|
+
|
30
|
+
# description of result structures (see ascmdtypes.h).
|
31
|
+
# Base types are big endian
|
32
|
+
# key = name of type
|
33
|
+
# index in array `fields` is the type (minus ENUM_START)
|
34
|
+
# decoding always start at `result`
|
35
|
+
# some fields have special handling indicated by `special`
|
36
|
+
# field_list, list_tlv_list, list_tlv_restart are composed with a list of TLV
|
37
|
+
TYPES_DESCR = {
|
38
|
+
result: {decode: :field_list,
|
39
|
+
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},
|
40
|
+
{name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
|
41
|
+
{name: :df, is_a: :mnt, special: :list_tlv_restart}, {name: :md5sum, is_a: :md5sum}]},
|
42
|
+
stat: {decode: :field_list,
|
43
|
+
fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
|
44
|
+
{name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
|
45
|
+
{name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
|
46
|
+
{name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
|
47
|
+
{name: :errstr, is_a: :zstr}]},
|
48
|
+
info: {decode: :field_list,
|
49
|
+
fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
|
50
|
+
{name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
|
51
|
+
{name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :list_multiple}, {name: :browse_caps, is_a: :zstr},
|
52
|
+
{name: :protocol, is_a: :zstr}]},
|
53
|
+
size: {decode: :field_list,
|
54
|
+
fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
|
55
|
+
{name: :failed_dcount, is_a: :int32}]},
|
56
|
+
error: {decode: :field_list,
|
57
|
+
fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
|
58
|
+
mnt: {decode: :field_list,
|
59
|
+
fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
|
60
|
+
{name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
|
61
|
+
{name: :errstr, is_a: :zstr}]},
|
62
|
+
md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
|
63
|
+
int8: {decode: :base, unpack: 'C', size: 1},
|
64
|
+
int32: {decode: :base, unpack: 'L>', size: 4},
|
65
|
+
int64: {decode: :base, unpack: 'Q>', size: 8},
|
66
|
+
epoch: {decode: :base, unpack: 'Q>', size: 8},
|
67
|
+
zstr: {decode: :base, unpack: 'Z*'},
|
68
|
+
blist: {decode: :buffer_list}
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
private_constant :TYPES_DESCR, :ENUM_START, :OPS_ARGS
|
72
|
+
|
26
73
|
# list of supported actions
|
27
74
|
OPERATIONS = OPS_ARGS.keys.freeze
|
28
75
|
|
@@ -55,6 +102,7 @@ module Aspera
|
|
55
102
|
arg_batches.each do |args|
|
56
103
|
command = [main_command]
|
57
104
|
# enclose arguments in double quotes, protect backslash and double quotes
|
105
|
+
# ascmd uses space as token separator, and optional quotes ('") or \ to escape
|
58
106
|
args.each do |v|
|
59
107
|
command.push(%Q{"#{v.gsub(/["\\]/){|s|"\\#{s}"}}"})
|
60
108
|
end
|
@@ -106,47 +154,6 @@ module Aspera
|
|
106
154
|
def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
|
107
155
|
end
|
108
156
|
|
109
|
-
# description of result structures (see ascmdtypes.h). Base types are big endian
|
110
|
-
# key = name of type
|
111
|
-
TYPES_DESCR = {
|
112
|
-
result: {decode: :field_list,
|
113
|
-
fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :sub_struct}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
|
114
|
-
{name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
|
115
|
-
{name: :df, is_a: :mnt, special: :restart_on_first}, {name: :md5sum, is_a: :md5sum}]},
|
116
|
-
stat: {decode: :field_list,
|
117
|
-
fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
|
118
|
-
{name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
|
119
|
-
{name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
|
120
|
-
{name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
|
121
|
-
{name: :errstr, is_a: :zstr}]},
|
122
|
-
info: {decode: :field_list,
|
123
|
-
fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
|
124
|
-
{name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
|
125
|
-
{name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :multiple}, {name: :browse_caps, is_a: :zstr},
|
126
|
-
{name: :protocol, is_a: :zstr}]},
|
127
|
-
size: {decode: :field_list,
|
128
|
-
fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
|
129
|
-
{name: :failed_dcount, is_a: :int32}]},
|
130
|
-
error: {decode: :field_list,
|
131
|
-
fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
|
132
|
-
mnt: {decode: :field_list,
|
133
|
-
fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
|
134
|
-
{name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
|
135
|
-
{name: :errstr, is_a: :zstr}]},
|
136
|
-
md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
|
137
|
-
int8: {decode: :base, unpack: 'C', size: 1},
|
138
|
-
int32: {decode: :base, unpack: 'L>', size: 4},
|
139
|
-
int64: {decode: :base, unpack: 'Q>', size: 8},
|
140
|
-
epoch: {decode: :base, unpack: 'Q>', size: 8},
|
141
|
-
zstr: {decode: :base, unpack: 'Z*'},
|
142
|
-
blist: {decode: :buffer_list}
|
143
|
-
}.freeze
|
144
|
-
|
145
|
-
# protocol enum start at one, but array index start at zero
|
146
|
-
ENUM_START = 1
|
147
|
-
|
148
|
-
private_constant :TYPES_DESCR, :ENUM_START
|
149
|
-
|
150
157
|
class << self
|
151
158
|
# get description of structure's field, @param struct_name, @param typed_buffer provides field name
|
152
159
|
def field_description(struct_name, typed_buffer)
|
@@ -175,6 +182,7 @@ module Aspera
|
|
175
182
|
Log.log.trace1{"#{' .' * indent_level}-> base:#{byte_array} -> #{result}"}
|
176
183
|
result = Time.at(result) if type_name.eql?(:epoch)
|
177
184
|
when :buffer_list
|
185
|
+
# return a list of type_buffer
|
178
186
|
result = []
|
179
187
|
until buffer.empty?
|
180
188
|
btype = parse(buffer, :int8, indent_level)
|
@@ -193,16 +201,16 @@ module Aspera
|
|
193
201
|
field_info = field_description(type_name, typed_buffer)
|
194
202
|
Log.log.trace1{"#{' .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green}
|
195
203
|
case field_info[:special]
|
196
|
-
when nil
|
204
|
+
when nil # normal case
|
197
205
|
result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
|
198
|
-
when :return_true
|
206
|
+
when :return_true # nothing to parse, just return true
|
199
207
|
result[field_info[:name]] = true
|
200
|
-
when :
|
201
|
-
result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
|
202
|
-
when :multiple
|
208
|
+
when :list_multiple # field appears multiple times, and is an array of values (base type)
|
203
209
|
result[field_info[:name]] ||= []
|
204
210
|
result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
|
205
|
-
when :
|
211
|
+
when :list_tlv_list # field is an array of values in a list of buffers
|
212
|
+
result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
|
213
|
+
when :list_tlv_restart # field is an array of values, but a new value is started on index 1
|
206
214
|
fl = result[field_info[:name]] = []
|
207
215
|
parse(typed_buffer[:buffer], :blist, indent_level).map do |tb|
|
208
216
|
fl.push({}) if tb[:btype].eql?(ENUM_START)
|
@@ -5,16 +5,16 @@ require 'aspera/environment'
|
|
5
5
|
require 'aspera/data_repository'
|
6
6
|
require 'aspera/ascp/products'
|
7
7
|
require 'aspera/log'
|
8
|
+
require 'aspera/rest'
|
8
9
|
require 'aspera/assert'
|
9
10
|
require 'aspera/web_server_simple'
|
10
11
|
require 'English'
|
11
12
|
require 'singleton'
|
12
13
|
require 'xmlsimple'
|
13
|
-
require 'zlib'
|
14
14
|
require 'base64'
|
15
15
|
require 'fileutils'
|
16
16
|
require 'openssl'
|
17
|
-
|
17
|
+
require 'yaml'
|
18
18
|
module Aspera
|
19
19
|
module Ascp
|
20
20
|
# Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
|
@@ -28,7 +28,7 @@ module Aspera
|
|
28
28
|
include Singleton
|
29
29
|
# protobuf generated files from sdk
|
30
30
|
EXT_RUBY_PROTOBUF = '_pb.rb'
|
31
|
-
|
31
|
+
RB_SDK_SUBFOLDER = 'lib'
|
32
32
|
DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
|
33
33
|
<?xml version='1.0' encoding='UTF-8'?>
|
34
34
|
<CONF version="2">
|
@@ -43,9 +43,13 @@ module Aspera
|
|
43
43
|
# all ascp files (in SDK)
|
44
44
|
EXE_FILES = %i[ascp ascp4 async].freeze
|
45
45
|
FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
|
46
|
-
|
46
|
+
TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
|
47
|
+
FILE_SCHEME_PREFIX = 'file:///'
|
48
|
+
SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
|
49
|
+
private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
|
47
50
|
# options for SSH client private key
|
48
51
|
CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
|
52
|
+
|
49
53
|
# set ascp executable path
|
50
54
|
def ascp_path=(v)
|
51
55
|
@path_to_ascp = v
|
@@ -55,12 +59,6 @@ module Aspera
|
|
55
59
|
path(:ascp)
|
56
60
|
end
|
57
61
|
|
58
|
-
def sdk_ruby_folder
|
59
|
-
ruby_pb_folder = File.join(sdk_folder, RB_SDK_FOLDER)
|
60
|
-
FileUtils.mkdir_p(ruby_pb_folder)
|
61
|
-
return ruby_pb_folder
|
62
|
-
end
|
63
|
-
|
64
62
|
# location of SDK files
|
65
63
|
def sdk_folder=(v)
|
66
64
|
Log.log.debug{"sdk_folder=#{v}"}
|
@@ -83,7 +81,7 @@ module Aspera
|
|
83
81
|
def use_ascp_from_product(product_name)
|
84
82
|
if product_name.eql?(FIRST_FOUND)
|
85
83
|
pl = Products.installed_products.first
|
86
|
-
raise "no
|
84
|
+
raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
|
87
85
|
else
|
88
86
|
pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
|
89
87
|
raise "no such product installed: #{product_name}" if pl.nil?
|
@@ -177,7 +175,7 @@ module Aspera
|
|
177
175
|
return nil unless File.exist?(exe_path)
|
178
176
|
exe_version = nil
|
179
177
|
cmd_out = %x("#{exe_path}" #{vers_arg})
|
180
|
-
raise "An error occurred when testing #{
|
178
|
+
raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
|
181
179
|
# get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
|
182
180
|
m = cmd_out.match(/ version ([0-9.]+)/)
|
183
181
|
exe_version = m[1].gsub(/\.$/, '') unless m.nil?
|
@@ -229,54 +227,113 @@ module Aspera
|
|
229
227
|
return data
|
230
228
|
end
|
231
229
|
|
230
|
+
# information for `ascp info`
|
232
231
|
def ascp_info
|
233
|
-
|
234
|
-
|
232
|
+
ascp_data = file_paths
|
233
|
+
ascp_data.merge!(ascp_pvcl_info)
|
234
|
+
ascp_data['sdk_locations'] = TRANSFER_SDK_LOCATION_URL
|
235
|
+
ascp_data.merge!(ascp_ssl_info)
|
236
|
+
return ascp_data
|
237
|
+
end
|
238
|
+
|
239
|
+
# Loads YAML from cloud with locations of SDK archives for all platforms
|
240
|
+
# @return location structure
|
241
|
+
def sdk_locations
|
242
|
+
yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
|
243
|
+
YAML.load(yaml_text)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @return the url for download of SDK archive for the given platform and version
|
247
|
+
def sdk_url_for_platform(platform: nil, version: nil)
|
248
|
+
locations = sdk_locations
|
249
|
+
platform = Environment.architecture if platform.nil?
|
250
|
+
locations = locations.select{|l|l['platform'].eql?(platform)}
|
251
|
+
raise "No SDK for platform: #{platform}" if locations.empty?
|
252
|
+
version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
|
253
|
+
info = locations.select{|entry| entry['version'].eql?(version)}
|
254
|
+
raise "No such version: #{version} for #{platform}" if info.empty?
|
255
|
+
return info.first['url']
|
256
|
+
end
|
257
|
+
|
258
|
+
def extract_archive_files(sdk_archive_path)
|
259
|
+
raise 'missing block' unless block_given?
|
260
|
+
case sdk_archive_path
|
261
|
+
# Windows and Mac use zip
|
262
|
+
when /\.zip$/
|
263
|
+
require 'zip'
|
264
|
+
# extract files from archive
|
265
|
+
Zip::File.open(sdk_archive_path) do |zip_file|
|
266
|
+
zip_file.each do |entry|
|
267
|
+
next if entry.name.end_with?('/')
|
268
|
+
yield(entry.name, entry.get_input_stream)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
# Other Unixes use tar.gz
|
272
|
+
when /\.tar\.gz/
|
273
|
+
require 'zlib'
|
274
|
+
require 'rubygems/package'
|
275
|
+
Zlib::GzipReader.open(sdk_archive_path) do |gzip|
|
276
|
+
Gem::Package::TarReader.new(gzip) do |tar|
|
277
|
+
tar.each do |entry|
|
278
|
+
next if entry.directory?
|
279
|
+
yield(entry.full_name, entry)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
else
|
284
|
+
raise "unknown archive extension: #{sdk_archive_path}"
|
285
|
+
end
|
235
286
|
end
|
236
287
|
|
237
288
|
# download aspera SDK or use local file
|
238
289
|
# extracts ascp binary for current system architecture
|
290
|
+
# @param url [String] URL to SDK archive, or SpecialValues::DEF
|
239
291
|
# @return ascp version (from execution)
|
240
|
-
def install_sdk(
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
292
|
+
def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
|
293
|
+
url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
|
294
|
+
folder = sdk_folder if folder.nil?
|
295
|
+
subfolder_lambda = block
|
296
|
+
if subfolder_lambda.nil?
|
297
|
+
subfolder_lambda = ->(name) do
|
298
|
+
if SDK_ARCHIVE_FOLDERS.any?{|i|name.include?(i)}
|
299
|
+
'/'
|
300
|
+
elsif name.end_with?(EXT_RUBY_PROTOBUF)
|
301
|
+
RB_SDK_SUBFOLDER
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
if url.start_with?('file:')
|
246
306
|
# require specific file scheme: the path part is "relative", or absolute if there are 4 slash
|
247
|
-
raise 'use format: file:///<path>' unless
|
248
|
-
|
307
|
+
raise 'use format: file:///<path>' unless url.start_with?(FILE_SCHEME_PREFIX)
|
308
|
+
sdk_archive_path = url[FILE_SCHEME_PREFIX.length..-1]
|
309
|
+
delete_archive = false
|
249
310
|
else
|
250
|
-
|
311
|
+
sdk_archive_path = File.join(Dir.tmpdir, File.basename(url))
|
312
|
+
Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
|
313
|
+
delete_archive = true
|
251
314
|
end
|
252
315
|
# rename old install
|
253
|
-
if !Dir.empty?(
|
316
|
+
if backup && !Dir.empty?(folder)
|
254
317
|
Log.log.warn('Previous install exists, renaming folder.')
|
255
|
-
File.rename(
|
318
|
+
File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
|
256
319
|
# TODO: delete old archives ?
|
257
320
|
end
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
dest_folder = sdk_folder if entry.name.include?(arch_filter)
|
266
|
-
# ruby adapters
|
267
|
-
dest_folder = sdk_ruby_folder if entry.name.end_with?(EXT_RUBY_PROTOBUF)
|
268
|
-
next if dest_folder.nil?
|
269
|
-
File.open(File.join(dest_folder, File.basename(entry.name)), 'wb') do |output_stream|
|
270
|
-
IO.copy_stream(entry.get_input_stream, output_stream)
|
271
|
-
end
|
321
|
+
extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
|
322
|
+
subfolder = subfolder_lambda.call(entry_name)
|
323
|
+
next if subfolder.nil?
|
324
|
+
dest_folder = File.join(folder, subfolder)
|
325
|
+
FileUtils.mkdir_p(dest_folder)
|
326
|
+
File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
|
327
|
+
IO.copy_stream(entry_stream, output_stream)
|
272
328
|
end
|
273
329
|
end
|
274
|
-
File.unlink(
|
330
|
+
File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
|
331
|
+
return unless with_exe
|
275
332
|
# ensure license file are generated so that ascp invocation for version works
|
276
333
|
path(:aspera_license)
|
277
334
|
path(:aspera_conf)
|
278
335
|
sdk_ascp_file = Products.ascp_filename
|
279
|
-
sdk_ascp_path = File.join(
|
336
|
+
sdk_ascp_path = File.join(folder, sdk_ascp_file)
|
280
337
|
raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
|
281
338
|
EXE_FILES.each do |exe_sym|
|
282
339
|
exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
|
@@ -289,7 +346,7 @@ module Aspera
|
|
289
346
|
transferd_version = get_exe_version(sdk_daemon_path, 'version')
|
290
347
|
sdk_name = 'IBM Aspera Transfer SDK'
|
291
348
|
sdk_version = transferd_version || sdk_ascp_version
|
292
|
-
File.write(File.join(
|
349
|
+
File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
|
293
350
|
return sdk_name, sdk_version
|
294
351
|
end
|
295
352
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/assert'
|
4
|
+
|
3
5
|
module Aspera
|
4
6
|
module Ascp
|
5
7
|
# processing of ascp management port events
|
@@ -237,8 +239,7 @@ module Aspera
|
|
237
239
|
@last_event = @event_build
|
238
240
|
@event_build = nil
|
239
241
|
return @last_event
|
240
|
-
else
|
241
|
-
raise "mgt port: unexpected line: [#{line}]"
|
242
|
+
else Aspera.error_unexpected_value(line){'mgt port'}
|
242
243
|
end
|
243
244
|
return nil
|
244
245
|
end
|
data/lib/aspera/ascp/products.rb
CHANGED
@@ -56,6 +56,18 @@ module Aspera
|
|
56
56
|
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
57
57
|
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
58
58
|
sub_bin: File.join('Contents', 'Resources')
|
59
|
+
}, {
|
60
|
+
expected: CONNECT,
|
61
|
+
app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
|
62
|
+
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
63
|
+
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
64
|
+
sub_bin: File.join('Contents', 'Resources')
|
65
|
+
}, {
|
66
|
+
expected: CONNECT,
|
67
|
+
app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
|
68
|
+
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
69
|
+
run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
|
70
|
+
sub_bin: File.join('Contents', 'Resources')
|
59
71
|
}, {
|
60
72
|
expected: CLI_V1,
|
61
73
|
app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
|
data/lib/aspera/assert.rb
CHANGED
@@ -8,13 +8,13 @@ module Aspera
|
|
8
8
|
end
|
9
9
|
class << self
|
10
10
|
# the block is executed in the context of the Aspera module
|
11
|
-
def assert(assertion, info = nil,
|
11
|
+
def assert(assertion, info = nil, exception_class: AssertError)
|
12
12
|
raise InternalError, 'bad assert: both info and block given' unless info.nil? || !block_given?
|
13
13
|
return if assertion
|
14
14
|
message = 'assertion failed'
|
15
15
|
info = yield if block_given?
|
16
16
|
message = "#{message}: #{info}" if info
|
17
|
-
message = "#{message}: #{caller(
|
17
|
+
message = "#{message}: #{caller.find{|call|!call.start_with?(__FILE__)}}"
|
18
18
|
raise exception_class, message
|
19
19
|
end
|
20
20
|
|
@@ -22,13 +22,18 @@ module Aspera
|
|
22
22
|
# @param value [Object] the value to check
|
23
23
|
# @param type [Class] the expected type
|
24
24
|
def assert_type(value, type, exception_class: AssertError)
|
25
|
-
assert(value.is_a?(type),
|
25
|
+
assert(value.is_a?(type), exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
|
26
26
|
end
|
27
27
|
|
28
28
|
# assert that value is one of the given values
|
29
|
+
# @param value value to check
|
30
|
+
# @param values accepted values
|
31
|
+
# @param exception_class exception in case of no match
|
29
32
|
def assert_values(value, values, exception_class: AssertError)
|
30
|
-
assert(values.include?(value),
|
31
|
-
|
33
|
+
assert(values.include?(value), exception_class: exception_class) do
|
34
|
+
val_list = values.inspect
|
35
|
+
val_list = "one of #{val_list}" if values.is_a?(Array)
|
36
|
+
"#{block_given? ? "#{yield}: " : nil}expecting #{val_list}, but have #{value.inspect}"
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|