aspera-cli 4.26.2 → 4.27.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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -6,6 +6,7 @@ require 'aspera/cli/plugins/node'
6
6
  require 'aspera/cli/extended_value'
7
7
  require 'aspera/cli/special_values'
8
8
  require 'aspera/cli/transfer_agent'
9
+ require 'aspera/transfer/result'
9
10
  require 'aspera/transfer/uri'
10
11
  require 'aspera/transfer/spec'
11
12
  require 'aspera/persistency_action_once'
@@ -22,6 +23,7 @@ module Aspera
22
23
  module Cli
23
24
  module Plugins
24
25
  class Faspex < BasicAuth
26
+ application_name 'Faspex v4'
25
27
  # required hash key for source in config
26
28
  KEY_NODE = 'node' # value must be hash with url, username, password
27
29
  KEY_PATH = 'path' # value must be same sub-path as in Faspex's node
@@ -94,7 +96,7 @@ module Aspera
94
96
  # get Transfer::Uri::SCHEME URI from entry in xml, and fix problems.
95
97
  def get_fasp_uri_from_entry(entry, raise_no_link: true)
96
98
  unless entry.key?('link')
97
- raise Cli::BadArgument, 'package has no link (deleted?)' if raise_no_link
99
+ Aspera.assert(!raise_no_link, type: Cli::BadArgument){'package has no link (deleted?)'}
98
100
  return
99
101
  end
100
102
  result = entry['link'].find{ |e| e['rel'].eql?('package')}['href']
@@ -123,16 +125,17 @@ module Aspera
123
125
  }
124
126
  end
125
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
+
126
135
  def initialize(**_)
127
136
  super
128
137
  @api_v3 = nil
129
138
  @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
139
  options.parse_options!
137
140
  end
138
141
 
@@ -242,7 +245,7 @@ module Aspera
242
245
  delivery_info = package_create_params['delivery']
243
246
  # pub link user
244
247
  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])
248
+ Aspera.assert_values(link_data[:subpath], ['external/submissions/new', 'external/dropbox_submissions/new'], type: Cli::BadArgument){'pub link subpath'}
246
249
  create_path = link_data[:subpath].split('/')[0..-2].join('/')
247
250
  package_create_params[:passcode] = link_data[:query]['passcode']
248
251
  delivery_info[:transfer_type] = 'connect'
@@ -273,265 +276,299 @@ module Aspera
273
276
  return package_creation_data.first
274
277
  end
275
278
 
276
- ACTIONS = %i[health package source me dropbox v4 address_book login_methods].freeze
279
+ # --- DSL ---
277
280
 
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)
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)
288
363
  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)
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')
436
369
  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
- }
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
477
398
  )
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
399
+ )
481
400
  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])
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)}]
491
426
  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")
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 ?'
511
444
  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
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
525
477
  end
526
- u['first_name'], u['last_name'] = u['displayName'].split(' ', 2)
527
- u['x'] = true
478
+ transfer_spec['direction'] = Transfer::Spec::DIRECTION_RECEIVE
479
+ statuses = transfer.start(transfer_spec)
528
480
  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'])
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)
534
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'])
535
572
  end
536
573
  end
537
574
  end