aspera-cli 4.12.0 → 4.14.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 +45 -5
- data/CONTRIBUTING.md +113 -22
- data/README.md +1289 -754
- data/bin/ascli +3 -3
- data/examples/dascli +1 -1
- data/examples/rubyc +24 -0
- data/lib/aspera/aoc.rb +63 -74
- data/lib/aspera/ascmd.rb +5 -3
- data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
- data/lib/aspera/cli/extended_value.rb +24 -37
- data/lib/aspera/cli/formatter.rb +23 -25
- data/lib/aspera/cli/info.rb +2 -4
- data/lib/aspera/cli/main.rb +27 -27
- data/lib/aspera/cli/manager.rb +143 -120
- data/lib/aspera/cli/plugin.rb +88 -43
- data/lib/aspera/cli/plugins/alee.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +235 -104
- data/lib/aspera/cli/plugins/ats.rb +16 -18
- data/lib/aspera/cli/plugins/bss.rb +3 -3
- data/lib/aspera/cli/plugins/config.rb +190 -373
- data/lib/aspera/cli/plugins/console.rb +4 -6
- data/lib/aspera/cli/plugins/cos.rb +12 -13
- data/lib/aspera/cli/plugins/faspex.rb +21 -21
- data/lib/aspera/cli/plugins/faspex5.rb +399 -150
- data/lib/aspera/cli/plugins/node.rb +260 -174
- data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
- data/lib/aspera/cli/plugins/preview.rb +40 -62
- data/lib/aspera/cli/plugins/server.rb +33 -16
- data/lib/aspera/cli/plugins/shares.rb +24 -33
- data/lib/aspera/cli/plugins/sync.rb +6 -6
- data/lib/aspera/cli/transfer_agent.rb +47 -30
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +9 -7
- data/lib/aspera/command_line_builder.rb +2 -1
- data/lib/aspera/cos_node.rb +1 -1
- data/lib/aspera/data/6 +0 -0
- data/lib/aspera/environment.rb +7 -3
- data/lib/aspera/fasp/agent_connect.rb +6 -1
- data/lib/aspera/fasp/agent_direct.rb +17 -17
- data/lib/aspera/fasp/agent_httpgw.rb +138 -60
- data/lib/aspera/fasp/agent_node.rb +14 -4
- data/lib/aspera/fasp/agent_trsdk.rb +2 -0
- data/lib/aspera/fasp/error_info.rb +2 -0
- data/lib/aspera/fasp/installation.rb +19 -19
- data/lib/aspera/fasp/parameters.rb +29 -20
- data/lib/aspera/fasp/parameters.yaml +5 -2
- data/lib/aspera/fasp/resume_policy.rb +3 -3
- data/lib/aspera/fasp/transfer_spec.rb +8 -5
- data/lib/aspera/fasp/uri.rb +23 -21
- data/lib/aspera/faspex_gw.rb +1 -0
- data/lib/aspera/faspex_postproc.rb +3 -3
- data/lib/aspera/hash_ext.rb +12 -2
- data/lib/aspera/keychain/macos_security.rb +13 -13
- data/lib/aspera/log.rb +1 -0
- data/lib/aspera/node.rb +73 -84
- data/lib/aspera/oauth.rb +4 -3
- data/lib/aspera/persistency_action_once.rb +1 -1
- data/lib/aspera/preview/file_types.rb +8 -6
- data/lib/aspera/preview/generator.rb +23 -11
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +80 -0
- data/lib/aspera/preview/utils.rb +11 -11
- data/lib/aspera/proxy_auto_config.js +2 -2
- data/lib/aspera/rest.rb +42 -4
- data/lib/aspera/rest_call_error.rb +3 -1
- data/lib/aspera/secret_hider.rb +10 -5
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/sync.rb +41 -33
- data/lib/aspera/web_server_simple.rb +22 -18
- data.tar.gz.sig +0 -0
- metadata +40 -48
- metadata.gz.sig +0 -0
- data/docs/test_env.conf +0 -179
- data/examples/aoc.rb +0 -30
- data/examples/faspex4.rb +0 -94
- data/examples/node.rb +0 -96
- data/examples/server.rb +0 -93
- data/lib/aspera/data/7 +0 -0
@@ -18,42 +18,74 @@ module Aspera
|
|
18
18
|
RECIPIENT_TYPES = %w[user workgroup external_user distribution_list shared_inbox].freeze
|
19
19
|
PACKAGE_TERMINATED = %w[completed failed].freeze
|
20
20
|
API_DETECT = 'api/v5/configuration/ping'
|
21
|
+
# list of supported mailbox types (to list packages)
|
22
|
+
API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
|
23
|
+
PACKAGE_ALL_INIT = 'INIT'
|
24
|
+
PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
|
25
|
+
ADMIN_RESOURCES = %i[accounts contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs metadata_profiles
|
26
|
+
email_notifications].freeze
|
27
|
+
private_constant(*%i[RECIPIENT_TYPES PACKAGE_TERMINATED API_DETECT API_LIST_MAILBOX_TYPES PACKAGE_SEND_FROM_REMOTE_SOURCE])
|
21
28
|
class << self
|
22
29
|
def detect(base_url)
|
23
30
|
api = Rest.new(base_url: base_url, redirect_max: 1)
|
24
31
|
result = api.read(API_DETECT)
|
25
32
|
if result[:http].code.start_with?('2') && result[:http].body.strip.empty?
|
26
|
-
|
33
|
+
suffix_length = -2 - API_DETECT.length
|
34
|
+
return {
|
35
|
+
version: result[:http]['x-ibm-aspera'] || '5',
|
36
|
+
url: result[:http].uri.to_s[0..suffix_length],
|
37
|
+
name: 'Faspex 5'
|
38
|
+
}
|
27
39
|
end
|
28
40
|
return nil
|
29
41
|
end
|
30
42
|
end
|
31
43
|
|
44
|
+
# Faspex API v5: get transfer spec for connect
|
32
45
|
TRANSFER_CONNECT = 'connect'
|
33
46
|
|
34
47
|
def initialize(env)
|
35
48
|
super(env)
|
36
|
-
options.
|
37
|
-
options.
|
38
|
-
options.
|
39
|
-
options.
|
40
|
-
options.
|
41
|
-
options.
|
42
|
-
options.
|
43
|
-
options.
|
49
|
+
options.declare(:client_id, 'OAuth client identifier')
|
50
|
+
options.declare(:client_secret, 'OAuth client secret')
|
51
|
+
options.declare(:redirect_uri, 'OAuth redirect URI for web authentication')
|
52
|
+
options.declare(:auth, 'OAuth type of authentication', values: %i[boot link].concat(Oauth::STD_AUTH_TYPES), default: :jwt)
|
53
|
+
options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
|
54
|
+
options.declare(:passphrase, 'OAuth JWT RSA private key passphrase')
|
55
|
+
options.declare(:link, 'Public link authorization (specific operations)')
|
56
|
+
options.declare(:box, "Package inbox, either shared inbox name or one of #{API_LIST_MAILBOX_TYPES} or #{VAL_ALL}", default: 'inbox')
|
57
|
+
options.declare(:shared_folder, 'Send package with files from shared folder')
|
58
|
+
options.declare(:group_type, 'Shared inbox or workgroup', values: %i[shared_inboxes workgroups], default: :shared_inboxes)
|
44
59
|
options.parse_options!
|
45
60
|
end
|
46
61
|
|
47
62
|
def set_api
|
48
|
-
|
63
|
+
public_link = options.get_option(:link)
|
64
|
+
unless public_link.nil?
|
65
|
+
@faspex5_api_base_url = public_link.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
|
66
|
+
options.set_option(:auth, :link)
|
67
|
+
end
|
68
|
+
@faspex5_api_base_url ||= options.get_option(:url, mandatory: true).gsub(%r{/+$}, '')
|
49
69
|
@faspex5_api_auth_url = "#{@faspex5_api_base_url}/auth"
|
50
70
|
faspex5_api_v5_url = "#{@faspex5_api_base_url}/api/v5"
|
51
|
-
case options.get_option(:auth,
|
71
|
+
case options.get_option(:auth, mandatory: true)
|
72
|
+
when :link
|
73
|
+
uri = URI.parse(public_link)
|
74
|
+
args = URI.decode_www_form(uri.query).each_with_object({}){|v, h|h[v.first] = v.last; }
|
75
|
+
Log.dump(:args, args)
|
76
|
+
context = args['context']
|
77
|
+
raise 'missing context' if context.nil?
|
78
|
+
@pub_link_context = JSON.parse(Base64.decode64(context))
|
79
|
+
Log.dump(:@pub_link_context, @pub_link_context)
|
80
|
+
@api_v5 = Rest.new({
|
81
|
+
base_url: faspex5_api_v5_url,
|
82
|
+
headers: {'Passcode' => @pub_link_context['passcode']}
|
83
|
+
})
|
52
84
|
when :boot
|
53
85
|
# the password here is the token copied directly from browser in developer mode
|
54
86
|
@api_v5 = Rest.new({
|
55
87
|
base_url: faspex5_api_v5_url,
|
56
|
-
headers: {'Authorization' => options.get_option(:password,
|
88
|
+
headers: {'Authorization' => options.get_option(:password, mandatory: true)}
|
57
89
|
})
|
58
90
|
when :web
|
59
91
|
# opens a browser and ask user to auth using web
|
@@ -63,11 +95,11 @@ module Aspera
|
|
63
95
|
type: :oauth2,
|
64
96
|
base_url: @faspex5_api_auth_url,
|
65
97
|
grant_method: :web,
|
66
|
-
client_id: options.get_option(:client_id,
|
67
|
-
web: {redirect_uri: options.get_option(:redirect_uri,
|
98
|
+
client_id: options.get_option(:client_id, mandatory: true),
|
99
|
+
web: {redirect_uri: options.get_option(:redirect_uri, mandatory: true)}
|
68
100
|
}})
|
69
101
|
when :jwt
|
70
|
-
app_client_id = options.get_option(:client_id,
|
102
|
+
app_client_id = options.get_option(:client_id, mandatory: true)
|
71
103
|
@api_v5 = Rest.new({
|
72
104
|
base_url: faspex5_api_v5_url,
|
73
105
|
auth: {
|
@@ -78,27 +110,31 @@ module Aspera
|
|
78
110
|
jwt: {
|
79
111
|
payload: {
|
80
112
|
iss: app_client_id, # issuer
|
81
|
-
aud: app_client_id, # audience
|
82
|
-
sub: "user:#{options.get_option(:username,
|
113
|
+
aud: app_client_id, # audience (this field is not clear...)
|
114
|
+
sub: "user:#{options.get_option(:username, mandatory: true)}" # subject is a user
|
83
115
|
},
|
84
|
-
|
85
|
-
private_key_obj: OpenSSL::PKey::RSA.new(options.get_option(:private_key, is_type: :mandatory), options.get_option(:passphrase)),
|
116
|
+
private_key_obj: OpenSSL::PKey::RSA.new(options.get_option(:private_key, mandatory: true), options.get_option(:passphrase)),
|
86
117
|
headers: {typ: 'JWT'}
|
87
118
|
}
|
88
119
|
}})
|
120
|
+
else raise 'Unexpected case for option: auth'
|
89
121
|
end
|
90
122
|
end
|
91
123
|
|
124
|
+
# if recipient is just an email, then convert to expected API hash : name and type
|
92
125
|
def normalize_recipients(parameters)
|
93
126
|
return unless parameters.key?('recipients')
|
94
127
|
raise 'Field recipients must be an Array' unless parameters['recipients'].is_a?(Array)
|
95
|
-
|
128
|
+
recipient_types = RECIPIENT_TYPES
|
129
|
+
if parameters.key?('recipient_types')
|
130
|
+
recipient_types = parameters['recipient_types']
|
131
|
+
parameters.delete('recipient_types')
|
132
|
+
recipient_types = [recipient_types] unless recipient_types.is_a?(Array)
|
133
|
+
end
|
134
|
+
parameters['recipients'].map! do |recipient_data|
|
96
135
|
# if just a string, assume it is the name
|
97
136
|
if recipient_data.is_a?(String)
|
98
|
-
|
99
|
-
raise "No matching contact for #{recipient_data}" if result.empty?
|
100
|
-
raise "Multiple matching contact for #{recipient_data} : #{result['contacts'].map{|i|i['name']}.join(', ')}" unless 1.eql?(result['total_count'])
|
101
|
-
matched = result['contacts'].first
|
137
|
+
matched = @api_v5.lookup_by_name('contacts', recipient_data, {context: 'packages', type: Rest.array_params(recipient_types)})
|
102
138
|
recipient_data = {
|
103
139
|
name: matched['name'],
|
104
140
|
recipient_type: matched['type']
|
@@ -109,14 +145,14 @@ module Aspera
|
|
109
145
|
end
|
110
146
|
end
|
111
147
|
|
112
|
-
|
113
|
-
|
148
|
+
# wait for package status to be in provided list
|
149
|
+
def wait_package_status(id, status_list: PACKAGE_TERMINATED)
|
114
150
|
spinner = nil
|
115
151
|
progress = nil
|
116
152
|
while true
|
117
153
|
status = @api_v5.read("packages/#{id}/upload_details")[:data]
|
118
154
|
# user asked to not follow
|
119
|
-
break
|
155
|
+
break if status_list.nil? || status_list.include?(status['upload_status'])
|
120
156
|
if status['upload_status'].eql?('submitted')
|
121
157
|
if spinner.nil?
|
122
158
|
spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
|
@@ -133,22 +169,233 @@ module Aspera
|
|
133
169
|
else
|
134
170
|
progress.progress = status['bytes_written'].to_i
|
135
171
|
end
|
136
|
-
break if PACKAGE_TERMINATED.include?(status['upload_status'])
|
137
172
|
sleep(0.5)
|
138
173
|
end
|
139
174
|
status['id'] = id
|
140
175
|
return status
|
141
176
|
end
|
142
177
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
178
|
+
# get a (full or partial) list of all entities of a given type
|
179
|
+
# @param type [String] the type of entity to list (just a name)
|
180
|
+
# @param query [Hash,nil] additional query parameters
|
181
|
+
# @param path [String] optional prefix to add to the path (nil or empty string: no prefix)
|
182
|
+
# @param item_list_key [String] key in the result to get the list of items
|
183
|
+
def list_entities(type:, path: nil, query: nil, item_list_key: nil)
|
184
|
+
query = {} if query.nil?
|
185
|
+
type = type.to_s if type.is_a?(Symbol)
|
186
|
+
item_list_key = type if item_list_key.nil?
|
187
|
+
raise "internal error: Invalid type #{type.class}" unless type.is_a?(String)
|
188
|
+
full_path = type
|
189
|
+
full_path = "#{path}/#{full_path}" unless path.nil? || path.empty?
|
190
|
+
result = []
|
191
|
+
offset = 0
|
192
|
+
max_items = query.delete(MAX_ITEMS)
|
193
|
+
remain_pages = query.delete(MAX_PAGES)
|
194
|
+
# merge default parameters, by default 100 per page
|
195
|
+
query = {'limit'=> 100}.merge(query)
|
196
|
+
loop do
|
197
|
+
query['offset'] = offset
|
198
|
+
page_result = @api_v5.read(full_path, query)[:data]
|
199
|
+
result.concat(page_result[item_list_key])
|
200
|
+
# reach the limit set by user ?
|
201
|
+
if !max_items.nil? && (result.length >= max_items)
|
202
|
+
result = result.slice(0, max_items)
|
203
|
+
break
|
204
|
+
end
|
205
|
+
break if result.length >= page_result['total_count']
|
206
|
+
remain_pages -= 1 unless remain_pages.nil?
|
207
|
+
break if remain_pages == 0
|
208
|
+
offset += page_result[item_list_key].length
|
209
|
+
end
|
210
|
+
return result
|
211
|
+
end
|
212
|
+
|
213
|
+
# lookup an entity id from its name
|
214
|
+
def lookup_entity_by_field(type:, value:, field: 'name', query: :default, path: nil, item_list_key: nil)
|
215
|
+
query = {'q'=> value} if query.eql?(:default)
|
216
|
+
found = list_entities(type: type, path: path, query: query, item_list_key: item_list_key).select{|i|i[field].eql?(value)}
|
217
|
+
case found.length
|
218
|
+
when 0 then raise "No #{type} with #{field} = #{value}"
|
219
|
+
when 1 then return found.first
|
220
|
+
else raise "Found #{found.length} #{path} with #{field} = #{value}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# list all packages with optional filter
|
225
|
+
def list_packages_with_filter
|
226
|
+
filter = options.get_next_argument('filter', mandatory: false, type: Proc, default: ->(_x){true})
|
227
|
+
# translate box name to API prefix (with ending slash)
|
228
|
+
box = options.get_option(:box)
|
229
|
+
api_path =
|
230
|
+
case box
|
231
|
+
when VAL_ALL then '' # only admin can list all packages globally
|
232
|
+
when *API_LIST_MAILBOX_TYPES then box
|
233
|
+
else
|
234
|
+
group_type = options.get_option(:group_type)
|
235
|
+
"#{group_type}/#{lookup_entity_by_field(type: group_type, value: box)['id']}"
|
236
|
+
end
|
237
|
+
return list_entities(
|
238
|
+
type: 'packages',
|
239
|
+
query: query_read_delete(default: {}),
|
240
|
+
path: api_path).select(&filter)
|
149
241
|
end
|
150
242
|
|
151
|
-
|
243
|
+
def package_receive
|
244
|
+
# prepare persistency if needed
|
245
|
+
skip_ids_persistency = nil
|
246
|
+
if options.get_option(:once_only, mandatory: true)
|
247
|
+
# read ids from persistency
|
248
|
+
skip_ids_persistency = PersistencyActionOnce.new(
|
249
|
+
manager: @agents[:persistency],
|
250
|
+
data: [],
|
251
|
+
id: IdGenerator.from_list([
|
252
|
+
'faspex_recv',
|
253
|
+
options.get_option(:url, mandatory: true),
|
254
|
+
options.get_option(:username, mandatory: true)]))
|
255
|
+
end
|
256
|
+
package_ids =
|
257
|
+
if @pub_link_context&.key?('package_id')
|
258
|
+
@pub_link_context['package_id']
|
259
|
+
else
|
260
|
+
# one or several packages
|
261
|
+
instance_identifier
|
262
|
+
end
|
263
|
+
case package_ids
|
264
|
+
when PACKAGE_ALL_INIT
|
265
|
+
raise 'Only with option once_only' unless skip_ids_persistency
|
266
|
+
skip_ids_persistency.data.clear.concat(list_packages_with_filter.map{|p|p['id']})
|
267
|
+
skip_ids_persistency.save
|
268
|
+
return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
|
269
|
+
when VAL_ALL
|
270
|
+
# TODO: if packages have same name, they will overwrite ?
|
271
|
+
package_ids = list_packages_with_filter.map{|p|p['id']}
|
272
|
+
Log.dump(:package_ids, package_ids)
|
273
|
+
Log.dump(:package_ids, skip_ids_persistency.data)
|
274
|
+
package_ids.reject!{|i|skip_ids_persistency.data.include?(i)} if skip_ids_persistency
|
275
|
+
Log.dump(:package_ids, package_ids)
|
276
|
+
end
|
277
|
+
# a single id was provided
|
278
|
+
# TODO: check package_ids is a list of strings
|
279
|
+
package_ids = [package_ids] if package_ids.is_a?(String)
|
280
|
+
result_transfer = []
|
281
|
+
package_ids.each do |pkg_id|
|
282
|
+
formatter.display_status("Receiving package #{pkg_id}")
|
283
|
+
param_file_list = {}
|
284
|
+
begin
|
285
|
+
param_file_list['paths'] = transfer.source_list.map{|source|{'path'=>source}}
|
286
|
+
rescue Aspera::Cli::CliBadArgument
|
287
|
+
# paths is optional
|
288
|
+
end
|
289
|
+
download_params = {
|
290
|
+
type: 'received',
|
291
|
+
transfer_type: TRANSFER_CONNECT
|
292
|
+
}
|
293
|
+
box = options.get_option(:box)
|
294
|
+
case box
|
295
|
+
when /outbox/ then download_params[:type] = 'sent'
|
296
|
+
when *API_LIST_MAILBOX_TYPES then nil # nothing to do
|
297
|
+
else # shared inbox / workgroup
|
298
|
+
download_params[:recipient_workgroup_id] = lookup_entity_by_field(type: options.get_option(:group_type), value: box)['id']
|
299
|
+
end
|
300
|
+
# TODO: allow from sent as well ?
|
301
|
+
transfer_spec = @api_v5.call(
|
302
|
+
operation: 'POST',
|
303
|
+
subpath: "packages/#{pkg_id}/transfer_spec/download",
|
304
|
+
headers: {'Accept' => 'application/json'},
|
305
|
+
url_params: download_params,
|
306
|
+
json_params: param_file_list
|
307
|
+
)[:data]
|
308
|
+
# delete flag for Connect Client
|
309
|
+
transfer_spec.delete('authentication')
|
310
|
+
statuses = transfer.start(transfer_spec)
|
311
|
+
result_transfer.push({'package' => pkg_id, Main::STATUS_FIELD => statuses})
|
312
|
+
# skip only if all sessions completed
|
313
|
+
if TransferAgent.session_status(statuses).eql?(:success) && skip_ids_persistency
|
314
|
+
skip_ids_persistency.data.push(pkg_id)
|
315
|
+
skip_ids_persistency.save
|
316
|
+
end
|
317
|
+
end
|
318
|
+
return Main.result_transfer_multiple(result_transfer)
|
319
|
+
end
|
320
|
+
|
321
|
+
def package_action
|
322
|
+
command = options.get_next_command(%i[list show browse status delete send receive])
|
323
|
+
case command
|
324
|
+
when :list
|
325
|
+
return {
|
326
|
+
type: :object_list,
|
327
|
+
data: list_packages_with_filter,
|
328
|
+
fields: %w[id title release_date total_bytes total_files created_time state]
|
329
|
+
}
|
330
|
+
when :show
|
331
|
+
id = @pub_link_context['package_id'] if @pub_link_context&.key?('package_id')
|
332
|
+
id ||= instance_identifier
|
333
|
+
return {type: :single_object, data: @api_v5.read("packages/#{id}")[:data]}
|
334
|
+
when :browse
|
335
|
+
id = @pub_link_context['package_id'] if @pub_link_context&.key?('package_id')
|
336
|
+
id ||= instance_identifier
|
337
|
+
path = options.get_next_argument('path', expected: :single, mandatory: false) || '/'
|
338
|
+
# TODO: support multi-page listing ?
|
339
|
+
params = {
|
340
|
+
# recipient_user_id: 25,
|
341
|
+
# offset: 0,
|
342
|
+
# limit: 25
|
343
|
+
}
|
344
|
+
result = @api_v5.call({
|
345
|
+
operation: 'POST',
|
346
|
+
subpath: "packages/#{id}/files/received",
|
347
|
+
headers: {'Accept' => 'application/json'},
|
348
|
+
url_params: params,
|
349
|
+
json_params: {'path' => path, 'filters' => {'basenames'=>[]}}})[:data]
|
350
|
+
formatter.display_item_count(result['item_count'], result['total_count'])
|
351
|
+
return {type: :object_list, data: result['items']}
|
352
|
+
when :status
|
353
|
+
status = wait_package_status(instance_identifier, status_list: nil)
|
354
|
+
return {type: :single_object, data: status}
|
355
|
+
when :delete
|
356
|
+
ids = instance_identifier
|
357
|
+
ids = [ids] unless ids.is_a?(Array)
|
358
|
+
raise 'Package identifier must be a single id or an Array' unless ids.is_a?(Array) && ids.all?(String)
|
359
|
+
# API returns 204, empty on success
|
360
|
+
@api_v5.call({operation: 'DELETE', subpath: 'packages', headers: {'Accept' => 'application/json'}, json_params: {ids: ids}})
|
361
|
+
return Main.result_status('Package(s) deleted')
|
362
|
+
when :send
|
363
|
+
parameters = value_create_modify(command: command, type: Hash)
|
364
|
+
normalize_recipients(parameters)
|
365
|
+
package = @api_v5.create('packages', parameters)[:data]
|
366
|
+
shared_folder = options.get_option(:shared_folder)
|
367
|
+
if shared_folder.nil?
|
368
|
+
# TODO: option to send from remote source or httpgw
|
369
|
+
transfer_spec = @api_v5.call(
|
370
|
+
operation: 'POST',
|
371
|
+
subpath: "packages/#{package['id']}/transfer_spec/upload",
|
372
|
+
headers: {'Accept' => 'application/json'},
|
373
|
+
url_params: {transfer_type: TRANSFER_CONNECT},
|
374
|
+
json_params: {paths: transfer.source_list}
|
375
|
+
)[:data]
|
376
|
+
# well, we asked a TS for connect, but we actually want a generic one
|
377
|
+
transfer_spec.delete('authentication')
|
378
|
+
return Main.result_transfer(transfer.start(transfer_spec))
|
379
|
+
else
|
380
|
+
if (m = shared_folder.match(REGEX_LOOKUP_ID_BY_FIELD))
|
381
|
+
shared_folder = lookup_entity_by_field(type: 'shared_folders', value: m[2])['id']
|
382
|
+
end
|
383
|
+
transfer_request = {shared_folder_id: shared_folder, paths: transfer.source_list}
|
384
|
+
# start remote transfer and get first status
|
385
|
+
result = @api_v5.create("packages/#{package['id']}/remote_transfer", transfer_request)[:data]
|
386
|
+
result['id'] = package['id']
|
387
|
+
unless result['status'].eql?('completed')
|
388
|
+
formatter.display_status("Package #{package['id']}")
|
389
|
+
result = wait_package_status(package['id'])
|
390
|
+
end
|
391
|
+
return {type: :single_object, data: result}
|
392
|
+
end
|
393
|
+
when :receive
|
394
|
+
return package_receive
|
395
|
+
end # case package
|
396
|
+
end
|
397
|
+
|
398
|
+
ACTIONS = %i[health version user bearer_token packages shared_folders admin gateway postprocessing].freeze
|
152
399
|
|
153
400
|
def execute_action
|
154
401
|
command = options.get_next_command(ACTIONS)
|
@@ -180,110 +427,20 @@ module Aspera
|
|
180
427
|
end
|
181
428
|
when :bearer_token
|
182
429
|
return {type: :text, data: @api_v5.oauth_token}
|
183
|
-
when :
|
184
|
-
|
185
|
-
case command
|
186
|
-
when :list
|
187
|
-
parameters = options.get_option(:value)
|
188
|
-
return {
|
189
|
-
type: :object_list,
|
190
|
-
data: @api_v5.read('packages', parameters)[:data]['packages'],
|
191
|
-
fields: %w[id title release_date total_bytes total_files created_time state]
|
192
|
-
}
|
193
|
-
when :show
|
194
|
-
id = instance_identifier
|
195
|
-
return {type: :single_object, data: @api_v5.read("packages/#{id}")[:data]}
|
196
|
-
when :status
|
197
|
-
status = wait_for_complete_upload(instance_identifier)
|
198
|
-
return {type: :single_object, data: status}
|
199
|
-
when :send
|
200
|
-
parameters = options.get_option(:value, is_type: :mandatory)
|
201
|
-
raise CliBadArgument, 'Value must be Hash, refer to API' unless parameters.is_a?(Hash)
|
202
|
-
normalize_recipients(parameters)
|
203
|
-
package = @api_v5.create('packages', parameters)[:data]
|
204
|
-
shared_folder = options.get_option(:shared_folder)
|
205
|
-
if shared_folder.nil?
|
206
|
-
# TODO: option to send from remote source or httpgw
|
207
|
-
transfer_spec = @api_v5.call(
|
208
|
-
operation: 'POST',
|
209
|
-
subpath: "packages/#{package['id']}/transfer_spec/upload",
|
210
|
-
headers: {'Accept' => 'application/json'},
|
211
|
-
url_params: {transfer_type: TRANSFER_CONNECT},
|
212
|
-
json_params: {paths: transfer.source_list}
|
213
|
-
)[:data]
|
214
|
-
transfer_spec.delete('authentication')
|
215
|
-
return Main.result_transfer(transfer.start(transfer_spec))
|
216
|
-
else
|
217
|
-
if !shared_folder.to_i.to_s.eql?(shared_folder)
|
218
|
-
shared_folder = lookup_entity('shared_folders', 'name', shared_folder)['id']
|
219
|
-
end
|
220
|
-
transfer_request = {shared_folder_id: shared_folder, paths: transfer.source_list}
|
221
|
-
# start remote transfer and get first status
|
222
|
-
result = @api_v5.create("packages/#{package['id']}/remote_transfer", transfer_request)[:data]
|
223
|
-
result['id'] = package['id']
|
224
|
-
unless result['status'].eql?('completed')
|
225
|
-
formatter.display_status("Package #{package['id']}")
|
226
|
-
result = wait_for_complete_upload(package['id'])
|
227
|
-
end
|
228
|
-
return {type: :single_object, data: result}
|
229
|
-
end
|
230
|
-
when :receive
|
231
|
-
pkg_type = 'received'
|
232
|
-
pack_id = instance_identifier
|
233
|
-
package_ids = [pack_id]
|
234
|
-
skip_ids_data = []
|
235
|
-
skip_ids_persistency = nil
|
236
|
-
if options.get_option(:once_only, is_type: :mandatory)
|
237
|
-
# read ids from persistency
|
238
|
-
skip_ids_persistency = PersistencyActionOnce.new(
|
239
|
-
manager: @agents[:persistency],
|
240
|
-
data: skip_ids_data,
|
241
|
-
id: IdGenerator.from_list([
|
242
|
-
'faspex_recv',
|
243
|
-
options.get_option(:url, is_type: :mandatory),
|
244
|
-
options.get_option(:username, is_type: :mandatory),
|
245
|
-
pkg_type]))
|
246
|
-
end
|
247
|
-
if VAL_ALL.eql?(pack_id)
|
248
|
-
# TODO: if packages have same name, they will overwrite
|
249
|
-
parameters = options.get_option(:value)
|
250
|
-
parameters ||= {'type' => 'received', 'subtype' => 'mypackages', 'limit' => 1000}
|
251
|
-
raise CliBadArgument, 'value filter must be Hash (API GET)' unless parameters.is_a?(Hash)
|
252
|
-
package_ids = @api_v5.read('packages', parameters)[:data]['packages'].map{|p|p['id']}
|
253
|
-
package_ids.reject!{|i|skip_ids_data.include?(i)}
|
254
|
-
end
|
255
|
-
result_transfer = []
|
256
|
-
package_ids.each do |pkg_id|
|
257
|
-
param_file_list = {}
|
258
|
-
begin
|
259
|
-
param_file_list['paths'] = transfer.source_list
|
260
|
-
rescue Aspera::Cli::CliBadArgument
|
261
|
-
# paths is optional
|
262
|
-
end
|
263
|
-
# TODO: allow from sent as well ?
|
264
|
-
transfer_spec = @api_v5.call(
|
265
|
-
operation: 'POST',
|
266
|
-
subpath: "packages/#{pkg_id}/transfer_spec/download",
|
267
|
-
headers: {'Accept' => 'application/json'},
|
268
|
-
url_params: {transfer_type: TRANSFER_CONNECT, type: pkg_type},
|
269
|
-
json_params: param_file_list
|
270
|
-
)[:data]
|
271
|
-
transfer_spec.delete('authentication')
|
272
|
-
statuses = transfer.start(transfer_spec)
|
273
|
-
result_transfer.push({'package' => pkg_id, Main::STATUS_FIELD => statuses})
|
274
|
-
# skip only if all sessions completed
|
275
|
-
skip_ids_data.push(pkg_id) if TransferAgent.session_status(statuses).eql?(:success)
|
276
|
-
end
|
277
|
-
skip_ids_persistency&.save
|
278
|
-
return Main.result_transfer_multiple(result_transfer)
|
279
|
-
end # case package
|
430
|
+
when :packages
|
431
|
+
return package_action
|
280
432
|
when :shared_folders
|
281
433
|
all_shared_folders = @api_v5.read('shared_folders')[:data]['shared_folders']
|
282
434
|
case options.get_next_command(%i[list browse])
|
283
435
|
when :list
|
284
436
|
return {type: :object_list, data: all_shared_folders}
|
285
437
|
when :browse
|
286
|
-
shared_folder_id = instance_identifier
|
438
|
+
shared_folder_id = instance_identifier do |field, value|
|
439
|
+
matches = all_shared_folders.select{|i|i[field].eql?(value)}
|
440
|
+
raise "no match for #{field} = #{value}" if matches.empty?
|
441
|
+
raise "multiple matches for #{field} = #{value}" if matches.length > 1
|
442
|
+
matches.first['id']
|
443
|
+
end
|
287
444
|
path = options.get_next_argument('folder path', mandatory: false) || '/'
|
288
445
|
node = all_shared_folders.find{|i|i['id'].eql?(shared_folder_id)}
|
289
446
|
raise "No such shared folder id #{shared_folder_id}" if node.nil?
|
@@ -301,12 +458,14 @@ module Aspera
|
|
301
458
|
end
|
302
459
|
end
|
303
460
|
when :admin
|
304
|
-
case options.get_next_command(%i[resource])
|
461
|
+
case options.get_next_command(%i[resource smtp].freeze)
|
305
462
|
when :resource
|
306
|
-
res_type = options.get_next_command(
|
307
|
-
email_notifications])
|
463
|
+
res_type = options.get_next_command(ADMIN_RESOURCES)
|
308
464
|
res_path = list_key = res_type.to_s
|
309
465
|
id_as_arg = false
|
466
|
+
display_fields = nil
|
467
|
+
adm_api = @api_v5
|
468
|
+
available_commands = [].concat(Plugin::ALL_OPS)
|
310
469
|
case res_type
|
311
470
|
when :metadata_profiles
|
312
471
|
res_path = 'configuration/metadata_profiles'
|
@@ -314,34 +473,93 @@ module Aspera
|
|
314
473
|
when :email_notifications
|
315
474
|
list_key = false
|
316
475
|
id_as_arg = 'type'
|
476
|
+
when :accounts
|
477
|
+
display_fields = [:all_but, 'user_profile_data_attributes']
|
478
|
+
when :oauth_clients
|
479
|
+
display_fields = [:all_but, 'public_key']
|
480
|
+
adm_api = Rest.new(@api_v5.params.merge({base_url: @faspex5_api_auth_url}))
|
481
|
+
when :shared_inboxes, :workgroups
|
482
|
+
available_commands.push(:members, :saml_groups, :invite_external_collaborator)
|
317
483
|
end
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
484
|
+
res_command = options.get_next_command(available_commands)
|
485
|
+
case res_command
|
486
|
+
when *Plugin::ALL_OPS
|
487
|
+
return entity_command(res_command, adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg)
|
488
|
+
when :invite_external_collaborator
|
489
|
+
shared_inbox_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
|
490
|
+
creation_payload = value_create_modify(command: res_command, type: [Hash, String])
|
491
|
+
creation_payload = {'email_address' => creation_payload} if creation_payload.is_a?(String)
|
492
|
+
res_path = "#{res_type}/#{shared_inbox_id}/external_collaborator"
|
493
|
+
result = adm_api.create(res_path, creation_payload)[:data]
|
494
|
+
formatter.display_status(result['message'])
|
495
|
+
result = lookup_entity_by_field(type: 'members', path: "#{res_type}/#{shared_inbox_id}", value: creation_payload['email_address'], query: {})
|
496
|
+
return {type: :single_object, data: result}
|
497
|
+
when :members, :saml_groups
|
498
|
+
res_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
|
499
|
+
res_prefix = "#{res_type}/#{res_id}"
|
500
|
+
res_path = "#{res_prefix}/#{res_command}"
|
501
|
+
list_key = res_command.to_s
|
502
|
+
list_key = 'groups' if res_command.eql?(:saml_groups)
|
503
|
+
sub_command = options.get_next_command(%i[create list modify delete])
|
504
|
+
if sub_command.eql?(:create) && options.get_option(:value).nil?
|
505
|
+
raise "use option 'value' to provide saml group_id and access (refer to API)" unless res_command.eql?(:members)
|
506
|
+
# first arg is one user name or list of users
|
507
|
+
users = options.get_next_argument('user id, or email, or list of')
|
508
|
+
users = [users] unless users.is_a?(Array)
|
509
|
+
users = users.map do |user|
|
510
|
+
if (m = user.match(REGEX_LOOKUP_ID_BY_FIELD))
|
511
|
+
lookup_entity_by_field(
|
512
|
+
type: 'accounts', field: m[1], value: m[2],
|
513
|
+
query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
|
514
|
+
else
|
515
|
+
# it's the user id (not member id...)
|
516
|
+
user
|
517
|
+
end
|
518
|
+
end
|
519
|
+
access = options.get_next_argument('level', mandatory: false, expected: %i[submit_only standard shared_inbox_admin], default: :standard)
|
520
|
+
# TODO: unshift to command line parameters instead of using deprecated option "value"
|
521
|
+
options.set_option(:value, {user: users.map{|u|{id: u, access: access}}})
|
322
522
|
end
|
323
|
-
|
324
|
-
|
325
|
-
|
523
|
+
return entity_command(sub_command, adm_api, res_path, item_list_key: list_key) do |field, value|
|
524
|
+
lookup_entity_by_field(
|
525
|
+
type: 'accounts', field: field, value: value,
|
526
|
+
query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
|
527
|
+
end
|
528
|
+
end
|
529
|
+
when :smtp
|
530
|
+
smtp_path = 'configuration/smtp'
|
531
|
+
smtp_cmd = options.get_next_command(%i[show create modify delete test])
|
532
|
+
case smtp_cmd
|
533
|
+
when :show
|
534
|
+
return { type: :single_object, data: @api_v5.read(smtp_path)[:data] }
|
535
|
+
when :create
|
536
|
+
return { type: :single_object, data: @api_v5.create(smtp_path, value_create_modify(command: smtp_cmd, type: Hash))[:data] }
|
537
|
+
when :modify
|
538
|
+
return { type: :single_object, data: @api_v5.modify(smtp_path, value_create_modify(command: smtp_cmd, type: Hash))[:data] }
|
539
|
+
when :delete
|
540
|
+
return { type: :single_object, data: @api_v5.delete(smtp_path)[:data] }
|
541
|
+
when :test
|
542
|
+
test_data = options.get_next_argument('Email or test data, see API')
|
543
|
+
test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
|
544
|
+
return { type: :single_object, data: @api_v5.create(File.join(smtp_path, 'test'), test_data)[:data] }
|
326
545
|
end
|
327
|
-
return entity_action(adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg)
|
328
546
|
end
|
329
547
|
when :gateway
|
330
548
|
require 'aspera/faspex_gw'
|
331
|
-
url =
|
549
|
+
url = value_create_modify(type: String)
|
332
550
|
uri = URI.parse(url)
|
333
551
|
server = WebServerSimple.new(uri)
|
334
552
|
server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
|
553
|
+
# on ctrl-c, tell server main loop to exit
|
335
554
|
trap('INT') { server.shutdown }
|
336
|
-
formatter.display_status("Faspex 4
|
555
|
+
formatter.display_status("Gateway for Faspex 4-style API listening on #{url}")
|
337
556
|
Log.log.info("Listening on #{url}")
|
338
557
|
# this is blocking until server exits
|
339
558
|
server.start
|
340
559
|
return Main.result_status('Gateway terminated')
|
341
560
|
when :postprocessing
|
342
|
-
require 'aspera/faspex_postproc'
|
343
|
-
parameters =
|
344
|
-
raise 'parameters must be Hash' unless parameters.is_a?(Hash)
|
561
|
+
require 'aspera/faspex_postproc' # cspell:disable-line
|
562
|
+
parameters = value_create_modify(type: Hash)
|
345
563
|
parameters = parameters.symbolize_keys
|
346
564
|
raise 'Missing key: url' unless parameters.key?(:url)
|
347
565
|
uri = URI.parse(parameters[:url])
|
@@ -349,14 +567,45 @@ module Aspera
|
|
349
567
|
parameters[:processing][:root] = uri.path
|
350
568
|
server = WebServerSimple.new(uri, certificate: parameters[:certificate])
|
351
569
|
server.mount(uri.path, Faspex4PostProcServlet, parameters[:processing])
|
570
|
+
# on ctrl-c, tell server main loop to exit
|
352
571
|
trap('INT') { server.shutdown }
|
353
|
-
formatter.display_status("Faspex 4 post processing listening on #{uri.port}")
|
572
|
+
formatter.display_status("Web-hook for Faspex 4-style post processing listening on #{uri.port}")
|
354
573
|
Log.log.info("Listening on #{uri.port}")
|
355
574
|
# this is blocking until server exits
|
356
575
|
server.start
|
357
576
|
return Main.result_status('Gateway terminated')
|
358
577
|
end # case command
|
359
578
|
end # action
|
579
|
+
|
580
|
+
def wizard(params)
|
581
|
+
if params[:prepare]
|
582
|
+
# if not defined by user, generate unique name
|
583
|
+
params[:preset_name] ||= [params[:plugin_sym]].concat(URI.parse(params[:instance_url]).host.gsub(/[^a-z0-9.]/, '').split('.')).join('_')
|
584
|
+
params[:need_private_key] = true
|
585
|
+
return
|
586
|
+
end
|
587
|
+
formatter.display_status('Ask the ascli client id and secret to your Administrator, or ask them to go to:'.red)
|
588
|
+
OpenApplication.instance.uri(params[:instance_url])
|
589
|
+
formatter.display_status('Then: 𓃑 → Admin → Configurations → API clients')
|
590
|
+
formatter.display_status('Create an API client with:')
|
591
|
+
formatter.display_status('- name: ascli')
|
592
|
+
formatter.display_status('- JWT: enabled')
|
593
|
+
formatter.display_status('Then, logged in as user go to your profile:')
|
594
|
+
formatter.display_status('👤 → Account Settings → Preferences -> Public Key in PEM:')
|
595
|
+
formatter.display_status(params[:pub_key_pem])
|
596
|
+
formatter.display_status('Once set, fill in the parameters:')
|
597
|
+
return {
|
598
|
+
preset_value: {
|
599
|
+
url: params[:instance_url],
|
600
|
+
username: options.get_option(:username, mandatory: true),
|
601
|
+
auth: :jwt.to_s,
|
602
|
+
private_key: '@file:' + params[:private_key_path],
|
603
|
+
client_id: options.get_option(:client_id, mandatory: true),
|
604
|
+
client_secret: options.get_option(:client_secret, mandatory: true)
|
605
|
+
},
|
606
|
+
test_args: "#{params[:plugin_sym]} user profile show"
|
607
|
+
}
|
608
|
+
end
|
360
609
|
end # Faspex5
|
361
610
|
end # Plugins
|
362
611
|
end # Cli
|