aspera-cli 4.17.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +3 -4
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +15 -1
- data/README.md +711 -432
- data/bin/ascli +5 -0
- data/bin/asession +2 -2
- data/examples/build_package.sh +28 -0
- data/lib/aspera/agent/alpha.rb +10 -8
- data/lib/aspera/agent/base.rb +9 -6
- data/lib/aspera/agent/connect.rb +7 -8
- data/lib/aspera/agent/direct.rb +56 -37
- data/lib/aspera/agent/httpgw.rb +23 -324
- data/lib/aspera/agent/node.rb +19 -20
- data/lib/aspera/agent/trsdk.rb +19 -20
- data/lib/aspera/api/aoc.rb +17 -14
- data/lib/aspera/api/cos_node.rb +4 -4
- data/lib/aspera/api/httpgw.rb +342 -0
- data/lib/aspera/api/node.rb +135 -89
- data/lib/aspera/ascmd.rb +4 -3
- data/lib/aspera/ascp/installation.rb +15 -7
- data/lib/aspera/ascp/management.rb +2 -2
- data/lib/aspera/ascp/products.rb +1 -1
- data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
- data/lib/aspera/cli/extended_value.rb +35 -16
- data/lib/aspera/cli/formatter.rb +161 -70
- data/lib/aspera/cli/hints.rb +18 -0
- data/lib/aspera/cli/main.rb +32 -39
- data/lib/aspera/cli/manager.rb +151 -119
- data/lib/aspera/cli/plugin.rb +27 -21
- data/lib/aspera/cli/plugin_factory.rb +31 -20
- data/lib/aspera/cli/plugins/alee.rb +14 -2
- data/lib/aspera/cli/plugins/aoc.rb +152 -141
- data/lib/aspera/cli/plugins/ats.rb +1 -1
- data/lib/aspera/cli/plugins/config.rb +72 -65
- data/lib/aspera/cli/plugins/console.rb +8 -5
- data/lib/aspera/cli/plugins/faspex.rb +32 -23
- data/lib/aspera/cli/plugins/faspex5.rb +232 -156
- data/lib/aspera/cli/plugins/faspio.rb +85 -0
- data/lib/aspera/cli/plugins/httpgw.rb +55 -0
- data/lib/aspera/cli/plugins/node.rb +129 -64
- data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
- data/lib/aspera/cli/plugins/preview.rb +7 -3
- data/lib/aspera/cli/plugins/server.rb +6 -6
- data/lib/aspera/cli/plugins/shares.rb +16 -14
- data/lib/aspera/cli/special_values.rb +13 -0
- data/lib/aspera/cli/sync_actions.rb +10 -10
- data/lib/aspera/cli/transfer_agent.rb +7 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +70 -9
- data/lib/aspera/faspex_gw.rb +5 -4
- data/lib/aspera/faspex_postproc.rb +2 -2
- data/lib/aspera/log.rb +6 -3
- data/lib/aspera/node_simulator.rb +2 -2
- data/lib/aspera/oauth/base.rb +31 -19
- data/lib/aspera/oauth/factory.rb +12 -13
- data/lib/aspera/oauth/generic.rb +1 -0
- data/lib/aspera/oauth/jwt.rb +18 -15
- data/lib/aspera/oauth/url_json.rb +8 -6
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/options.rb +3 -3
- data/lib/aspera/preview/terminal.rb +4 -4
- data/lib/aspera/preview/utils.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +5 -1
- data/lib/aspera/rest.rb +105 -88
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +2 -2
- data/lib/aspera/rest_errors_aspera.rb +1 -1
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +2 -4
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +39 -36
- data/lib/aspera/transfer/spec.rb +2 -0
- data/lib/aspera/transfer/sync.rb +2 -1
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +5 -4
- data/lib/aspera/web_auth.rb +1 -1
- data/lib/aspera/web_server_simple.rb +4 -3
- data.tar.gz.sig +0 -0
- metadata +7 -4
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/plugins/bss.rb +0 -71
- data/lib/aspera/open_application.rb +0 -71
data/lib/aspera/api/node.rb
CHANGED
|
@@ -14,21 +14,28 @@ module Aspera
|
|
|
14
14
|
module Api
|
|
15
15
|
# Provides additional functions using node API with gen4 extensions (access keys)
|
|
16
16
|
class Node < Aspera::Rest
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
SCOPE_SEPARATOR = ':'
|
|
18
|
+
SCOPE_NODE_PREFIX = 'node.'
|
|
19
19
|
# prefix for ruby code for filter (deprecated)
|
|
20
20
|
MATCH_EXEC_PREFIX = 'exec:'
|
|
21
21
|
MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
|
|
22
|
+
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
23
|
+
BEARER_TOKEN_VALIDITY_DEFAULT = 86400
|
|
24
|
+
# fields in @app_info
|
|
25
|
+
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
26
|
+
# methods of @app_info[:api]
|
|
27
|
+
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
28
|
+
private_constant :SCOPE_SEPARATOR, :SCOPE_NODE_PREFIX, :MATCH_EXEC_PREFIX, :MATCH_TYPES,
|
|
29
|
+
:SIGNATURE_DELIMITER, :BEARER_TOKEN_VALIDITY_DEFAULT,
|
|
30
|
+
:REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
31
|
+
|
|
32
|
+
# node api permissions
|
|
33
|
+
ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
|
|
22
34
|
HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
|
|
23
|
-
|
|
24
|
-
TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
|
|
35
|
+
HEADER_X_TOTAL_COUNT = 'X-Total-Count'
|
|
25
36
|
SCOPE_USER = 'user:all'
|
|
26
37
|
SCOPE_ADMIN = 'admin:all'
|
|
27
|
-
|
|
28
|
-
SCOPE_SEPARATOR = ':'
|
|
29
|
-
SIGNATURE_DELIMITER = '==SIGNATURE=='
|
|
30
|
-
BEARER_TOKEN_VALIDITY_DEFAULT = 86400
|
|
31
|
-
BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
|
|
38
|
+
PATH_SEPARATOR = '/'
|
|
32
39
|
|
|
33
40
|
# register node special token decoder
|
|
34
41
|
OAuth::Factory.instance.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
|
|
@@ -57,19 +64,19 @@ module Aspera
|
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
def file_matcher_from_argument(options)
|
|
60
|
-
return file_matcher(options.get_next_argument('filter',
|
|
67
|
+
return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
# node API scopes
|
|
64
71
|
def token_scope(access_key, scope)
|
|
65
|
-
return [
|
|
72
|
+
return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
|
|
66
73
|
end
|
|
67
74
|
|
|
68
75
|
def decode_scope(scope)
|
|
69
76
|
items = scope.split(SCOPE_SEPARATOR, 2)
|
|
70
77
|
Aspera.assert(items.length.eql?(2)){"invalid scope: #{scope}"}
|
|
71
|
-
Aspera.assert(items[0].start_with?(
|
|
72
|
-
return {access_key: items[0][
|
|
78
|
+
Aspera.assert(items[0].start_with?(SCOPE_NODE_PREFIX)){"invalid scope: #{scope}"}
|
|
79
|
+
return {access_key: items[0][SCOPE_NODE_PREFIX.length..-1], scope: items[1]}
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
# Create an Aspera Node bearer token
|
|
@@ -84,7 +91,7 @@ module Aspera
|
|
|
84
91
|
# manage convenience parameters
|
|
85
92
|
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
|
86
93
|
payload.delete('_validity')
|
|
87
|
-
scope = payload['_scope'] ||
|
|
94
|
+
scope = payload['_scope'] || SCOPE_USER
|
|
88
95
|
payload.delete('_scope')
|
|
89
96
|
payload['scope'] ||= token_scope(access_key, scope)
|
|
90
97
|
payload['auth_type'] ||= 'access_key'
|
|
@@ -115,25 +122,20 @@ module Aspera
|
|
|
115
122
|
end
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
# fields in @app_info
|
|
119
|
-
REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
|
|
120
|
-
# methods of @app_info[:api]
|
|
121
|
-
REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
|
|
122
|
-
private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
|
|
123
|
-
|
|
124
125
|
attr_reader :app_info
|
|
125
126
|
|
|
127
|
+
# @param app_info [Hash,NilClass] Special processing for AoC
|
|
128
|
+
# @param add_tspec [Hash,NilClass] Additional transfer spec
|
|
126
129
|
# @param base_url [String] Rest parameters
|
|
127
130
|
# @param auth [String,NilClass] Rest parameters
|
|
128
131
|
# @param headers [String,NilClass] Rest parameters
|
|
129
|
-
# @param app_info [Hash,NilClass] Special processing for AoC
|
|
130
|
-
# @param add_tspec [Hash,NilClass] Additional transfer spec
|
|
131
132
|
def initialize(app_info: nil, add_tspec: nil, **rest_args)
|
|
132
133
|
# init Rest
|
|
133
134
|
super(**rest_args)
|
|
134
135
|
@app_info = app_info
|
|
135
136
|
# this is added to transfer spec, for instance to add tags (COS)
|
|
136
137
|
@add_tspec = add_tspec
|
|
138
|
+
@std_t_spec_cache = nil
|
|
137
139
|
if !@app_info.nil?
|
|
138
140
|
REQUIRED_APP_INFO_FIELDS.each do |field|
|
|
139
141
|
Aspera.assert(@app_info.key?(field)){"app_info lacks field #{field}"}
|
|
@@ -159,25 +161,42 @@ module Aspera
|
|
|
159
161
|
workspace_id: @app_info[:workspace_id],
|
|
160
162
|
workspace_name: @app_info[:workspace_name])
|
|
161
163
|
end
|
|
162
|
-
Log.log.warn{"
|
|
164
|
+
Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
|
|
163
165
|
return nil
|
|
164
166
|
end
|
|
165
167
|
|
|
168
|
+
# Check if a link entry in folder has target information
|
|
169
|
+
# @param entry [Hash] entry in folder
|
|
170
|
+
# @return [Boolean] true if target information is available
|
|
171
|
+
def entry_has_link_information(entry)
|
|
172
|
+
# if target information is missing in folder, try to get it on entry
|
|
173
|
+
if entry['target_node_id'].nil? || entry['target_id'].nil?
|
|
174
|
+
link_entry = read("files/#{entry['id']}")[:data]
|
|
175
|
+
entry['target_node_id'] = link_entry['target_node_id']
|
|
176
|
+
entry['target_id'] = link_entry['target_id']
|
|
177
|
+
end
|
|
178
|
+
return true unless entry['target_node_id'].nil? || entry['target_id'].nil?
|
|
179
|
+
Log.log.warn{"Missing target information for link: #{entry['name']}"}
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
|
|
166
183
|
# Recursively browse in a folder (with non-recursive method)
|
|
167
184
|
# sub folders are processed if the processing method returns true
|
|
185
|
+
# links are processed on the respective node
|
|
168
186
|
# @param state [Object] state object sent to processing method
|
|
169
187
|
# @param top_file_id [String] file id to start at (default = access key root file id)
|
|
170
188
|
# @param top_file_path [String] path of top folder (default = /)
|
|
171
189
|
# @param block [Proc] processing method, arguments: entry, path, state
|
|
172
|
-
def process_folder_tree(state:, top_file_id:, top_file_path: '/'
|
|
190
|
+
def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
|
|
173
191
|
Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
|
|
174
|
-
|
|
192
|
+
Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
|
|
175
193
|
# start at top folder
|
|
176
194
|
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
|
177
195
|
Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
|
|
178
196
|
until folders_to_explore.empty?
|
|
197
|
+
# consume first in job list
|
|
179
198
|
current_item = folders_to_explore.shift
|
|
180
|
-
Log.log.debug{"
|
|
199
|
+
Log.log.debug{"Exploring #{current_item[:path]}".bg_green}
|
|
181
200
|
# get folder content
|
|
182
201
|
folder_contents =
|
|
183
202
|
begin
|
|
@@ -189,23 +208,25 @@ module Aspera
|
|
|
189
208
|
Log.log.debug{Log.dump(:folder_contents, folder_contents)}
|
|
190
209
|
folder_contents.each do |entry|
|
|
191
210
|
relative_path = File.join(current_item[:path], entry['name'])
|
|
192
|
-
Log.log.debug{"process_folder_tree checking #{relative_path}"}
|
|
193
|
-
# continue only if method returns true
|
|
194
|
-
next unless
|
|
211
|
+
Log.log.debug{"process_folder_tree: checking #{relative_path}"}
|
|
212
|
+
# call block, continue only if method returns true
|
|
213
|
+
next unless send(method_sym, entry, relative_path, state)
|
|
195
214
|
# entry type is file, folder or link
|
|
196
215
|
case entry['type']
|
|
197
216
|
when 'folder'
|
|
198
217
|
folders_to_explore.push({id: entry['id'], path: relative_path})
|
|
199
218
|
when 'link'
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
if entry_has_link_information(entry)
|
|
220
|
+
node_id_to_node(entry['target_node_id'])&.process_folder_tree(
|
|
221
|
+
method_sym: method_sym,
|
|
222
|
+
state: state,
|
|
223
|
+
top_file_id: entry['target_id'],
|
|
224
|
+
top_file_path: relative_path)
|
|
225
|
+
end
|
|
205
226
|
end
|
|
206
227
|
end
|
|
207
228
|
end
|
|
208
|
-
end
|
|
229
|
+
end
|
|
209
230
|
|
|
210
231
|
# Navigate the path from given file id
|
|
211
232
|
# @param top_file_id [String] id initial file id
|
|
@@ -213,65 +234,44 @@ module Aspera
|
|
|
213
234
|
# @return [Hash] {.api,.file_id}
|
|
214
235
|
def resolve_api_fid(top_file_id, path)
|
|
215
236
|
Aspera.assert_type(top_file_id, String)
|
|
237
|
+
Aspera.assert_type(path, String)
|
|
238
|
+
# if last element is a link and followed by "/", we list the content of that folder, else we return the link
|
|
216
239
|
process_last_link = path.end_with?(PATH_SEPARATOR)
|
|
240
|
+
# keep only non-empty elements
|
|
217
241
|
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
|
218
242
|
return {api: self, file_id: top_file_id} if path_elements.empty?
|
|
219
|
-
resolve_state = {path: path_elements, result: nil}
|
|
220
|
-
process_folder_tree(state: resolve_state, top_file_id: top_file_id)
|
|
221
|
-
# this block is called recursively for each entry in folder
|
|
222
|
-
# stop digging here if not in right path
|
|
223
|
-
next false unless entry['name'].eql?(state[:path].first)
|
|
224
|
-
# ok it matches, so we remove the match
|
|
225
|
-
state[:path].shift
|
|
226
|
-
case entry['type']
|
|
227
|
-
when 'file'
|
|
228
|
-
# file must be terminal
|
|
229
|
-
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
|
|
230
|
-
# it's terminal, we found it
|
|
231
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
232
|
-
next false
|
|
233
|
-
when 'folder'
|
|
234
|
-
if state[:path].empty?
|
|
235
|
-
# we found it
|
|
236
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
237
|
-
next false
|
|
238
|
-
end
|
|
239
|
-
when 'link'
|
|
240
|
-
if state[:path].empty?
|
|
241
|
-
if process_last_link
|
|
242
|
-
# we found it
|
|
243
|
-
other_node = node_id_to_node(entry['target_node_id'])
|
|
244
|
-
raise 'cannot resolve link' if other_node.nil?
|
|
245
|
-
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
246
|
-
else
|
|
247
|
-
# we found it but we do not process the link
|
|
248
|
-
state[:result] = {api: self, file_id: entry['id']}
|
|
249
|
-
end
|
|
250
|
-
next false
|
|
251
|
-
end
|
|
252
|
-
else
|
|
253
|
-
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
254
|
-
end
|
|
255
|
-
# continue to dig folder
|
|
256
|
-
next true
|
|
257
|
-
end
|
|
243
|
+
resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
|
|
244
|
+
process_folder_tree(method_sym: :process_api_fid, state: resolve_state, top_file_id: top_file_id)
|
|
258
245
|
raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
|
|
246
|
+
Log.log.debug{"resolve_api_fid: #{path} -> #{resolve_state[:result][:api].base_url} #{resolve_state[:result][:file_id]}"}
|
|
259
247
|
return resolve_state[:result]
|
|
260
248
|
end
|
|
261
249
|
|
|
262
250
|
def find_files(top_file_id, test_block)
|
|
263
251
|
Log.log.debug{"find_files: file id=#{top_file_id}"}
|
|
264
252
|
find_state = {found: [], test_block: test_block}
|
|
265
|
-
process_folder_tree(state: find_state, top_file_id: top_file_id)
|
|
266
|
-
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
267
|
-
# test all files deeply
|
|
268
|
-
true
|
|
269
|
-
end
|
|
253
|
+
process_folder_tree(method_sym: :process_find_files, state: find_state, top_file_id: top_file_id)
|
|
270
254
|
return find_state[:found]
|
|
271
255
|
end
|
|
272
256
|
|
|
273
257
|
def refreshed_transfer_token
|
|
274
|
-
return
|
|
258
|
+
return oauth.token(refresh: true)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# @return part of transfer spec with transport parameters only
|
|
262
|
+
def transport_params
|
|
263
|
+
if @std_t_spec_cache.nil?
|
|
264
|
+
# retrieve values from API (and keep a copy/cache)
|
|
265
|
+
full_spec = create(
|
|
266
|
+
'files/download_setup',
|
|
267
|
+
{transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
|
|
268
|
+
)[:data]['transfer_specs'].first['transfer_spec']
|
|
269
|
+
# set available fields
|
|
270
|
+
@std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
|
|
271
|
+
h[i] = full_spec[i] if full_spec.key?(i)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
return @std_t_spec_cache
|
|
275
275
|
end
|
|
276
276
|
|
|
277
277
|
# Create transfer spec for gen4
|
|
@@ -285,9 +285,9 @@ module Aspera
|
|
|
285
285
|
ak_token = Rest.basic_token(auth_params[:username], auth_params[:password])
|
|
286
286
|
when :oauth2
|
|
287
287
|
ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
|
|
288
|
-
# TODO: token_generation_lambda = lambda{|do_refresh|
|
|
288
|
+
# TODO: token_generation_lambda = lambda{|do_refresh|oauth.token(refresh: do_refresh)}
|
|
289
289
|
# get bearer token, possibly use cache
|
|
290
|
-
ak_token =
|
|
290
|
+
ak_token = oauth.token
|
|
291
291
|
else Aspera.error_unexpected_value(auth_params[:type])
|
|
292
292
|
end
|
|
293
293
|
transfer_spec = {
|
|
@@ -327,18 +327,64 @@ module Aspera
|
|
|
327
327
|
transfer_spec[i] = settings[i] if settings.key?(i)
|
|
328
328
|
end if settings.is_a?(Hash)
|
|
329
329
|
else
|
|
330
|
-
|
|
331
|
-
@std_t_spec_cache ||= create(
|
|
332
|
-
'files/download_setup',
|
|
333
|
-
{transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
|
|
334
|
-
)[:data]['transfer_specs'].first['transfer_spec']
|
|
335
|
-
# copy some parts
|
|
336
|
-
TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
|
|
330
|
+
transfer_spec.merge!(transport_params)
|
|
337
331
|
end
|
|
338
332
|
Log.log.warn{"Expected transfer user: #{Transfer::Spec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
|
|
339
333
|
unless transfer_spec['remote_user'].eql?(Transfer::Spec::ACCESS_KEY_TRANSFER_USER)
|
|
340
334
|
return transfer_spec
|
|
341
335
|
end
|
|
336
|
+
|
|
337
|
+
private
|
|
338
|
+
|
|
339
|
+
def process_api_fid(entry, path, state)
|
|
340
|
+
# this block is called recursively for each entry in folder
|
|
341
|
+
# stop digging here if not in right path
|
|
342
|
+
return false unless entry['name'].eql?(state[:path].first)
|
|
343
|
+
# ok it matches, so we remove the match, and continue digging
|
|
344
|
+
state[:path].shift
|
|
345
|
+
path_fully_consumed = state[:path].empty?
|
|
346
|
+
case entry['type']
|
|
347
|
+
when 'file'
|
|
348
|
+
# file must be terminal
|
|
349
|
+
raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless path_fully_consumed
|
|
350
|
+
# it's terminal, we found it
|
|
351
|
+
Log.log.debug{"resolve_api_fid: found #{path} -> #{entry['id']}"}
|
|
352
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
353
|
+
return false
|
|
354
|
+
when 'folder'
|
|
355
|
+
if path_fully_consumed
|
|
356
|
+
# we found it
|
|
357
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
358
|
+
return false
|
|
359
|
+
end
|
|
360
|
+
when 'link'
|
|
361
|
+
if path_fully_consumed
|
|
362
|
+
if state[:process_last_link]
|
|
363
|
+
# we found it
|
|
364
|
+
other_node = nil
|
|
365
|
+
if entry_has_link_information(entry)
|
|
366
|
+
other_node = node_id_to_node(entry['target_node_id'])
|
|
367
|
+
end
|
|
368
|
+
raise 'Cannot resolve link' if other_node.nil?
|
|
369
|
+
state[:result] = {api: other_node, file_id: entry['target_id']}
|
|
370
|
+
else
|
|
371
|
+
# we found it but we do not process the link
|
|
372
|
+
state[:result] = {api: self, file_id: entry['id']}
|
|
373
|
+
end
|
|
374
|
+
return false
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
Log.log.warn{"Unknown element type: #{entry['type']}"}
|
|
378
|
+
end
|
|
379
|
+
# continue to dig folder
|
|
380
|
+
return true
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def process_find_files(entry, _path, state)
|
|
384
|
+
state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
|
|
385
|
+
# test all files deeply
|
|
386
|
+
return true
|
|
387
|
+
end
|
|
342
388
|
end
|
|
343
389
|
end
|
|
344
390
|
end
|
data/lib/aspera/ascmd.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Aspera
|
|
|
22
22
|
mv: 2,
|
|
23
23
|
rm: 1
|
|
24
24
|
}.freeze
|
|
25
|
+
private_constant :OPS_ARGS
|
|
25
26
|
# list of supported actions
|
|
26
27
|
OPERATIONS = OPS_ARGS.keys.freeze
|
|
27
28
|
|
|
@@ -94,7 +95,7 @@ module Aspera
|
|
|
94
95
|
raise Error.new(result[:errno], result[:errstr], action_sym, arguments) if
|
|
95
96
|
result.is_a?(Hash) && (result.keys.sort == TYPES_DESCR[:error][:fields].map{|i|i[:name]}.sort)
|
|
96
97
|
return result
|
|
97
|
-
end
|
|
98
|
+
end
|
|
98
99
|
|
|
99
100
|
# This exception is raised when +ascmd+ returns an error.
|
|
100
101
|
class Error < StandardError
|
|
@@ -103,7 +104,7 @@ module Aspera
|
|
|
103
104
|
|
|
104
105
|
def message; "ascmd: #{@errstr} (#{@errno})"; end
|
|
105
106
|
def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
|
|
106
|
-
end
|
|
107
|
+
end
|
|
107
108
|
|
|
108
109
|
# description of result structures (see ascmdtypes.h). Base types are big endian
|
|
109
110
|
# key = name of type
|
|
@@ -211,7 +212,7 @@ module Aspera
|
|
|
211
212
|
end
|
|
212
213
|
end
|
|
213
214
|
else Aspera.error_unexpected_value(type_descr[:decode])
|
|
214
|
-
end
|
|
215
|
+
end
|
|
215
216
|
return result
|
|
216
217
|
end
|
|
217
218
|
end
|
|
@@ -173,8 +173,7 @@ module Aspera
|
|
|
173
173
|
return exe_version
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
-
def
|
|
177
|
-
data = file_paths
|
|
176
|
+
def ascp_add_pvcl(data)
|
|
178
177
|
# read PATHs from ascp directly, and pvcl modules as well
|
|
179
178
|
Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
|
|
180
179
|
last_line = ''
|
|
@@ -202,15 +201,24 @@ module Aspera
|
|
|
202
201
|
raise last_line
|
|
203
202
|
end
|
|
204
203
|
end
|
|
205
|
-
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# extract some stings from ascp binary
|
|
207
|
+
def ascp_add_openssl(data)
|
|
206
208
|
ascp_file = data['ascp']
|
|
207
|
-
File.binread(ascp_file).scan(/[\x20-\x7E]{
|
|
208
|
-
if (m =
|
|
209
|
+
File.binread(ascp_file).scan(/[\x20-\x7E]{10,}/) do |bin_string|
|
|
210
|
+
if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
|
|
209
211
|
data['openssldir'] = m[1]
|
|
212
|
+
elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
|
|
213
|
+
data['openssl_version'] = m[1]
|
|
210
214
|
end
|
|
211
215
|
end if File.file?(ascp_file)
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ascp_info
|
|
219
|
+
data = file_paths
|
|
220
|
+
ascp_add_pvcl(data)
|
|
221
|
+
ascp_add_openssl(data)
|
|
214
222
|
return data
|
|
215
223
|
end
|
|
216
224
|
|
|
@@ -209,7 +209,7 @@ module Aspera
|
|
|
209
209
|
h[new_name] = value
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
|
-
end
|
|
212
|
+
end
|
|
213
213
|
|
|
214
214
|
def initialize
|
|
215
215
|
# current event being parsed line by line
|
|
@@ -236,7 +236,7 @@ module Aspera
|
|
|
236
236
|
return @last_event
|
|
237
237
|
else
|
|
238
238
|
raise "mgt port: unexpected line: [#{line}]"
|
|
239
|
-
end
|
|
239
|
+
end
|
|
240
240
|
return nil
|
|
241
241
|
end
|
|
242
242
|
end
|
data/lib/aspera/ascp/products.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Aspera
|
|
|
44
44
|
app_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server'),
|
|
45
45
|
log_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server', 'var', 'log')
|
|
46
46
|
}]
|
|
47
|
-
when Aspera::Environment::
|
|
47
|
+
when Aspera::Environment::OS_MACOS then [{
|
|
48
48
|
expected: CONNECT,
|
|
49
49
|
app_root: File.join(Dir.home, 'Applications', 'Aspera Connect.app'),
|
|
50
50
|
log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
|
|
@@ -8,10 +8,7 @@ module Aspera
|
|
|
8
8
|
# base class for applications supporting basic authentication
|
|
9
9
|
class BasicAuthPlugin < Cli::Plugin
|
|
10
10
|
class << self
|
|
11
|
-
|
|
12
|
-
def declare_options(options) # , force: false
|
|
13
|
-
#return if @@basic_options_declared && !force
|
|
14
|
-
#@@basic_options_declared = true # rubocop:disable Style/ClassVars
|
|
11
|
+
def declare_options(options)
|
|
15
12
|
options.declare(:url, 'URL of application, e.g. https://faspex.example.com/aspera/faspex')
|
|
16
13
|
options.declare(:username, "User's name to log in")
|
|
17
14
|
options.declare(:password, "User's password")
|
|
@@ -21,14 +18,13 @@ module Aspera
|
|
|
21
18
|
|
|
22
19
|
def initialize(basic_options: true, **env)
|
|
23
20
|
super(**env)
|
|
24
|
-
# , force: env[:all_manuals]
|
|
25
21
|
BasicAuthPlugin.declare_options(options) if basic_options
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
# returns a Rest object with basic auth
|
|
29
25
|
def basic_auth_params(subpath=nil)
|
|
30
26
|
api_url = options.get_option(:url, mandatory: true)
|
|
31
|
-
api_url = api_url
|
|
27
|
+
api_url = "#{api_url}/#{subpath}" unless subpath.nil?
|
|
32
28
|
return {
|
|
33
29
|
base_url: api_url,
|
|
34
30
|
auth: {
|
|
@@ -41,6 +37,6 @@ module Aspera
|
|
|
41
37
|
def basic_auth_api(subpath=nil)
|
|
42
38
|
return Rest.new(**basic_auth_params(subpath))
|
|
43
39
|
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# cspell:ignore csvt jsonpp
|
|
3
|
+
# cspell:ignore csvt jsonpp stdbin
|
|
4
4
|
require 'aspera/uri_reader'
|
|
5
5
|
require 'aspera/environment'
|
|
6
6
|
require 'aspera/log'
|
|
@@ -17,10 +17,9 @@ module Aspera
|
|
|
17
17
|
class ExtendedValue
|
|
18
18
|
include Singleton
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
DEF = 'DEF'
|
|
20
|
+
MARKER_START = '@'
|
|
21
|
+
MARKER_END = ':'
|
|
22
|
+
MARKER_IN_END = '@'
|
|
24
23
|
|
|
25
24
|
class << self
|
|
26
25
|
# decode comma separated table text
|
|
@@ -61,18 +60,31 @@ module Aspera
|
|
|
61
60
|
list: lambda{|v|v[1..-1].split(v[0])},
|
|
62
61
|
none: lambda{|v|ExtendedValue.assert_no_value(v, :none); nil}, # rubocop:disable Style/Semicolon
|
|
63
62
|
path: lambda{|v|File.expand_path(v)},
|
|
64
|
-
re: lambda{|v|Regexp.new(v)},
|
|
63
|
+
re: lambda{|v|Regexp.new(v, Regexp::MULTILINE)},
|
|
65
64
|
ruby: lambda{|v|Environment.secure_eval(v, __FILE__, __LINE__)},
|
|
66
65
|
secret: lambda{|v|prompt = v.empty? ? 'secret' : v; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
|
|
67
66
|
stdin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
|
|
67
|
+
stdbin: lambda{|v|ExtendedValue.assert_no_value(v, :stdin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
|
|
68
68
|
yaml: lambda{|v|YAML.load(v)},
|
|
69
69
|
zlib: lambda{|v|Zlib::Inflate.inflate(v)},
|
|
70
70
|
extend: lambda{|v|ExtendedValue.instance.evaluate_all(v)}
|
|
71
71
|
}
|
|
72
|
+
@default_decoder = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Regex to match an extended value
|
|
76
|
+
def handler_regex_string
|
|
77
|
+
"#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
public
|
|
75
81
|
|
|
82
|
+
def default_decoder=(value)
|
|
83
|
+
Log.log.debug{"setting default decoder to #{value} (#{value.class})"}
|
|
84
|
+
Aspera.assert(value.nil? || @handlers.key?(value))
|
|
85
|
+
@default_decoder = value
|
|
86
|
+
end
|
|
87
|
+
|
|
76
88
|
def modifiers; @handlers.keys; end
|
|
77
89
|
|
|
78
90
|
# add a new handler
|
|
@@ -82,16 +94,13 @@ module Aspera
|
|
|
82
94
|
@handlers[name] = method
|
|
83
95
|
end
|
|
84
96
|
|
|
85
|
-
#
|
|
86
|
-
def ext_re
|
|
87
|
-
"@(#{modifiers.join('|')}):"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# parse an option value if it is a String using supported extended value modifiers
|
|
97
|
+
# parse an string value to extended value, if it is a String using supported extended value modifiers
|
|
91
98
|
# other value types are returned as is
|
|
99
|
+
# @param value [String] the value to parse
|
|
100
|
+
# @param expect [Class,Array] one or a list of expected types
|
|
92
101
|
def evaluate(value)
|
|
93
102
|
return value unless value.is_a?(String)
|
|
94
|
-
regex = Regexp.new("^#{
|
|
103
|
+
regex = Regexp.new("^#{handler_regex_string}(.*)$", Regexp::MULTILINE)
|
|
95
104
|
# first determine decoders, in reversed order
|
|
96
105
|
handlers_reversed = []
|
|
97
106
|
while (m = value.match(regex))
|
|
@@ -101,18 +110,28 @@ module Aspera
|
|
|
101
110
|
# stop processing if handler is extend (it will be processed later)
|
|
102
111
|
break if handler.eql?(:extend)
|
|
103
112
|
end
|
|
113
|
+
Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
|
|
104
114
|
handlers_reversed.each do |handler|
|
|
105
115
|
value = @handlers[handler].call(value)
|
|
106
116
|
end
|
|
107
117
|
return value
|
|
108
|
-
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# parse string value as extended value
|
|
121
|
+
# use default decoder if none is specified
|
|
122
|
+
def evaluate_with_default(value)
|
|
123
|
+
if value.is_a?(String) && value.match(/^#{handler_regex_string}.*$/).nil? && !@default_decoder.nil?
|
|
124
|
+
value = [MARKER_START, @default_decoder, MARKER_END, value].join
|
|
125
|
+
end
|
|
126
|
+
return evaluate(value)
|
|
127
|
+
end
|
|
109
128
|
|
|
110
129
|
# find inner extended values
|
|
111
130
|
def evaluate_all(value)
|
|
112
|
-
regex = Regexp.new("^(.*)#{
|
|
131
|
+
regex = Regexp.new("^(.*)#{handler_regex_string}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
|
|
113
132
|
while (m = value.match(regex))
|
|
114
133
|
sub_value = "@#{m[2]}:#{m[3]}"
|
|
115
|
-
Log.log.debug
|
|
134
|
+
Log.log.debug{"evaluating #{sub_value}"}
|
|
116
135
|
value = m[1] + evaluate(sub_value) + m[4]
|
|
117
136
|
end
|
|
118
137
|
return value
|