aspera-cli 4.22.0 → 4.24.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 (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +405 -364
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +1856 -961
  6. data/bin/ascli +2 -1
  7. data/bin/asession +4 -4
  8. data/lib/aspera/agent/base.rb +4 -0
  9. data/lib/aspera/agent/connect.rb +20 -18
  10. data/lib/aspera/agent/desktop.rb +14 -11
  11. data/lib/aspera/agent/direct.rb +39 -31
  12. data/lib/aspera/agent/httpgw.rb +2 -2
  13. data/lib/aspera/agent/node.rb +9 -11
  14. data/lib/aspera/agent/transferd.rb +18 -11
  15. data/lib/aspera/api/aoc.rb +53 -43
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +23 -22
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +35 -21
  20. data/lib/aspera/ascp/installation.rb +43 -43
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +55 -24
  23. data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
  24. data/lib/aspera/cli/error.rb +1 -1
  25. data/lib/aspera/cli/extended_value.rb +28 -29
  26. data/lib/aspera/cli/formatter.rb +191 -168
  27. data/lib/aspera/cli/hints.rb +38 -4
  28. data/lib/aspera/cli/main.rb +139 -108
  29. data/lib/aspera/cli/manager.rb +51 -31
  30. data/lib/aspera/cli/plugin.rb +149 -78
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +217 -88
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +105 -227
  35. data/lib/aspera/cli/plugins/console.rb +49 -18
  36. data/lib/aspera/cli/plugins/cos.rb +4 -4
  37. data/lib/aspera/cli/plugins/faspex.rb +45 -51
  38. data/lib/aspera/cli/plugins/faspex5.rb +162 -163
  39. data/lib/aspera/cli/plugins/faspio.rb +6 -5
  40. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  41. data/lib/aspera/cli/plugins/node.rb +233 -247
  42. data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
  43. data/lib/aspera/cli/plugins/preview.rb +26 -29
  44. data/lib/aspera/cli/plugins/server.rb +29 -28
  45. data/lib/aspera/cli/plugins/shares.rb +40 -28
  46. data/lib/aspera/cli/sync_actions.rb +101 -80
  47. data/lib/aspera/cli/transfer_agent.rb +55 -58
  48. data/lib/aspera/cli/transfer_progress.rb +29 -20
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +160 -0
  51. data/lib/aspera/colors.rb +13 -8
  52. data/lib/aspera/command_line_builder.rb +28 -22
  53. data/lib/aspera/command_line_converter.rb +31 -0
  54. data/lib/aspera/data_repository.rb +1 -0
  55. data/lib/aspera/environment.rb +144 -100
  56. data/lib/aspera/faspex_gw.rb +1 -1
  57. data/lib/aspera/faspex_postproc.rb +3 -2
  58. data/lib/aspera/hash_ext.rb +1 -1
  59. data/lib/aspera/id_generator.rb +10 -10
  60. data/lib/aspera/keychain/base.rb +18 -0
  61. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  62. data/lib/aspera/keychain/factory.rb +9 -3
  63. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  64. data/lib/aspera/keychain/macos_security.rb +13 -13
  65. data/lib/aspera/log.rb +70 -20
  66. data/lib/aspera/nagios.rb +5 -6
  67. data/lib/aspera/node_simulator.rb +12 -7
  68. data/lib/aspera/oauth/base.rb +6 -2
  69. data/lib/aspera/oauth/factory.rb +25 -18
  70. data/lib/aspera/oauth/jwt.rb +13 -1
  71. data/lib/aspera/oauth/url_json.rb +3 -3
  72. data/lib/aspera/oauth/web.rb +5 -3
  73. data/lib/aspera/persistency_folder.rb +2 -2
  74. data/lib/aspera/preview/file_types.rb +43 -35
  75. data/lib/aspera/preview/generator.rb +26 -13
  76. data/lib/aspera/preview/terminal.rb +10 -7
  77. data/lib/aspera/preview/utils.rb +11 -9
  78. data/lib/aspera/products/connect.rb +2 -1
  79. data/lib/aspera/products/desktop.rb +1 -1
  80. data/lib/aspera/products/other.rb +2 -2
  81. data/lib/aspera/products/transferd.rb +8 -6
  82. data/lib/aspera/proxy_auto_config.rb +1 -1
  83. data/lib/aspera/rest.rb +46 -28
  84. data/lib/aspera/rest_call_error.rb +1 -1
  85. data/lib/aspera/rest_error_analyzer.rb +1 -0
  86. data/lib/aspera/resumer.rb +1 -1
  87. data/lib/aspera/secret_hider.rb +46 -40
  88. data/lib/aspera/ssh.rb +14 -4
  89. data/lib/aspera/sync/args.schema.yaml +102 -0
  90. data/lib/aspera/sync/conf.schema.yaml +701 -0
  91. data/lib/aspera/sync/database.rb +83 -0
  92. data/lib/aspera/{transfer/sync.rb → sync/operations.rb} +145 -68
  93. data/lib/aspera/temp_file_manager.rb +4 -2
  94. data/lib/aspera/timer_limiter.rb +7 -5
  95. data/lib/aspera/transfer/error.rb +1 -1
  96. data/lib/aspera/transfer/error_info.rb +1 -2
  97. data/lib/aspera/transfer/faux_file.rb +11 -10
  98. data/lib/aspera/transfer/parameters.rb +6 -5
  99. data/lib/aspera/transfer/spec.rb +15 -1
  100. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  101. data/lib/aspera/transfer/spec_doc.rb +34 -16
  102. data/lib/aspera/transfer/uri.rb +5 -5
  103. data/lib/aspera/uri_reader.rb +14 -10
  104. data/lib/aspera/web_auth.rb +2 -2
  105. data/lib/aspera/web_server_simple.rb +2 -2
  106. data.tar.gz.sig +0 -0
  107. metadata +15 -15
  108. metadata.gz.sig +0 -0
  109. data/examples/dascli +0 -30
  110. data/examples/get_proto_file.rb +0 -8
  111. data/examples/proxy.pac +0 -60
  112. data/lib/aspera/transfer/convert.rb +0 -29
  113. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -13
  114. data/lib/aspera/transfer/sync_session.schema.yaml +0 -79
@@ -44,16 +44,18 @@ module Aspera
44
44
  application
45
45
  client_registration_token
46
46
  client_access_key
47
- kms_profile].freeze
47
+ kms_profile
48
+ ].freeze
48
49
  # query to list fully received packages
49
50
  PACKAGE_RECEIVED_BASE_QUERY = {
50
51
  'archived' => false,
51
52
  'has_content' => true,
52
53
  'received' => true,
53
- 'completed' => true}.freeze
54
+ 'completed' => true
55
+ }.freeze
56
+ PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
54
57
  # options and parameters for Api::AoC.new
55
58
  OPTIONS_NEW = %i[url auth client_id client_secret scope redirect_uri private_key passphrase username password workspace].freeze
56
- PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
57
59
 
58
60
  private_constant :REDIRECT_LOCALHOST, :STD_AUTH_TYPES, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :OPTIONS_NEW, :PACKAGE_LIST_DEFAULT_FIELDS
59
61
  class << self
@@ -67,12 +69,12 @@ module Aspera
67
69
  # only org provided ?
68
70
  base_url = "#{base_url}.#{Api::AoC::SAAS_DOMAIN_PROD}" unless base_url.include?('.')
69
71
  # AoC is only https
70
- return nil unless base_url.start_with?('https://')
72
+ return unless base_url.start_with?('https://')
71
73
  res_http = Rest.new(base_url: base_url, redirect_max: 0).call(operation: 'GET', subpath: 'auth/ping', return_error: true)[:http]
72
- return nil if res_http['Location'].nil?
74
+ return if res_http['Location'].nil?
73
75
  redirect_uri = URI.parse(res_http['Location'])
74
76
  od = Api::AoC.split_org_domain(URI.parse(base_url))
75
- return nil unless redirect_uri.path.end_with?("oauth2/#{od[:organization]}/login")
77
+ return unless redirect_uri.path.end_with?("oauth2/#{od[:organization]}/login")
76
78
  # either in standard domain, or product name in page
77
79
  return {
78
80
  version: Api::AoC.saas_url?(base_url) ? 'SaaS' : 'Self-managed',
@@ -150,14 +152,14 @@ module Aspera
150
152
  formatter.display_status('We will use web authentication to bootstrap.')
151
153
  auto_set_pub_key = true
152
154
  auto_set_jwt = true
153
- raise 'TODO'
155
+ Aspera.error_not_implemented
154
156
  # aoc_api.oauth.grant_method = :web
155
157
  # aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN
156
158
  # aoc_api.oauth.specific_parameters[:redirect_uri] = REDIRECT_LOCALHOST
157
159
  end
158
160
  myself = object.aoc_api.read('self')
159
161
  if auto_set_pub_key
160
- Aspera.assert(myself['public_key'].empty?, exception_class: Cli::Error){'Public key is already set in profile (use --override=yes)'} unless option_override
162
+ Aspera.assert(myself['public_key'].empty?, type: Cli::Error){'Public key is already set in profile (use --override=yes)'} unless option_override
161
163
  formatter.display_status('Updating profile with the public key.')
162
164
  aoc_api.update("users/#{myself['id']}", {'public_key' => pub_key_pem})
163
165
  end
@@ -181,6 +183,38 @@ module Aspera
181
183
  test_args: 'user profile show'
182
184
  }
183
185
  end
186
+
187
+ # @param base [String] Base folder path
188
+ # @return [String] Folder path that does jot exist, with possible .<number> extension
189
+ def next_available_folder(base, always: false)
190
+ counter = always ? 1 : 0
191
+ loop do
192
+ result = counter.zero? ? base : "#{base}.#{counter}"
193
+ return result unless Dir.exist?(result)
194
+ counter += 1
195
+ end
196
+ end
197
+
198
+ # Get folder path that does not exist
199
+ # If it exists, an extension is added
200
+ # or a sequential number if extension == :seq
201
+ # @param folder [String] base folder
202
+ def unique_folder(folder, extension: nil, always: false)
203
+ case extension
204
+ when nil
205
+ folder
206
+ when :seq
207
+ # reuse helper
208
+ next_available_folder(folder, always: always)
209
+ else
210
+ if Dir.exist?(folder) || always
211
+ # NOTE: it might already exist
212
+ "#{folder}.#{Environment.instance.sanitized_filename(extension)}"
213
+ else
214
+ folder
215
+ end
216
+ end
217
+ end
184
218
  end
185
219
 
186
220
  def initialize(**_)
@@ -191,22 +225,29 @@ module Aspera
191
225
  options.declare(:auth, 'OAuth type of authentication', values: STD_AUTH_TYPES, default: :jwt)
192
226
  options.declare(:client_id, 'OAuth API client identifier')
193
227
  options.declare(:client_secret, 'OAuth API client secret')
194
- options.declare(:scope, 'OAuth scope for AoC API calls', default: Api::AoC::SCOPE_FILES_USER)
228
+ options.declare(:scope, 'OAuth scope for AoC API calls')
195
229
  options.declare(:redirect_uri, 'OAuth API client redirect URI')
196
230
  options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
197
231
  options.declare(:passphrase, 'RSA private key passphrase', types: String)
198
232
  options.declare(:workspace, 'Name of workspace', types: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
199
233
  options.declare(:new_user_option, 'New user creation option for unknown package recipients', types: Hash)
200
234
  options.declare(:validate_metadata, 'Validate shared inbox metadata', values: :bool, default: true)
235
+ options.declare(:package_folder, 'Field of package to use as folder name, or @none:', types: [String, NilClass])
201
236
  options.parse_options!
202
237
  # add node plugin options (for manual)
203
238
  Node.declare_options(options)
204
239
  end
205
240
 
206
- def api_from_options(new_base_path)
207
- create_values = {subpath: new_base_path, secret_finder: config}
241
+ def api_from_options(aoc_base_path)
242
+ create_values = OPTIONS_NEW.each_with_object({
243
+ subpath: aoc_base_path,
244
+ secret_finder: config
245
+ }) do |i, m|
246
+ m[i] = options.get_option(i) unless options.get_option(i).nil?
247
+ end
248
+ create_values[:scope] = Api::AoC::SCOPE_FILES_USER if create_values[:scope].nil?
208
249
  # create an API object with the same options, but with a different subpath
209
- return Api::AoC.new(**OPTIONS_NEW.each_with_object(create_values){ |i, m| m[i] = options.get_option(i) unless options.get_option(i).nil?})
250
+ return Api::AoC.new(**create_values)
210
251
  rescue ArgumentError => e
211
252
  if (m = e.message.match(/missing keyword: :(.*)$/))
212
253
  raise Cli::Error, "Missing option: #{m[1]}"
@@ -249,7 +290,7 @@ module Aspera
249
290
  # @return identifier
250
291
  def get_resource_id_from_args(resource_class_path)
251
292
  return instance_identifier do |field, value|
252
- Aspera.assert(field.eql?('name'), exception_class: Cli::BadArgument){'only selection by name is supported'}
293
+ Aspera.assert(field.eql?('name'), type: Cli::BadArgument){'only selection by name is supported'}
253
294
  aoc_api.lookup_by_name(resource_class_path, value)['id']
254
295
  end
255
296
  end
@@ -262,7 +303,7 @@ module Aspera
262
303
  # Call block with same query using paging and response information
263
304
  # block must return a hash with :data and :http keys
264
305
  # @return [Hash] {items: , total: }
265
- def api_call_paging(base_query={})
306
+ def api_call_paging(base_query = {})
266
307
  Aspera.assert_type(base_query, Hash){'query'}
267
308
  Aspera.assert(block_given?)
268
309
  # set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
@@ -298,7 +339,7 @@ module Aspera
298
339
 
299
340
  # read using the query and paging
300
341
  # @return [Hash] {data: , total: }
301
- def api_read_all(resource_class_path, base_query={})
342
+ def api_read_all(resource_class_path, base_query = {})
302
343
  return api_call_paging(base_query) do |query|
303
344
  aoc_api.call(operation: 'GET', subpath: resource_class_path, headers: {'Accept' => Rest::MIME_JSON}, query: query)
304
345
  end
@@ -324,7 +365,7 @@ module Aspera
324
365
  def resolve_dropbox_name_default_ws_id(query)
325
366
  if query.key?('dropbox_name')
326
367
  # convenience: specify name instead of id
327
- raise 'Use field dropbox_name or dropbox_id, not both' if query.key?('dropbox_id')
368
+ raise BadArgument, 'Use field dropbox_name or dropbox_id, not both' if query.key?('dropbox_id')
328
369
  # TODO : craft a query that looks for dropbox only in current workspace
329
370
  query['dropbox_id'] = aoc_api.lookup_by_name('dropboxes', query.delete('dropbox_name'))['id']
330
371
  end
@@ -333,6 +374,8 @@ module Aspera
333
374
  query['exclude_dropbox_packages'] = !query.key?('dropbox_id') unless query.key?('exclude_dropbox_packages')
334
375
  end
335
376
 
377
+ # List all packages according to `query` option.
378
+ # @param <none>
336
379
  # @return [Hash] {items,total} with all packages according to combination of user's query and default query
337
380
  def list_all_packages_with_query
338
381
  query = query_read_delete(default: {})
@@ -354,7 +397,7 @@ module Aspera
354
397
  **workspace_id_hash(name: true)
355
398
  )
356
399
  file_id = top_node_api.read("access_keys/#{top_node_api.app_info[:node_info]['access_key']}")['root_file_id'] if file_id.nil?
357
- node_plugin = Node.new(**init_params, api: top_node_api)
400
+ node_plugin = Node.new(context: context, api: top_node_api)
358
401
  case command_repo
359
402
  when *Node::COMMANDS_GEN4
360
403
  return node_plugin.execute_command_gen4(command_repo, file_id)
@@ -393,8 +436,9 @@ module Aspera
393
436
  return Main.result_transfer(transfer.start(server_apfid[:api].transfer_spec_gen4(
394
437
  server_apfid[:file_id],
395
438
  client_direction,
396
- add_ts)))
397
- else Aspera.error_unreachable_line
439
+ add_ts
440
+ )))
441
+ else Aspera.error_unexpected_value(command_repo){'command'}
398
442
  end
399
443
  Aspera.error_unreachable_line
400
444
  end
@@ -443,7 +487,9 @@ module Aspera
443
487
  default_fields.push('app_type', 'app_name', 'available', 'direct_authorizations_allowed', 'workspace_authorizations_allowed')
444
488
  when :client, :client_access_key, :dropbox, :group, :package, :saml_configuration, :workspace then default_fields.push('name')
445
489
  when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
446
- when :contact then default_fields = %w[email name source_id source_type]
490
+ when :contact
491
+ default_fields = %w[source_type source_id name email]
492
+ default_query = {'include_only_user_personal_contacts' => true} if aoc_api.oauth.scope == Api::AoC::SCOPE_FILES_USER
447
493
  when :node then default_fields.push('name', 'host', 'access_key')
448
494
  when :operation then default_fields = nil
449
495
  when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
@@ -459,12 +505,12 @@ module Aspera
459
505
  return Main.result_single_object(object, fields: fields)
460
506
  when :modify
461
507
  changes = options.get_next_argument('properties', validation: Hash)
462
- return do_bulk_operation(command: command, descr: 'identifier', values: res_id) do |one_id|
508
+ return do_bulk_operation(command: command, values: res_id) do |one_id|
463
509
  aoc_api.update("#{resource_class_path}/#{one_id}", changes)
464
510
  {'id' => one_id}
465
511
  end
466
512
  when :delete
467
- return do_bulk_operation(command: command, descr: 'identifier', values: res_id) do |one_id|
513
+ return do_bulk_operation(command: command, values: res_id) do |one_id|
468
514
  aoc_api.delete("#{resource_class_path}/#{one_id}")
469
515
  {'id' => one_id}
470
516
  end
@@ -486,8 +532,8 @@ module Aspera
486
532
  ADMIN_ACTIONS = %i[ats resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
487
533
 
488
534
  def execute_admin_action
489
- # upgrade scope to admin
490
- aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN
535
+ # default scope to admin
536
+ aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN if options.get_option(:scope).nil?
491
537
  command_admin = options.get_next_command(ADMIN_ACTIONS)
492
538
  case command_admin
493
539
  when :resource
@@ -501,7 +547,7 @@ module Aspera
501
547
  when :list
502
548
  return result_list('admin/auth_providers')
503
549
  when :update
504
- raise 'not implemented'
550
+ Aspera.error_not_implemented
505
551
  end
506
552
  when :subscription
507
553
  org = aoc_api.read('organization')
@@ -603,12 +649,16 @@ module Aspera
603
649
  # cspell:enable
604
650
  result = bss_graphql.create(
605
651
  nil,
606
- {query: graphql_query,
607
- variables: {
608
- organization_id: org['id'],
609
- aggregate: aggregate,
610
- startDate: start_date,
611
- endDate: end_date}})['data']
652
+ {
653
+ query: graphql_query,
654
+ variables: {
655
+ organization_id: org['id'],
656
+ aggregate: aggregate,
657
+ startDate: start_date,
658
+ endDate: end_date
659
+ }
660
+ }
661
+ )['data']
612
662
  return Main.result_single_object(result['aoc'])
613
663
  end
614
664
  when :ats
@@ -616,13 +666,13 @@ module Aspera
616
666
  base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
617
667
  auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
618
668
  }))
619
- return Ats.new(**init_params).execute_action_gen(ats_api)
669
+ return Ats.new(context: context).execute_action_gen(ats_api)
620
670
  when :analytics
621
671
  analytics_api = Rest.new(**aoc_api.params.deep_merge({
622
672
  base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
623
673
  auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
624
674
  }))
625
- command_analytics = options.get_next_command(%i[application_events transfers])
675
+ command_analytics = options.get_next_command(%i[application_events transfers files])
626
676
  case command_analytics
627
677
  when :application_events
628
678
  event_type = command_analytics.to_s
@@ -630,15 +680,15 @@ module Aspera
630
680
  return Main.result_object_list(events)
631
681
  when :transfers
632
682
  event_type = command_analytics.to_s
633
- filter_resource = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
634
- filter_id = options.get_next_argument('identifier', mandatory: false) ||
635
- case filter_resource
683
+ event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
684
+ event_resource_id = options.get_next_argument("#{event_resource_type} identifier", mandatory: false) ||
685
+ case event_resource_type
636
686
  when :organizations then aoc_api.current_user_info['organization_id']
637
687
  when :users then aoc_api.current_user_info['id']
638
- when :nodes then aoc_api.current_user_info['id'] # TODO: consistent ? # rubocop:disable Lint/DuplicateBranch
688
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
639
689
  else Aspera.error_unreachable_line
640
690
  end
641
- filter = options.get_option(:query) || {}
691
+ filter = query_read_delete(default: {})
642
692
  filter['limit'] ||= 100
643
693
  if options.get_option(:once_only, mandatory: true)
644
694
  aoc_api.context = :files
@@ -650,18 +700,17 @@ module Aspera
650
700
  'aoc_ana_date',
651
701
  options.get_option(:url, mandatory: true),
652
702
  aoc_api.workspace[:name],
653
- filter_resource.to_s,
654
- filter_id
655
- ]))
703
+ event_resource_type.to_s,
704
+ event_resource_id
705
+ ])
706
+ )
656
707
  start_date_time = saved_date.first
657
708
  stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
658
- # Log.log().error("start: #{start_date_time}")
659
- # Log.log().error("end: #{stop_date_time}")
660
709
  saved_date[0] = stop_date_time
661
710
  filter['start_time'] = start_date_time unless start_date_time.nil?
662
711
  filter['stop_time'] = stop_date_time
663
712
  end
664
- events = analytics_api.read("#{filter_resource}/#{filter_id}/#{event_type}", query_read_delete(default: filter))[event_type]
713
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/#{event_type}", filter)[event_type]
665
714
  start_date_persistency&.save
666
715
  if !options.get_option(:notify_to).nil?
667
716
  events.each do |tr_event|
@@ -669,6 +718,22 @@ module Aspera
669
718
  end
670
719
  end
671
720
  return Main.result_object_list(events)
721
+ when :files
722
+ event_type = command_analytics.to_s
723
+ event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
724
+ event_resource_id = instance_identifier(description: "#{event_resource_type} identifier")
725
+ event_resource_id =
726
+ case event_resource_type
727
+ when :organizations then aoc_api.current_user_info['organization_id']
728
+ when :users then aoc_api.current_user_info['id']
729
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
730
+ else Aspera.error_unreachable_line
731
+ end if event_resource_id.empty?
732
+ event_uuid = instance_identifier(description: 'event uuid')
733
+ filter = query_read_delete(default: {})
734
+ filter['limit'] ||= 100
735
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/#{event_type}", filter)[event_type]
736
+ return Main.result_object_list(events)
672
737
  end
673
738
  when :usage_reports
674
739
  aoc_api.context = :files
@@ -692,20 +757,21 @@ module Aspera
692
757
  when :private then 'shared_folder_auth_link'
693
758
  else Aspera.error_unreachable_line
694
759
  end
695
- case options.get_next_command(%i[create delete list])
760
+ command = options.get_next_command(%i[create delete list show modify])
761
+ case command
696
762
  when :create
697
- creation_params = {
763
+ entity_data = {
698
764
  purpose: purpose_local,
699
765
  user_selected_name: nil
700
766
  }
701
767
  case link_type
702
768
  when :private
703
- creation_params[:data] = shared_data
769
+ entity_data[:data] = shared_data
704
770
  when :public
705
- creation_params[:expires_at] = nil
706
- creation_params[:password_enabled] = false
771
+ entity_data[:expires_at] = nil
772
+ entity_data[:password_enabled] = false
707
773
  shared_data[:name] = ''
708
- creation_params[:data] = {
774
+ entity_data[:data] = {
709
775
  aoc: true,
710
776
  url_token_data: {
711
777
  data: shared_data,
@@ -713,11 +779,17 @@ module Aspera
713
779
  }
714
780
  }
715
781
  end
716
- result_create_short_link = aoc_api.create('short_links', creation_params)
782
+ custom_data = value_create_modify(command: command, default: {})
783
+ if (pass = custom_data.delete('password'))
784
+ entity_data[:data][:url_token_data][:password] = pass
785
+ entity_data[:password_enabled] = true
786
+ end
787
+ entity_data.deep_merge!(custom_data)
788
+ result_create_short_link = aoc_api.create('short_links', entity_data)
717
789
  # public: Creation: permission on node
718
790
  yield(result_create_short_link['resource_id']) if block_given? && link_type.eql?(:public)
719
791
  return Main.result_single_object(result_create_short_link)
720
- when :list
792
+ when :list, :show
721
793
  query = if link_type.eql?(:private)
722
794
  shared_data
723
795
  else
@@ -735,7 +807,31 @@ module Aspera
735
807
  # embed: 'updated_by_user',
736
808
  sort: '-created_at'
737
809
  }
738
- return result_list('short_links', fields: Formatter.all_but('data'), base_query: list_params)
810
+ return result_list('short_links', fields: Formatter.all_but('data'), base_query: list_params) if command.eql?(:list)
811
+ one_id = instance_identifier
812
+ found = api_read_all('short_links', list_params)[:items].find{ |item| item['id'].eql?(one_id)}
813
+ raise Cli::BadIdentifier.new('Short link', one_id) if found.nil?
814
+ return Main.result_single_object(found, fields: Formatter.all_but('data'))
815
+ when :modify
816
+ raise Cli::BadArgument, 'Only public links can be modified' unless link_type.eql?(:public)
817
+ node_file = shared_data.slice(:node_id, :file_id)
818
+ entity_data = {
819
+ data: {
820
+ url_token_data: {
821
+ data: node_file
822
+ }
823
+ },
824
+ json_query: node_file
825
+ }
826
+ one_id = instance_identifier
827
+ custom_data = value_create_modify(command: command, default: {})
828
+ if (pass = custom_data.delete('password'))
829
+ entity_data[:data][:url_token_data][:password] = pass
830
+ entity_data[:password_enabled] = true
831
+ end
832
+ entity_data.deep_merge!(custom_data)
833
+ aoc_api.update("short_links/#{one_id}", entity_data)
834
+ return Main.result_status('modified')
739
835
  when :delete
740
836
  one_id = instance_identifier
741
837
  shared_data.delete(:workspace_id)
@@ -754,7 +850,7 @@ module Aspera
754
850
 
755
851
  # @return persistency object if option `once_only` is used.
756
852
  def package_persistency
757
- return nil unless options.get_option(:once_only, mandatory: true)
853
+ return unless options.get_option(:once_only, mandatory: true)
758
854
  # TODO: add query info to id
759
855
  PersistencyActionOnce.new(
760
856
  manager: persistency,
@@ -762,8 +858,9 @@ module Aspera
762
858
  id: IdGenerator.from_list(
763
859
  ['aoc_recv',
764
860
  options.get_option(:url, mandatory: true),
765
- aoc_api.workspace[:id]
766
- ].concat(aoc_api.additional_persistence_ids)))
861
+ aoc_api.workspace[:id]].concat(aoc_api.additional_persistence_ids)
862
+ )
863
+ )
767
864
  end
768
865
 
769
866
  def reject_packages_from_persistency(all_packages, skip_ids_persistency)
@@ -801,7 +898,9 @@ module Aspera
801
898
  when :tier_restrictions
802
899
  return Main.result_single_object(aoc_api.read('tier_restrictions'))
803
900
  when :user
804
- case options.get_next_command(%i[workspaces profile preferences])
901
+ case options.get_next_command(%i[workspaces profile preferences contacts])
902
+ when :contacts
903
+ return execute_resource_action(:contact)
805
904
  # when :settings
806
905
  # return Main.result_object_list(aoc_api.read('client_settings/'))
807
906
  when :workspaces
@@ -831,7 +930,7 @@ module Aspera
831
930
  end
832
931
  end
833
932
  when :packages
834
- package_command = options.get_next_command(%i[shared_inboxes send receive list show delete].concat(Node::NODE4_READ_ACTIONS), aliases: {recv: :receive})
933
+ package_command = options.get_next_command(%i[shared_inboxes send receive list show delete modify].concat(Node::NODE4_READ_ACTIONS), aliases: {recv: :receive})
835
934
  case package_command
836
935
  when :shared_inboxes
837
936
  case options.get_next_command(%i[list show short_link])
@@ -853,7 +952,7 @@ module Aspera
853
952
  package_data = value_create_modify(command: package_command)
854
953
  new_user_option = options.get_option(:new_user_option)
855
954
  option_validate = options.get_option(:validate_metadata)
856
- # works for both normal user auth and link auth
955
+ # Works for both normal user auth and link auth.
857
956
  workspace_id_hash(hash: package_data, string: true) unless package_data.key?('workspace_id')
858
957
  if !aoc_api.public_link.nil?
859
958
  aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
@@ -862,7 +961,7 @@ module Aspera
862
961
  # enforce workspace id from link (should be already ok, but in case user wanted to override)
863
962
  package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
864
963
  end
865
-
964
+ package_data['encryption_at_rest'] = true if transfer.option_transfer_spec['content_protection'].eql?('encrypt')
866
965
  # transfer may raise an error
867
966
  created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
868
967
  Main.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
@@ -872,50 +971,70 @@ module Aspera
872
971
  ids_to_download = nil
873
972
  if !aoc_api.public_link.nil?
874
973
  aoc_api.assert_public_link_types(['view_received_package'])
875
- # set the package id from link
974
+ # Set the package id from link
876
975
  ids_to_download = aoc_api.public_link['data']['package_id']
877
976
  end
878
- # get from command line unless it was a public link
977
+ # Get from command line unless it was a public link
879
978
  ids_to_download ||= instance_identifier
880
979
  skip_ids_persistency = package_persistency
881
980
  case ids_to_download
882
- when SpecialValues::ALL, SpecialValues::INIT
981
+ when SpecialValues::INIT
982
+ all_packages = list_all_packages_with_query[:items]
983
+ Aspera.assert(skip_ids_persistency){'INIT requires option once_only'}
984
+ skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
985
+ skip_ids_persistency.save
986
+ return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
987
+ when SpecialValues::ALL
883
988
  all_packages = list_all_packages_with_query[:items]
884
- if ids_to_download.eql?(SpecialValues::INIT)
885
- Aspera.assert(skip_ids_persistency){'INIT requires option once_only'}
886
- skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
887
- skip_ids_persistency.save
888
- return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
889
- end
890
989
  # remove from list the ones already downloaded
891
990
  reject_packages_from_persistency(all_packages, skip_ids_persistency)
892
991
  ids_to_download = all_packages.map{ |e| e['id']}
992
+ formatter.display_status("Found #{ids_to_download.length} package(s).")
893
993
  else
894
994
  # single id to array
895
995
  ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
896
996
  end
897
- file_list =
898
- begin
899
- transfer.source_list.map{ |i| {'source'=>i}}
900
- rescue Cli::BadArgument
901
- [{'source' => '.'}]
902
- end
903
- # list here
997
+ # download all files, or specified list only
998
+ ts_paths = transfer.ts_source_paths(default: ['.'])
999
+ per_package_def = options.get_option(:package_folder)
1000
+ unless per_package_def.nil?
1001
+ raise Cli::BadArgument, "Invalid package folder option : #{per_package_def}" unless per_package_def =~ /\A([^+]+)(?:\+([^?]+)(\?)?)?\z/
1002
+ per_package_field1 = Regexp.last_match(1)
1003
+ per_package_field2 = Regexp.last_match(2)
1004
+ per_package_sub_always = Regexp.last_match(3).nil?
1005
+ end
1006
+ # get value outside of loop
1007
+ destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
904
1008
  result_transfer = []
905
- formatter.display_status("Found #{ids_to_download.length} package(s).")
906
1009
  ids_to_download.each do |package_id|
907
1010
  package_info = aoc_api.read("packages/#{package_id}")
908
- formatter.display_status("downloading package: [#{package_info['id']}] #{package_info['name']}")
909
1011
  package_node_api = aoc_api.node_api_from(
910
1012
  node_id: package_info['node_id'],
911
1013
  package_info: package_info,
912
- **workspace_id_hash(name: true))
1014
+ **workspace_id_hash(name: true)
1015
+ )
1016
+ transfer_spec = package_node_api.transfer_spec_gen4(
1017
+ package_info['contents_file_id'],
1018
+ Transfer::Spec::DIRECTION_RECEIVE,
1019
+ {'paths'=> ts_paths}
1020
+ )
1021
+ unless per_package_def.nil?
1022
+ # folder based on first field
1023
+ folder = File.join(
1024
+ destination_folder,
1025
+ Environment.instance.sanitized_filename(package_info[per_package_field1])
1026
+ )
1027
+ transfer.option_transfer_spec['destination_root'] = self.class.unique_folder(
1028
+ folder,
1029
+ extension: per_package_field2.eql?('seq') ? :seq : package_info[per_package_field2],
1030
+ always: per_package_sub_always
1031
+ )
1032
+ end
1033
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{destination_folder}]})
913
1034
  statuses = transfer.start(
914
- package_node_api.transfer_spec_gen4(
915
- package_info['contents_file_id'],
916
- Transfer::Spec::DIRECTION_RECEIVE,
917
- {'paths'=> file_list}),
918
- rest_token: package_node_api)
1035
+ transfer_spec,
1036
+ rest_token: package_node_api
1037
+ )
919
1038
  result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
920
1039
  # update skip list only if all transfer sessions completed
921
1040
  if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
@@ -936,10 +1055,15 @@ module Aspera
936
1055
  display_fields += ['workspace_id'] if aoc_api.workspace[:id].nil?
937
1056
  return Main.result_object_list(result[:items], fields: display_fields, total: result[:total])
938
1057
  when :delete
939
- return do_bulk_operation(command: package_command, descr: 'identifier', values: instance_identifier) do |id|
1058
+ return do_bulk_operation(command: package_command, values: instance_identifier) do |id|
940
1059
  Aspera.assert_values(id.class, [String, Integer]){'identifier'}
941
1060
  aoc_api.delete("packages/#{id}")
942
1061
  end
1062
+ when :modify
1063
+ id = instance_identifier
1064
+ package_data = value_create_modify(command: package_command)
1065
+ aoc_api.update("packages/#{id}", package_data)
1066
+ return Main.result_status('modified')
943
1067
  when *Node::NODE4_READ_ACTIONS
944
1068
  package_id = instance_identifier
945
1069
  package_info = aoc_api.read("packages/#{package_id}")
@@ -954,7 +1078,8 @@ module Aspera
954
1078
  folder_dest = options.get_next_argument('path', validation: String)
955
1079
  home_node_api = aoc_api.node_api_from(
956
1080
  node_id: aoc_api.home[:node_id],
957
- **workspace_id_hash(name: true))
1081
+ **workspace_id_hash(name: true)
1082
+ )
958
1083
  shared_apfid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
959
1084
  return short_link_command(
960
1085
  purpose_public: 'view_shared_file',
@@ -992,12 +1117,16 @@ module Aspera
992
1117
  command_automation = options.get_next_command(%i[workflows instances])
993
1118
  case command_automation
994
1119
  when :instances
995
- return entity_action(aoc_api, 'workflow_instances')
1120
+ return entity_execute(api: aoc_api, entity: 'workflow_instances')
996
1121
  when :workflows
997
1122
  wf_command = options.get_next_command(%i[action launch].concat(Plugin::ALL_OPS))
998
1123
  case wf_command
999
1124
  when *Plugin::ALL_OPS
1000
- return entity_command(wf_command, automation_api, 'workflows')
1125
+ return entity_execute(
1126
+ api: automation_api,
1127
+ entity: 'workflows',
1128
+ command: wf_command
1129
+ )
1001
1130
  when :launch
1002
1131
  wf_id = instance_identifier
1003
1132
  data = automation_api.create("workflows/#{wf_id}/launch", {})