aspera-cli 4.27.0 → 4.27.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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +297 -313
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +16 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +12 -12
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -1,576 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # cspell:ignore passcode xrds workgroups dmembership wmembership
4
- require 'aspera/cli/plugins/basic_auth'
5
- require 'aspera/cli/plugins/node'
6
- require 'aspera/cli/extended_value'
7
- require 'aspera/cli/special_values'
8
- require 'aspera/cli/transfer_agent'
9
- require 'aspera/transfer/result'
10
- require 'aspera/transfer/uri'
11
- require 'aspera/transfer/spec'
12
- require 'aspera/persistency_action_once'
13
- require 'aspera/rest_list'
14
- require 'aspera/environment'
15
- require 'aspera/nagios'
16
- require 'aspera/id_generator'
17
- require 'aspera/log'
18
- require 'aspera/assert'
19
- require 'xmlsimple'
20
- require 'json'
21
-
22
- module Aspera
23
- module Cli
24
- module Plugins
25
- class Faspex < BasicAuth
26
- application_name 'Faspex v4'
27
- # required hash key for source in config
28
- KEY_NODE = 'node' # value must be hash with url, username, password
29
- KEY_PATH = 'path' # value must be same sub-path as in Faspex's node
30
- # added field in result that identifies the package
31
- PACKAGE_MATCH_FIELD = 'package_id'
32
- # list of supported atoms
33
- ATOM_MAILBOXES = %i[inbox archive sent].freeze
34
- # allowed parameters for inbox.atom
35
- ATOM_PARAMS = %w[page count startIndex].freeze
36
- # with special parameters (from Plugin class) : max and pmax (from Plugin)
37
- ATOM_EXT_PARAMS = [RestList::MAX_ITEMS, RestList::MAX_PAGES].concat(ATOM_PARAMS).freeze
38
- # sub path in url for public link delivery
39
- PUB_LINK_EXTERNAL_MATCH = 'external_deliveries/'
40
- STANDARD_PATH = '/aspera/faspex'
41
- HEADER_FASPEX_VERSION = 'X-IBM-Aspera'
42
- private_constant :KEY_NODE, :KEY_PATH, :PACKAGE_MATCH_FIELD, :ATOM_MAILBOXES, :ATOM_PARAMS, :ATOM_EXT_PARAMS, :PUB_LINK_EXTERNAL_MATCH, :HEADER_FASPEX_VERSION
43
-
44
- class << self
45
- # @return [Hash,NilClass]
46
- def detect(address_or_url)
47
- address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
48
- urls = [address_or_url]
49
- urls.push("#{address_or_url}#{STANDARD_PATH}") unless address_or_url.end_with?(STANDARD_PATH)
50
- error = nil
51
- urls.each do |base_url|
52
- next unless base_url.start_with?('https://')
53
- api = Rest.new(base_url: base_url, redirect_max: 1)
54
- http = api.call(
55
- operation: 'POST',
56
- headers: {
57
- 'Content-type' => Mime::TEXT,
58
- 'Accept' => 'application/xrds+xml'
59
- },
60
- ret: :resp
61
- )
62
- # 4.x
63
- next unless http.body.start_with?('<?xml')
64
- res_s = XmlSimple.xml_in(http.body, {'ForceArray' => false})
65
- Log.log.debug{"version: #{http[HEADER_FASPEX_VERSION]}"}
66
- version = res_s['XRD']['application']['version']
67
- # take redirect if any
68
- return {
69
- version: version,
70
- url: http.uri.to_s
71
- }
72
- rescue StandardError => e
73
- error = e
74
- Log.log.debug{"detect error: #{e}"}
75
- end
76
- raise error if error
77
- return
78
- end
79
-
80
- # extract elements from faspex public link
81
- def get_link_data(public_url)
82
- public_uri = URI.parse(public_url)
83
- Aspera.assert(m = public_uri.path.match(%r{^(.*)/(external.*)$}), 'Public link does not match Faspex format', type: Cli::BadArgument)
84
- base = m[1]
85
- subpath = m[2]
86
- port_add = public_uri.port.eql?(public_uri.default_port) ? '' : ":#{public_uri.port}"
87
- result = {
88
- base_url: "#{public_uri.scheme}://#{public_uri.host}#{port_add}#{base}",
89
- subpath: subpath,
90
- query: Rest.query_to_h(public_uri.query)
91
- }
92
- Log.dump(:link_data, result)
93
- return result
94
- end
95
-
96
- # get Transfer::Uri::SCHEME URI from entry in xml, and fix problems.
97
- def get_fasp_uri_from_entry(entry, raise_no_link: true)
98
- unless entry.key?('link')
99
- Aspera.assert(!raise_no_link, type: Cli::BadArgument){'package has no link (deleted?)'}
100
- return
101
- end
102
- result = entry['link'].find{ |e| e['rel'].eql?('package')}['href']
103
- return result
104
- end
105
-
106
- # @return [Integer] identifier of source
107
- def get_source_id_by_name(source_name, source_list)
108
- match_source = source_list.find{ |i| i['name'].eql?(source_name)}
109
- return match_source['id'] unless match_source.nil?
110
- raise Cli::Error, %Q(No such Faspex source: "#{source_name}" in [#{source_list.map{ |i| %Q("#{i['name']}")}.join(', ')}])
111
- end
112
- end
113
-
114
- # @param wizard [Wizard] The wizard object
115
- # @param app_url [String] Tested URL
116
- # @return [Hash] :preset_value, :test_args
117
- def wizard(wizard, app_url)
118
- return {
119
- preset_value: {
120
- url: app_url,
121
- username: options.get_option(:username, mandatory: true),
122
- password: options.get_option(:password, mandatory: true)
123
- },
124
- test_args: 'me'
125
- }
126
- end
127
-
128
- option :link, description: 'Public link for specific operation'
129
- option :delivery_info, description: 'Package delivery information', allowed: Hash
130
- option :remote_source, description: 'Remote source for package send (id or %name:)'
131
- option :storage, description: 'Faspex local storage definition (for browsing source)'
132
- option :recipient, description: 'Use if recipient is a dropbox (with *)'
133
- option :box, description: 'Package box', allowed: ATOM_MAILBOXES, default: :inbox
134
-
135
- def initialize(**_)
136
- super
137
- @api_v3 = nil
138
- @api_v4 = nil
139
- options.parse_options!
140
- end
141
-
142
- def api_v3
143
- @api_v3 = basic_auth_api if @api_v3.nil?
144
- return @api_v3
145
- end
146
-
147
- def api_v4
148
- if @api_v4.nil?
149
- faspex_api_base = options.get_option(:url, mandatory: true)
150
- @api_v4 = Rest.new(
151
- base_url: "#{faspex_api_base}/api",
152
- auth: {
153
- type: :oauth2,
154
- grant_method: :generic,
155
- base_url: "#{faspex_api_base}/auth/oauth2",
156
- auth: {type: :basic, username: options.get_option(:username, mandatory: true), password: options.get_option(:password, mandatory: true)},
157
- params: {
158
- scope: 'admin'
159
- },
160
- grant_type: 'password'
161
- }
162
- )
163
- end
164
- return @api_v4
165
- end
166
-
167
- # query supports : {"startIndex":10,"count":1,"page":109,"max":2,"pmax":1}
168
- def mailbox_filtered_entries(stop_at_id: nil)
169
- recipient_names = [options.get_option(:recipient) || options.get_option(:username, mandatory: true)]
170
- # some workgroup messages have no star in recipient name
171
- recipient_names.push(recipient_names.first.delete_prefix('*')) if recipient_names.first.start_with?('*')
172
- # mailbox is in ATOM_MAILBOXES
173
- mailbox = options.get_option(:box, mandatory: true)
174
- # parameters
175
- mailbox_query = options.get_option(:query)
176
- max_items = nil
177
- max_pages = nil
178
- result = []
179
- if !mailbox_query.nil?
180
- Aspera.assert_type(mailbox_query, Hash){'query'}
181
- Aspera.assert((mailbox_query.keys - ATOM_EXT_PARAMS).empty?){"query: supported params: #{ATOM_EXT_PARAMS}"}
182
- Aspera.assert(!(mailbox_query.key?('startIndex') && mailbox_query.key?('page')), 'query: startIndex and page are exclusive')
183
- # Extract pagination control parameters (not part of API query)
184
- max_items = mailbox_query.delete(RestList::MAX_ITEMS)
185
- max_pages = mailbox_query.delete(RestList::MAX_PAGES)
186
- end
187
- loop do
188
- # get a batch of package information
189
- # order: first batch is latest packages, and then in a batch ids are increasing
190
- atom_xml = api_v3.call(
191
- operation: 'GET',
192
- subpath: "#{mailbox}.atom",
193
- headers: {'Accept' => 'application/xml'},
194
- query: mailbox_query,
195
- ret: :resp
196
- ).body
197
- box_data = XmlSimple.xml_in(atom_xml, {'ForceArray' => %w[entry field link to]})
198
- Log.dump(:box_data, box_data)
199
- items = box_data.key?('entry') ? box_data['entry'] : []
200
- Log.log.debug{"new items: #{items.count}"}
201
- # it is the end if page is empty
202
- break if items.empty?
203
- stop_condition = false
204
- # results will be sorted in reverse id
205
- items.reverse_each do |package|
206
- # create the package id, based on recipient's box
207
- package[PACKAGE_MATCH_FIELD] =
208
- case mailbox
209
- when :inbox, :archive
210
- recipient = package['to'].find{ |i| recipient_names.include?(i['name'])}
211
- recipient.nil? ? nil : recipient['recipient_delivery_id']
212
- else # :sent
213
- package['delivery_id']
214
- end
215
- # add special key
216
- package['items'] = package['link'].is_a?(Array) ? package['link'].length : 0
217
- package['metadata'] = package['metadata']['field'].to_h{ |i| [i['name'], i['content']]}
218
- # if we look for a specific package
219
- stop_condition = true if !stop_at_id.nil? && stop_at_id.eql?(package[PACKAGE_MATCH_FIELD])
220
- # keep only those for the specified recipient
221
- result.push(package) unless package[PACKAGE_MATCH_FIELD].nil?
222
- end
223
- break if stop_condition
224
- # result.push({PACKAGE_MATCH_FIELD=>'======'})
225
- Log.log.debug{"total items: #{result.count}"}
226
- # reach the limit ?
227
- if !max_items.nil? && (result.count >= max_items)
228
- result = result.slice(0, max_items) if result.count > max_items
229
- break
230
- end
231
- link = box_data['link'].find{ |i| i['rel'].eql?('next')}
232
- Log.log.debug{"link: #{link}"}
233
- # no next link
234
- break if link.nil?
235
- # replace parameters with the ones from next link
236
- mailbox_query = Rest.query_to_h(URI.parse(link['href']).query)
237
- Log.log.debug{"query: #{mailbox_query}"}
238
- break if !max_pages.nil? && (mailbox_query['page'].to_i > max_pages)
239
- end
240
- return result
241
- end
242
-
243
- # retrieve transfer spec from pub link for send package
244
- def send_public_link_to_ts(public_link_url, package_create_params)
245
- delivery_info = package_create_params['delivery']
246
- # pub link user
247
- link_data = self.class.get_link_data(public_link_url)
248
- Aspera.assert_values(link_data[:subpath], ['external/submissions/new', 'external/dropbox_submissions/new'], type: Cli::BadArgument){'pub link subpath'}
249
- create_path = link_data[:subpath].split('/')[0..-2].join('/')
250
- package_create_params[:passcode] = link_data[:query]['passcode']
251
- delivery_info[:transfer_type] = 'connect'
252
- delivery_info[:source_paths_list] = transfer.source_list.join("\r\n")
253
- api_public_link = Rest.new(base_url: link_data[:base_url])
254
- # Hum, as this does not always work (only user, but not dropbox), we get the javascript and need hack
255
- # pkg_created=api_public_link.create(create_path,package_create_params)
256
- # so extract data from javascript
257
- package_creation_data = api_public_link.call(
258
- operation: 'POST',
259
- subpath: create_path,
260
- content_type: Mime::JSON,
261
- body: package_create_params,
262
- headers: {'Accept' => 'text/javascript'},
263
- ret: :resp
264
- ).body
265
- # get arguments of function call
266
- package_creation_data.delete!("\n") # one line
267
- package_creation_data.gsub!(/^[^"]+\("\{/, '{') # delete header
268
- package_creation_data.gsub!(/"\);[^"]+$/, '"') # delete trailer
269
- package_creation_data.gsub!(/\}", *"/, '},"') # between two arguments
270
- package_creation_data.gsub!('\\"', '"') # remove protecting quote
271
- begin
272
- package_creation_data = JSON.parse("[#{package_creation_data}]")
273
- rescue JSON::ParserError # => e
274
- raise Aspera::Error, 'Unexpected response: missing metadata ?'
275
- end
276
- return package_creation_data.first
277
- end
278
-
279
- # --- DSL ---
280
-
281
- command :health, description: 'Check Faspex 4 API health'
282
- command :package, description: 'Manage packages'
283
- command :source, description: 'Manage sources'
284
- command :me, description: 'Show current user information', action: ->{Result::SingleObject.new(api_v3.read('me'))}
285
- command :dropbox, description: 'Manage dropboxes'
286
- command :v4, description: 'Faspex v4 admin commands'
287
- command :address_book, description: 'Show address book'
288
- command :login_methods, description: 'Show login methods'
289
-
290
- commands_under(:package) do
291
- command :send, description: 'Send a package'
292
- command :receive, description: 'Receive a package', aliases: [:recv]
293
- command :list, description: 'List packages', action: ->{Result::ObjectList.new(mailbox_filtered_entries, fields: [PACKAGE_MATCH_FIELD, 'title', 'items'])}
294
- command :show, description: 'Show a package'
295
- end
296
-
297
- commands_under(:source) do
298
- command :list, description: 'List sources', action: ->{Result::ObjectList.new(api_v3.read('source_shares')['items'])}
299
- command :info, description: 'Show source info', setup: :setup_source_selected, action: ->(source_info:){Result::SingleObject.new(source_info)}
300
- command :node, description: 'Execute node commands on source', setup: :setup_source_selected
301
- end
302
-
303
- commands_under(%i[source node]) do
304
- Node::COMMANDS_FASPEX.each do |cmd|
305
- command(cmd, description: "Node #{cmd} command", setup: :setup_source_node)
306
- end
307
- end
308
-
309
- commands_under(:dropbox) do
310
- command :list, description: 'List dropboxes'
311
- end
312
-
313
- commands_under(:v4) do
314
- entity_command :dropbox, description: 'Manage v4 dropboxes', api: :api_v4, entity: 'admin/dropboxes', display_fields: %w[id e_wg_name e_wg_desc created_at]
315
- entity_command :dmembership, description: 'Manage dropbox memberships', api: :api_v4, entity: 'dropbox_memberships'
316
- entity_command :workgroup, api: :api_v4, entity: 'admin/workgroups', display_fields: %w[id e_wg_name e_wg_desc created_at]
317
- entity_command :wmembership, description: 'Manage workgroup memberships', api: :api_v4, entity: 'workgroup_memberships'
318
- entity_command :user, api: :api_v4, entity: 'users', display_fields: %w[id name first_name last_name]
319
- entity_command :metadata_profile, description: 'Manage metadata profiles', api: :api_v4, entity: 'metadata_profiles'
320
- command :package, description: 'List packages for a box'
321
- end
322
-
323
- commands_under(%i[v4 package]) do
324
- command :users, description: 'List packages for users'
325
- end
326
-
327
- # --- handlers ---
328
-
329
- def action_health
330
- nagios = Nagios.new
331
- begin
332
- api_v3.read('me')
333
- nagios.add_ok('faspex api', 'accessible')
334
- rescue StandardError => e
335
- nagios.add_critical('faspex api', e.to_s)
336
- end
337
- Result::ObjectList.new(nagios.status_list)
338
- end
339
-
340
- # package sub-handlers
341
-
342
- def action_package_show
343
- delivery_id = options.instance_identifier
344
- Result::SingleObject.new(mailbox_filtered_entries(stop_at_id: delivery_id).find{ |p| p[PACKAGE_MATCH_FIELD].eql?(delivery_id)})
345
- end
346
-
347
- def action_package_send
348
- delivery_info = options.get_option(:delivery_info, mandatory: true)
349
- Aspera.assert_type(delivery_info, Hash, type: Cli::BadArgument){'delivery_info'}
350
- # actual parameter to faspex API
351
- package_create_params = {'delivery' => delivery_info}
352
- public_link_url = options.get_option(:link)
353
- if public_link_url.nil?
354
- # authenticated user
355
- delivery_info['sources'] ||= [{'paths' => []}]
356
- first_source = delivery_info['sources'].first
357
- first_source['paths'].concat(transfer.source_list)
358
- source_id = options.get_option(:remote_source)
359
- if source_id && (m = Parser.percent_selector(source_id))
360
- Aspera.assert_values(m[:field], ['name'], type: Cli::BadArgument){'selector field'}
361
- source_list = api_v3.read('source_shares')['items']
362
- source_id = self.class.get_source_id_by_name(m[:value], source_list)
363
- end
364
- first_source['id'] = source_id.to_i unless source_id.nil?
365
- pkg_created = api_v3.create('send', package_create_params)
366
- if first_source.key?('id')
367
- # no transfer spec if remote source: handled by faspex
368
- return Result::ValueList.new([pkg_created['links']['status']], name: 'link')
369
- end
370
- Aspera.assert(pkg_created['xfer_sessions'].length == 1, type: Cli::BadArgument){'expecting one session exactly'}
371
- transfer_spec = pkg_created['xfer_sessions'].first
372
- # use source from cmd line, this one only contains destination (already in dest root)
373
- transfer_spec.delete('paths')
374
- else # public link
375
- transfer_spec = send_public_link_to_ts(public_link_url, package_create_params)
376
- end
377
- # Log.dump(:transfer_spec,transfer_spec)
378
- return Runner.result_transfer(transfer.start(transfer_spec))
379
- end
380
-
381
- def action_package_receive
382
- link_url = options.get_option(:link)
383
- # list of faspex ID/URI to download
384
- pkg_id_uri = nil
385
- skip_ids_data = []
386
- skip_ids_persistency = nil
387
- case link_url
388
- when nil # usual case: no link
389
- if options.get_option(:once_only, mandatory: true)
390
- skip_ids_persistency = PersistencyActionOnce.new(
391
- manager: persistency,
392
- data: skip_ids_data,
393
- id: IdGenerator.from_list(
394
- 'faspex_recv',
395
- options.get_option(:url, mandatory: true),
396
- options.get_option(:username, mandatory: true),
397
- options.get_option(:box, mandatory: true).to_s
398
- )
399
- )
400
- end
401
- # get command line parameters
402
- delivery_id = options.instance_identifier
403
- Aspera.assert(!delivery_id.empty?, 'empty id')
404
- recipient = options.get_option(:recipient)
405
- if delivery_id.eql?(SpecialValues::ALL)
406
- pkg_id_uri = mailbox_filtered_entries.map{ |i| {id: i[PACKAGE_MATCH_FIELD], uri: self.class.get_fasp_uri_from_entry(i, raise_no_link: false)}}
407
- elsif delivery_id.eql?(SpecialValues::INIT)
408
- Aspera.assert(skip_ids_persistency, 'Only with option once_only')
409
- skip_ids_persistency.data.clear.concat(mailbox_filtered_entries.map{ |i| {id: i[PACKAGE_MATCH_FIELD]}})
410
- skip_ids_persistency.save
411
- return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
412
- elsif !recipient.nil? && recipient.start_with?('*')
413
- found_package_link = mailbox_filtered_entries(stop_at_id: delivery_id).find{ |p| p[PACKAGE_MATCH_FIELD].eql?(delivery_id)}['link'].first['href']
414
- raise "Not Found. Dropbox and Workgroup packages can use the link option with #{Transfer::Uri::SCHEME}" if found_package_link.nil?
415
- pkg_id_uri = [{id: delivery_id, uri: found_package_link}]
416
- else
417
- # TODO: delivery id is the right one if package was receive by workgroup
418
- endpoint =
419
- case options.get_option(:box, mandatory: true)
420
- when :inbox, :archive then 'received'
421
- when :sent then 'sent'
422
- end
423
- entry_xml = api_v3.call(operation: 'GET', subpath: "#{endpoint}/#{delivery_id}", headers: {'Accept' => 'application/xml'}, ret: :resp).body
424
- package_entry = XmlSimple.xml_in(entry_xml, {'ForceArray' => true})
425
- pkg_id_uri = [{id: delivery_id, uri: self.class.get_fasp_uri_from_entry(package_entry)}]
426
- end
427
- when /^#{Transfer::Uri::SCHEME}:/o
428
- pkg_id_uri = [{id: 'package', uri: link_url}]
429
- else
430
- link_data = self.class.get_link_data(link_url)
431
- Aspera.assert(link_data[:subpath].start_with?(PUB_LINK_EXTERNAL_MATCH), type: Cli::BadArgument){"Pub link is #{link_data[:subpath]}. Expecting #{PUB_LINK_EXTERNAL_MATCH}"}
432
- # NOTE: unauthenticated API (authorization is in url params)
433
- api_public_link = Rest.new(base_url: link_data[:base_url])
434
- pkg_xml = api_public_link.call(
435
- operation: 'GET',
436
- subpath: link_data[:subpath],
437
- headers: {'Accept' => 'application/xml'},
438
- query: {passcode: link_data[:query]['passcode']},
439
- ret: :resp
440
- ).body
441
- if !pkg_xml.start_with?('<?xml ')
442
- Environment.instance.open_uri(link_url)
443
- raise Cli::Error, 'Unexpected response: package not found ?'
444
- end
445
- package_entry = XmlSimple.xml_in(pkg_xml, {'ForceArray' => false})
446
- Log.dump(:package_entry, package_entry)
447
- transfer_uri = self.class.get_fasp_uri_from_entry(package_entry)
448
- pkg_id_uri = [{id: package_entry['id'], uri: transfer_uri}]
449
- end
450
- # prune packages already downloaded
451
- # TODO : remove ids from skip not present in inbox to avoid growing too big
452
- # skip_ids_data.select!{|id|pkg_id_uri.select{|p|p[:id].eql?(id)}}
453
- pkg_id_uri.reject!{ |i| skip_ids_data.include?(i[:id])}
454
- Log.dump(:pkg_id_uri, pkg_id_uri)
455
- return Result::Status.new('no new package') if pkg_id_uri.empty?
456
- result_transfer = []
457
- pkg_id_uri.each do |id_uri|
458
- if id_uri[:uri].nil?
459
- # skip package with no link: empty or content deleted
460
- statuses = Transfer::Result.success
461
- else
462
- transfer_spec = Transfer::Uri.new(id_uri[:uri]).transfer_spec
463
- # NOTE: only external users have token in Transfer::Uri::SCHEME link !
464
- if !transfer_spec.key?('token')
465
- sanitized = id_uri[:uri].gsub('&', '&amp;')
466
- xml_payload =
467
- %Q(<?xml version="1.0" encoding="UTF-8"?><url-list xmlns="http://schemas.asperasoft.com/xml/url-list"><url href="#{sanitized}"/></url-list>)
468
- transfer_spec['token'] = api_v3.call(
469
- operation: 'POST',
470
- subpath: 'issue-token',
471
- query: {'direction' => 'down'},
472
- content_type: Mime::TEXT,
473
- body: xml_payload,
474
- headers: {'Accept' => Mime::TEXT, 'Content-Type' => 'application/vnd.aspera.url-list+xml'},
475
- ret: :resp
476
- ).body
477
- end
478
- transfer_spec['direction'] = Transfer::Spec::DIRECTION_RECEIVE
479
- statuses = transfer.start(transfer_spec)
480
- end
481
- result_transfer.push({'package' => id_uri[:id], Runner::STATUS_FIELD => statuses})
482
- # skip only if all sessions completed
483
- skip_ids_data.push(id_uri[:id]) if statuses.is_a?(Transfer::Result::Success)
484
- end
485
- skip_ids_persistency&.save
486
- return Runner.result_transfer_multiple(result_transfer)
487
- end
488
-
489
- # Resolve a source by id/name and validate the storage option.
490
- # @return [Hash] ctx with :source_info and :source_node_plugin
491
- def setup_source_selected(**)
492
- source_list = api_v3.read('source_shares')['items']
493
- source_id = options.instance_identifier do |field, value|
494
- Aspera.assert_values(field, ['name'], type: Cli::BadArgument){'selector field'}
495
- self.class.get_source_id_by_name(value, source_list)
496
- end.to_i
497
- selected_source = source_list.find{ |i| i['id'].eql?(source_id)}
498
- Aspera.assert(!selected_source.nil?, type: BadArgument){'No such source'}
499
- source_name = selected_source['name']
500
- source_hash = options.get_option(:storage, mandatory: true)
501
- Aspera.assert_type(source_hash, Hash, type: Cli::Error){'storage option'}
502
- source_hash.each do |name, storage|
503
- Aspera.assert_type(storage, Hash, type: Cli::Error){"storage '#{name}'"}
504
- [KEY_NODE, KEY_PATH].each do |key|
505
- Aspera.assert(storage.key?(key), type: Cli::Error){"storage '#{name}' must have a '#{key}'"}
506
- end
507
- end
508
- Aspera.assert(source_hash.key?(source_name), type: Cli::Error){"No such storage in config file: \"#{source_name}\" in [#{source_hash.keys.join(', ')}]"}
509
- {source_info: source_hash[source_name]}
510
- end
511
-
512
- # source > node sub-handlers: one per COMMANDS_FASPEX
513
- # setup_source_selected injects :source_info; setup_source_node builds the Node plugin.
514
- def setup_source_node(source_info:, **)
515
- Log.dump(:source_info, source_info)
516
- node_config = ExtendedValue.instance.evaluate(source_info[KEY_NODE], context: 'faspex node')
517
- Log.log.debug{"node=#{node_config}"}
518
- Aspera.assert_type(node_config, Hash, type: Cli::Error){source_info[KEY_NODE]}
519
- api_node = Rest.new(
520
- base_url: node_config['url'],
521
- auth: {
522
- type: :basic,
523
- username: node_config['username'],
524
- password: node_config['password']
525
- }
526
- )
527
- {source_node_plugin: Node.new(context: context, api: api_node, prefix_path: source_info[KEY_PATH])}
528
- end
529
-
530
- Node::COMMANDS_FASPEX.each do |cmd|
531
- define_action_method([:source, :node, cmd]) do |source_node_plugin:, **|
532
- source_node_plugin.dispatch_v3_command(cmd)
533
- end
534
- end
535
-
536
- # dropbox sub-handler
537
-
538
- def action_dropbox_list
539
- dropbox_list = api_v3.read('dropboxes')
540
- Result::ObjectList.new(dropbox_list['items'], fields: %w[name id description can_read can_write])
541
- end
542
-
543
- def action_v4_package_users
544
- pkg_box_id = options.instance_identifier
545
- entity_execute(api: api_v4, entity: "users/#{pkg_box_id}/packages", command: :list)
546
- end
547
-
548
- def action_address_book
549
- result = api_v3.read('address-book', {'format' => 'json', 'count' => 100_000})
550
- formatter.display_status("users: #{result['itemsPerPage']}/#{result['totalResults']}, start:#{result['startIndex']}")
551
- users = result['entry']
552
- # add missing entries
553
- users.each do |u|
554
- unless u['emails'].nil?
555
- email = u['emails'].find{ |i| i['primary'].eql?('true')}
556
- u['email'] = email['value'] unless email.nil?
557
- end
558
- if u['email'].nil?
559
- Log.log.warn{"Skip user without email: #{u}"}
560
- next
561
- end
562
- u['first_name'], u['last_name'] = u['displayName'].split(' ', 2)
563
- u['x'] = true
564
- end
565
- return Result::ObjectList.new(users)
566
- end
567
-
568
- def action_login_methods
569
- login_meths = api_v3.call(operation: 'GET', subpath: 'login/new', headers: {'Accept' => 'application/xrds+xml'}, ret: :resp).body
570
- login_methods = XmlSimple.xml_in(login_meths, {'ForceArray' => false})
571
- return Result::ObjectList.new(login_methods['XRD']['Service'])
572
- end
573
- end
574
- end
575
- end
576
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate faspe
4
-
5
- require 'aspera/log'
6
- require 'aspera/rest'
7
- require 'aspera/command_line_converter'
8
-
9
- module Aspera
10
- module Transfer
11
- # translates a "faspe:" URI (used in Faspex 4) into transfer spec (Hash)
12
- class Uri
13
- SCHEME = 'faspe'
14
- def initialize(fasp_link)
15
- @fasp_uri = URI.parse(fasp_link.gsub(' ', '%20'))
16
- Aspera.assert_values(@fasp_uri.scheme, [SCHEME]){"Invalid scheme: #{@fasp_uri.scheme}"}
17
- end
18
-
19
- # Generate transfer spec from provided faspe: URL
20
- def transfer_spec
21
- result_ts = {}
22
- result_ts['remote_host'] = @fasp_uri.host
23
- result_ts['remote_user'] = @fasp_uri.user
24
- result_ts['ssh_port'] = @fasp_uri.port
25
- result_ts['paths'] = [{'source' => URI.decode_www_form_component(@fasp_uri.path)}]
26
- # faspex 4 does not encode trailing base64 padding, fix that to be able to decode properly
27
- fixed_query = @fasp_uri.query.gsub(/(=+)$/){ |trail_equals| '%3D' * trail_equals.length}
28
-
29
- Rest.query_to_h(fixed_query).each do |name, value|
30
- case name
31
- when 'cookie' then result_ts['cookie'] = value
32
- when 'token' then result_ts['token'] = value
33
- when 'sshfp' then result_ts['sshfp'] = value
34
- when 'policy' then result_ts['rate_policy'] = value
35
- when 'httpport' then result_ts['http_fallback_port'] = value.to_i
36
- when 'targetrate' then result_ts['target_rate_kbps'] = value.to_i
37
- when 'minrate' then result_ts['min_rate_kbps'] = value.to_i
38
- when 'port' then result_ts['fasp_port'] = value.to_i
39
- when 'bwcap' then result_ts['target_rate_cap_kbps'] = value.to_i
40
- when 'enc' then result_ts['cipher'] = value.gsub(/^aes/, 'aes-').gsub(/cfb$/, '-cfb').gsub(/gcm$/, '-gcm').gsub('--', '-')
41
- when 'tags64' then result_ts['tags'] = JSON.parse(Base64.strict_decode64(value))
42
- when 'createpath' then result_ts['create_dir'] = CommandLineConverter.yes_to_true(value)
43
- when 'fallback' then result_ts['http_fallback'] = CommandLineConverter.yes_to_true(value)
44
- when 'lockpolicy' then result_ts['lock_rate_policy'] = CommandLineConverter.yes_to_true(value)
45
- when 'lockminrate' then result_ts['lock_min_rate'] = CommandLineConverter.yes_to_true(value)
46
- when 'auth' then Log.log.debug{"ignoring #{name}=#{value}"} # Not used (yes/no)
47
- when 'v' then Log.log.debug{"ignoring #{name}=#{value}"} # rubocop:disable Lint/DuplicateBranch -- Not used (shall be 2)
48
- when 'protect' then Log.log.debug{"ignoring #{name}=#{value}"} # rubocop:disable Lint/DuplicateBranch -- TODO: what is this ?
49
- else Log.log.warn{"URI parameter ignored: #{name} = #{value}"}
50
- end
51
- end
52
- return result_ts
53
- end
54
- end
55
- end
56
- end