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