aspera-cli 4.25.1 → 4.25.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +456 -405
  4. data/CONTRIBUTING.md +22 -18
  5. data/README.md +33 -9741
  6. data/bin/asession +111 -88
  7. data/lib/aspera/agent/connect.rb +1 -1
  8. data/lib/aspera/agent/desktop.rb +1 -1
  9. data/lib/aspera/agent/direct.rb +19 -18
  10. data/lib/aspera/agent/node.rb +1 -1
  11. data/lib/aspera/api/aoc.rb +44 -20
  12. data/lib/aspera/api/faspex.rb +25 -6
  13. data/lib/aspera/api/node.rb +20 -16
  14. data/lib/aspera/ascp/installation.rb +32 -51
  15. data/lib/aspera/assert.rb +2 -2
  16. data/lib/aspera/cli/extended_value.rb +1 -0
  17. data/lib/aspera/cli/formatter.rb +0 -4
  18. data/lib/aspera/cli/hints.rb +18 -4
  19. data/lib/aspera/cli/main.rb +3 -6
  20. data/lib/aspera/cli/manager.rb +46 -30
  21. data/lib/aspera/cli/plugins/aoc.rb +155 -131
  22. data/lib/aspera/cli/plugins/base.rb +15 -18
  23. data/lib/aspera/cli/plugins/config.rb +50 -87
  24. data/lib/aspera/cli/plugins/factory.rb +2 -2
  25. data/lib/aspera/cli/plugins/faspex.rb +4 -4
  26. data/lib/aspera/cli/plugins/faspex5.rb +74 -76
  27. data/lib/aspera/cli/plugins/node.rb +3 -5
  28. data/lib/aspera/cli/plugins/oauth.rb +26 -25
  29. data/lib/aspera/cli/plugins/preview.rb +9 -14
  30. data/lib/aspera/cli/plugins/shares.rb +15 -7
  31. data/lib/aspera/cli/transfer_agent.rb +2 -2
  32. data/lib/aspera/cli/version.rb +1 -1
  33. data/lib/aspera/colors.rb +7 -0
  34. data/lib/aspera/environment.rb +30 -16
  35. data/lib/aspera/faspex_gw.rb +6 -6
  36. data/lib/aspera/faspex_postproc.rb +20 -14
  37. data/lib/aspera/hash_ext.rb +8 -0
  38. data/lib/aspera/log.rb +15 -15
  39. data/lib/aspera/markdown.rb +22 -0
  40. data/lib/aspera/node_simulator.rb +1 -1
  41. data/lib/aspera/oauth/base.rb +2 -2
  42. data/lib/aspera/oauth/url_json.rb +2 -2
  43. data/lib/aspera/oauth/web.rb +1 -1
  44. data/lib/aspera/preview/generator.rb +9 -9
  45. data/lib/aspera/rest.rb +44 -37
  46. data/lib/aspera/rest_call_error.rb +16 -8
  47. data/lib/aspera/rest_error_analyzer.rb +38 -36
  48. data/lib/aspera/rest_errors_aspera.rb +19 -18
  49. data/lib/aspera/transfer/resumer.rb +2 -2
  50. data/lib/aspera/yaml.rb +49 -0
  51. data.tar.gz.sig +0 -0
  52. metadata +17 -3
  53. metadata.gz.sig +0 -0
  54. data/release_notes.md +0 -8
@@ -102,7 +102,7 @@ module Aspera
102
102
  # @param destination_folder [String] Base folder
103
103
  # @param fld. [Array] List of fields of package
104
104
  def unique_folder(package_info, destination_folder, fld: nil, seq: false, opt: false)
105
- Aspera.assert_array_all(fld, String, type: Cli::BadArgument){'fld'}
105
+ Aspera.assert_array_all(fld, String, type: BadArgument){'fld'}
106
106
  Aspera.assert([1, 2].include?(fld.length)){'fld must have 1 or 2 elements'}
107
107
  folder = Environment.instance.sanitized_filename(package_info[fld[0]])
108
108
  if seq
@@ -181,7 +181,7 @@ module Aspera
181
181
  end
182
182
  myself = aoc_api.read('self')
183
183
  if auto_set_pub_key
184
- Aspera.assert(myself['public_key'].empty?, type: Cli::Error){'Public key is already set in profile (use --override=yes)'} unless option_override
184
+ Aspera.assert(myself['public_key'].empty?, type: Error){'Public key is already set in profile (use --override=yes)'} unless option_override
185
185
  formatter.display_status('Updating profile with the public key.')
186
186
  aoc_api.update("users/#{myself['id']}", {'public_key' => pub_key_pem})
187
187
  end
@@ -224,21 +224,18 @@ module Aspera
224
224
  @scope = new_scope
225
225
  end
226
226
 
227
- # create an API object with the same options, but with a different subpath
227
+ # Create an API object with the options from CLI, but with a different subpath
228
228
  # @param aoc_base_path [String] New subpath
229
229
  # @return [Api::AoC] API object for AoC (is Rest)
230
230
  def api_from_options(aoc_base_path)
231
- return new_with_options(
232
- Api::AoC,
233
- kwargs: {
234
- scope: @scope,
235
- subpath: aoc_base_path,
236
- secret_finder: config
237
- },
238
- option: {
239
- workspace: nil
240
- }
241
- )
231
+ return Api::AoC.new(**Oauth.args_from_options(
232
+ options,
233
+ defaults: {workspace: nil},
234
+ scope: @scope,
235
+ subpath: aoc_base_path,
236
+ secret_finder: config,
237
+ progress_disp: formatter
238
+ ))
242
239
  end
243
240
 
244
241
  # AoC Rest object
@@ -256,11 +253,11 @@ module Aspera
256
253
  end
257
254
 
258
255
  # Generate or update Hash with workspace id and name (option), if not already set
259
- # @param hash [Hash,nil] Optional base hash (modified)
260
- # @param string [Boolean] true to set key as string, else as symbol
261
- # @param name [Boolean] include name
256
+ # @param hash [Hash,nil] Optional base `Hash` (modified)
257
+ # @param string [Boolean] `true` to set key as `String`, else as `Symbol`
258
+ # @param name [Boolean] Include name
262
259
  # @return [Hash] with key `workspace_[id,name]` (symbol or string) only if defined
263
- def workspace_id_hash(hash: nil, string: false, name: false)
260
+ def workspace_id_hash(hash = nil, string: false, name: false)
264
261
  info = aoc_api.workspace
265
262
  hash = {} if hash.nil?
266
263
  fields = %i[id]
@@ -273,12 +270,12 @@ module Aspera
273
270
  return hash
274
271
  end
275
272
 
276
- # Get resource identifier from command line, either directly or from name.
273
+ # Get resource identifier from command line, either directly specifying the `id` or from `name` (percent selector).
277
274
  # @param resource_class_path url path for resource
278
275
  # @return identifier
279
276
  def get_resource_id_from_args(resource_class_path)
280
277
  return instance_identifier do |field, value|
281
- Aspera.assert(field.eql?('name'), type: Cli::BadArgument){'only selection by name is supported'}
278
+ Aspera.assert(field.eql?('name'), type: BadArgument){'only selection by name is supported'}
282
279
  aoc_api.lookup_by_name(resource_class_path, value)['id']
283
280
  end
284
281
  end
@@ -300,7 +297,7 @@ module Aspera
300
297
  query = query_read_delete(default: default_query)
301
298
  # caller may add specific modifications or checks to query
302
299
  yield(query) if block_given?
303
- result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact, formatter: formatter)
300
+ result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact)
304
301
  return Main.result_object_list(result[:items], fields: fields, total: result[:total])
305
302
  end
306
303
 
@@ -312,7 +309,7 @@ module Aspera
312
309
  # TODO : craft a query that looks for dropbox only in current workspace
313
310
  query['dropbox_id'] = aoc_api.lookup_by_name('dropboxes', query.delete('dropbox_name'))['id']
314
311
  end
315
- workspace_id_hash(hash: query, string: true)
312
+ workspace_id_hash(query, string: true)
316
313
  # by default show dropbox packages only for dropboxes
317
314
  query['exclude_dropbox_packages'] = !query.key?('dropbox_id') unless query.key?('exclude_dropbox_packages')
318
315
  end
@@ -325,7 +322,7 @@ module Aspera
325
322
  Aspera.assert_type(query, Hash){'query'}
326
323
  PACKAGE_RECEIVED_BASE_QUERY.each{ |k, v| query[k] = v unless query.key?(k)}
327
324
  resolve_dropbox_name_default_ws_id(query)
328
- return aoc_api.read_with_paging('packages', query.compact, formatter: formatter)
325
+ return aoc_api.read_with_paging('packages', query.compact)
329
326
  end
330
327
 
331
328
  NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
@@ -470,11 +467,11 @@ module Aspera
470
467
  return Main.result_success
471
468
  when :do
472
469
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
473
- return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::SCOPE_ADMIN)
470
+ return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::Scope::ADMIN)
474
471
  when :bearer_token
475
472
  node_api = aoc_api.node_api_from(
476
473
  node_id: res_id,
477
- scope: options.get_next_argument('scope')
474
+ scope: options.get_next_argument('scope', default: Api::Node::Scope::ADMIN)
478
475
  )
479
476
  return Main.result_text(node_api.oauth.authorization)
480
477
  when :dropbox
@@ -504,7 +501,7 @@ module Aspera
504
501
  node_id: shared_folder['node_id'],
505
502
  workspace_id: res_id,
506
503
  workspace_name: nil,
507
- scope: Api::Node::SCOPE_USER
504
+ scope: Api::Node::Scope::USER
508
505
  )
509
506
  result = node_api.read(
510
507
  'permissions',
@@ -741,110 +738,132 @@ module Aspera
741
738
  end
742
739
  end
743
740
 
744
- # Create a shared link for the given entity
745
- # @param purpose_public [Symbol]
746
- # @param shared_data [Hash] information for shared data
747
- # @param block [Proc] Optional: called on creation
748
- def short_link_command(purpose_public:, **shared_data)
749
- link_type = options.get_next_argument('link type', accept_list: %i[public private])
750
- purpose_local = case link_type
751
- when :public
752
- case purpose_public
753
- when /package/ then 'send_package_to_dropbox'
754
- when /shared/ then 'token_auth_redirection'
755
- else Aspera.error_unexpected_value(purpose_public){'public link purpose'}
756
- end
757
- when :private then 'shared_folder_auth_link'
758
- else Aspera.error_unreachable_line
741
+ # Create a short link for the given entity: Shared folder or Shared Inbox
742
+ # Short link entity: `short_links` have:
743
+ # - a numerical id, e.g. `764412`
744
+ # - a resource type, e.g. `UrlToken`
745
+ # - a ressource id, e.g. `scQ7uXPbvQ`
746
+ # - a short URL path, e.g. `dxyRpT9`
747
+ # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
748
+ # @param &perm_block [Proc] Optional: create/modify/delete permissions on node
749
+ def short_link_command(**shared_data, &perm_block)
750
+ link_type = options.get_next_argument('link access (public or private)', accept_list: %i[public private])
751
+ if shared_data.keys.sort == %i[dropbox_id name]
752
+ # Packages app, Shared inbox
753
+ token_purpose = 'send_package_to_dropbox'
754
+ short_link_purpose = link_type.eql?(:public) ? 'send_package_to_dropbox' : 'shared_folder_auth_link'
755
+ elsif shared_data.keys.sort == %i[file_id node_id]
756
+ # Files app, Shared folder
757
+ token_purpose = 'view_shared_file'
758
+ short_link_purpose = link_type.eql?(:public) ? 'token_auth_redirection' : 'shared_folder_auth_link'
759
+ else
760
+ Aspera.error_unexpected_value(shared_data.keys)
759
761
  end
760
- command = options.get_next_command(%i[create delete list show modify])
762
+ command = options.get_next_command(%i[create delete list show] + (link_type.eql?(:public) ? %i[modify] : []))
761
763
  case command
762
764
  when :create
763
- entity_data = {
764
- purpose: purpose_local,
765
+ # Add workspace id
766
+ workspace_id_hash(shared_data)
767
+ create_payload = {
768
+ purpose: short_link_purpose,
765
769
  user_selected_name: nil
766
770
  }
767
771
  case link_type
768
772
  when :private
769
- entity_data[:data] = shared_data
773
+ create_payload[:data] = shared_data
770
774
  when :public
771
- entity_data[:expires_at] = nil
772
- entity_data[:password_enabled] = false
775
+ create_payload[:expires_at] = nil
776
+ create_payload[:password_enabled] = false
773
777
  shared_data[:name] = ''
774
- entity_data[:data] = {
778
+ create_payload[:data] = {
775
779
  aoc: true,
776
780
  url_token_data: {
777
781
  data: shared_data,
778
- purpose: purpose_public
782
+ purpose: token_purpose
779
783
  }
780
784
  }
781
785
  end
782
786
  custom_data = value_create_modify(command: command, default: {})
787
+ access_levels = custom_data.delete('access_levels')
783
788
  if (pass = custom_data.delete('password'))
784
- entity_data[:data][:url_token_data][:password] = pass
785
- entity_data[:password_enabled] = true
789
+ create_payload[:data][:url_token_data][:password] = pass
790
+ create_payload[:password_enabled] = true
786
791
  end
787
- entity_data.deep_merge!(custom_data)
788
- result_create_short_link = aoc_api.create('short_links', entity_data)
789
- # public: Creation: permission on node
790
- yield(result_create_short_link['resource_id']) if block_given? && link_type.eql?(:public)
792
+ create_payload.deep_merge!(custom_data)
793
+ result_create_short_link = aoc_api.create('short_links', create_payload)
794
+ # Creation: perm_block: permission on node
795
+ yield(:create, result_create_short_link['resource_id'], access_levels) if block_given? && link_type.eql?(:public)
791
796
  return Main.result_single_object(result_create_short_link)
792
- when :list, :show
797
+ when :delete, :list, :show, :modify
798
+ workspace_id_hash(shared_data)
793
799
  query = if link_type.eql?(:private)
794
800
  shared_data
795
801
  else
796
802
  {
797
803
  url_token_data: {
798
804
  data: shared_data,
799
- purpose: purpose_public
805
+ purpose: token_purpose
800
806
  }
801
807
  }
802
808
  end
803
809
  list_params = {
804
810
  json_query: query.to_json,
805
- purpose: purpose_local,
811
+ purpose: short_link_purpose,
806
812
  edit_access: true,
807
813
  # embed: 'updated_by_user',
808
814
  sort: '-created_at'
809
815
  }
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 = aoc_api.read_with_paging('short_links', list_params, formatter: formatter)[: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
816
+ short_list = aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact)
817
+ case command
818
+ when :delete
819
+ one_id = instance_identifier(description: 'short link id')
820
+ if link_type.eql?(:public)
821
+ found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
822
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
823
+ yield(:delete, found['resource_id'], nil)
824
+ end
825
+ aoc_api.delete("short_links/#{one_id}", {
826
+ edit_access: true,
827
+ json_query: shared_data.to_json
828
+ })
829
+ return Main.result_status('deleted')
830
+ when :list
831
+ return Main.result_object_list(short_list[:items], fields: Formatter.all_but('data'), total: short_list[:total])
832
+ when :show
833
+ one_id = instance_identifier(description: 'short link id')
834
+ found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
835
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
836
+ return Main.result_single_object(found, fields: Formatter.all_but('data'))
837
+ when :modify
838
+ one_id = instance_identifier(description: 'short link id')
839
+ node_file = shared_data.slice(:node_id, :file_id)
840
+ modify_payload = {
841
+ edit_access: true,
842
+ json_query: node_file
843
+ }
844
+ custom_data = value_create_modify(command: command)
845
+ if (pass = custom_data.delete('password'))
846
+ modify_payload[:password_enabled] = true
847
+ modify_payload[:data] = {
848
+ url_token_data: {
849
+ password: pass,
850
+ data: node_file
851
+ }
822
852
  }
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')
835
- when :delete
836
- one_id = instance_identifier
837
- shared_data.delete(:workspace_id)
838
- delete_params = {
839
- edit_access: true,
840
- json_query: shared_data.to_json
841
- }
842
- aoc_api.delete("short_links/#{one_id}", delete_params)
843
- if link_type.eql?(:public)
844
- # TODO: get permission id..
845
- # shared_apfid[:api].delete('permissions', {ids: })
853
+ else
854
+ modify_payload[:password_enabled] = false
855
+ end
856
+ if custom_data.delete('access_levels')
857
+ # Modification: perm_block: permission on node
858
+ found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
859
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
860
+ yield(:update, found['resource_id'], access_levels)
861
+ end
862
+ modify_payload.deep_merge!(custom_data)
863
+ aoc_api.update("short_links/#{one_id}", modify_payload)
864
+ return Main.result_status('modified')
846
865
  end
847
- return Main.result_status('deleted')
866
+ else Aspera.error_unexpected_value(command)
848
867
  end
849
868
  end
850
869
 
@@ -935,24 +954,20 @@ module Aspera
935
954
  case options.get_next_command(%i[list show short_link])
936
955
  when :list
937
956
  default_query = {'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}
938
- workspace_id_hash(hash: default_query, string: true)
957
+ workspace_id_hash(default_query, string: true)
939
958
  return result_list('dropbox_memberships', fields: %w[dropbox_id dropbox.name], default_query: default_query)
940
959
  when :show
941
960
  return Main.result_single_object(aoc_api.read(get_resource_path_from_args('dropboxes')))
942
961
  when :short_link
943
- return short_link_command(
944
- purpose_public: 'send_package_to_dropbox',
945
- dropbox_id: get_resource_id_from_args('dropboxes'),
946
- name: '',
947
- **workspace_id_hash
948
- )
962
+ # TODO: check name
963
+ return short_link_command(dropbox_id: get_resource_id_from_args('dropboxes'), name: '')
949
964
  end
950
965
  when :send
951
966
  package_data = value_create_modify(command: package_command)
952
967
  new_user_option = options.get_option(:new_user_option)
953
968
  option_validate = options.get_option(:validate_metadata)
954
969
  # Works for both normal user auth and link auth.
955
- workspace_id_hash(hash: package_data, string: true) unless package_data.key?('workspace_id')
970
+ workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
956
971
  if !aoc_api.public_link.nil?
957
972
  aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
958
973
  box_type = aoc_api.public_link['purpose'].split('_').last
@@ -1041,7 +1056,7 @@ module Aspera
1041
1056
  return Main.result_object_list(result[:items], fields: display_fields, total: result[:total])
1042
1057
  when :delete
1043
1058
  return do_bulk_operation(command: package_command, values: instance_identifier) do |id|
1044
- Aspera.assert_values(id.class, [String, Integer]){'identifier'}
1059
+ Aspera.assert_type(id, String, Integer){'identifier'}
1045
1060
  aoc_api.delete("packages/#{id}")
1046
1061
  end
1047
1062
  when :modify
@@ -1052,13 +1067,13 @@ module Aspera
1052
1067
  when *Node::NODE4_READ_ACTIONS
1053
1068
  package_id = instance_identifier
1054
1069
  package_info = aoc_api.read("packages/#{package_id}")
1055
- return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::SCOPE_USER)
1070
+ return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1056
1071
  end
1057
1072
  when :files
1058
1073
  command_repo = options.get_next_command([:short_link].concat(NODE4_EXT_COMMANDS))
1059
1074
  case command_repo
1060
1075
  when *NODE4_EXT_COMMANDS
1061
- return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::SCOPE_USER)
1076
+ return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1062
1077
  when :short_link
1063
1078
  folder_dest = options.get_next_argument('path', validation: String)
1064
1079
  home_node_api = aoc_api.node_api_from(
@@ -1067,33 +1082,42 @@ module Aspera
1067
1082
  )
1068
1083
  shared_apfid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1069
1084
  return short_link_command(
1070
- purpose_public: 'view_shared_file',
1071
1085
  node_id: shared_apfid[:api].app_info[:node_info]['id'],
1072
- file_id: shared_apfid[:file_id],
1073
- **workspace_id_hash
1074
- ) do |resource_id|
1075
- # TODO: merge with node permissions ?
1076
- # TODO: access level as arg
1077
- access_levels = Api::Node::ACCESS_LEVELS # ['delete','list','mkdir','preview','read','rename','write']
1078
- perm_data = {
1079
- 'file_id' => shared_apfid[:file_id],
1080
- 'access_id' => resource_id,
1081
- 'access_type' => 'user',
1082
- 'access_levels' => access_levels,
1083
- 'tags' => {
1084
- # TODO: really just here ? not in tags.aspera.files.workspace ?
1085
- 'url_token' => true,
1086
- 'folder_name' => File.basename(folder_dest),
1087
- 'created_by_name' => aoc_api.current_user_info['name'],
1088
- 'created_by_email' => aoc_api.current_user_info['email'],
1089
- 'access_key' => shared_apfid[:api].app_info[:node_info]['access_key'],
1090
- 'node' => shared_apfid[:api].app_info[:node_info]['host'],
1091
- **workspace_id_hash(string: true, name: true)
1092
- }
1093
- }
1094
- created_data = shared_apfid[:api].create('permissions', perm_data)
1095
- aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apfid[:api].app_info)
1096
- end
1086
+ file_id: shared_apfid[:file_id]
1087
+ ) do |op, id, access_levels|
1088
+ case op
1089
+ when :create
1090
+ # `id` is the resource id
1091
+ perm_data = {
1092
+ 'file_id' => shared_apfid[:file_id],
1093
+ 'access_id' => id,
1094
+ 'access_type' => 'user',
1095
+ 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1096
+ 'tags' => {
1097
+ 'url_token' => true,
1098
+ 'folder_name' => File.basename(folder_dest),
1099
+ 'created_by_name' => aoc_api.current_user_info['name'],
1100
+ 'created_by_email' => aoc_api.current_user_info['email'],
1101
+ 'access_key' => shared_apfid[:api].app_info[:node_info]['access_key'],
1102
+ 'node' => shared_apfid[:api].app_info[:node_info]['name'],
1103
+ **workspace_id_hash(string: true, name: true)
1104
+ }
1105
+ }
1106
+ created_data = shared_apfid[:api].create('permissions', perm_data)
1107
+ aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apfid[:api].app_info)
1108
+ when :update
1109
+ # `id` is the permission_id
1110
+ found = shared_apfid[:api].read('permissions', {file_id: shared_apfid[:file_id], inherited: false, access_type: 'user', access_id: id}).find{ |i| i['access_id'].eql?(id)}
1111
+ raise Error, 'Short link not found: #{id}' if found.nil?
1112
+ shared_apfid[:api].update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1113
+ when :delete
1114
+ # `id` is the resource id, i.e. `access_id`
1115
+ found = shared_apfid[:api].read('permissions', {file_id: shared_apfid[:file_id], inherited: false, access_type: 'user', access_id: id}).first
1116
+ raise Error, 'Short link not found: #{id}' if found.nil?
1117
+ shared_apfid[:api].delete("permissions/#{found['id']}")
1118
+ else Aspera.error_unexpected_value(op)
1119
+ end
1120
+ end
1097
1121
  end
1098
1122
  when :automation
1099
1123
  change_api_scope(Api::AoC::Scope::ADMIN_USER)
@@ -22,7 +22,6 @@ module Aspera
22
22
  class << self
23
23
  def declare_options(options)
24
24
  options.declare(:query, 'Additional filter for for some commands (list/delete)', allowed: [Hash, Array, NilClass])
25
- options.declare(:property, 'Name of property to set (modify operation)')
26
25
  options.declare(:bulk, 'Bulk operation (only some)', allowed: Allowed::TYPES_BOOLEAN, default: false)
27
26
  options.declare(:bfail, 'Bulk operation error handling', allowed: Allowed::TYPES_BOOLEAN, default: true)
28
27
  end
@@ -40,7 +39,7 @@ module Aspera
40
39
  def initialize(context:)
41
40
  # Check presence in descendant of mandatory method and constant
42
41
  Aspera.assert(respond_to?(:execute_action), type: InternalError){"Missing method 'execute_action' in #{self.class}"}
43
- Aspera.assert(self.class.constants.include?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
42
+ Aspera.assert(self.class.const_defined?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
44
43
  @context = context
45
44
  add_manual_header if @context.man_header
46
45
  end
@@ -70,25 +69,25 @@ module Aspera
70
69
  # Resource identifier as positional parameter
71
70
  #
72
71
  # @param description [String] description of the identifier
73
- # @param block [Proc] block to search for identifier based on attribute value
72
+ # @param &block [Proc] block to search for identifier based on attribute value
74
73
  # @return [String, Array] identifier or list of ids
75
- def instance_identifier(description: 'identifier', &block)
74
+ def instance_identifier(description: 'identifier')
76
75
  res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
77
76
  # Can be an Array
78
77
  if res_id.is_a?(String) && (m = Base.percent_selector(res_id))
79
- Aspera.assert(block, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
78
+ Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
80
79
  res_id = yield(m[:field], m[:value])
81
80
  end
82
81
  return res_id
83
82
  end
84
83
 
85
84
  # For create and delete operations: execute one action or multiple if bulk is yes
86
- # @param command [Symbol] operation: :create, :delete, ...
87
- # @param descr [String] description of the value
88
- # @param values [Object] the value(s), or the type of value to get from user
89
- # @param id_result [String] key in result hash to use as identifier
90
- # @param fields [Array] fields to display
91
- # @param &block [Proc] block to execute for each value
85
+ # @param command [Symbol] Operation: :create, :delete, ...
86
+ # @param descr [String] Description of the value
87
+ # @param values [Object] Value(s), or type of value to get from user
88
+ # @param id_result [String] Key in result hash to use as identifier
89
+ # @param fields [Array] Fields to display
90
+ # @param &block [Proc] Block to execute for each value
92
91
  def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default)
93
92
  Aspera.assert(block_given?){'missing block'}
94
93
  is_bulk = options.get_option(:bulk)
@@ -178,7 +177,7 @@ module Aspera
178
177
  api.delete(
179
178
  entity,
180
179
  nil,
181
- content_type: Rest::MIME_JSON,
180
+ content_type: Mime::JSON,
182
181
  body: {delete_style => one_res_id}
183
182
  )
184
183
  return Main.result_status('deleted')
@@ -209,20 +208,18 @@ module Aspera
209
208
  return Main.result_object_list(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
210
209
  return Main.result_value_list(data)
211
210
  else
212
- raise "An error occurred: unexpected result type for list: #{data.class}"
211
+ Aspera.error_unexpected_value(data.class.name){'list type'}
213
212
  end
214
213
  when :modify
215
214
  parameters = value_create_modify(command: command)
216
- property = options.get_option(:property)
217
- parameters = {property => parameters} unless property.nil?
218
215
  api.update(one_res_path, parameters)
219
216
  return Main.result_status('modified')
220
217
  else
221
- raise "unknown action: #{command}"
218
+ Aspera.error_unexpected_value(command){'command'}
222
219
  end
223
220
  end
224
221
 
225
- # Query parameters in URL suitable for REST: list/GET and delete/DELETE
222
+ # Query parameters in URL suitable for REST: list/`GET` and delete/`DELETE`
226
223
  def query_read_delete(default: nil)
227
224
  # Dup default, as it could be frozen
228
225
  query = options.get_option(:query) || default.dup
@@ -267,7 +264,7 @@ module Aspera
267
264
  # @param entity [String,Symbol] API endpoint of entity to list
268
265
  # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
269
266
  # @param query [Hash,nil] Additional query parameters
270
- # @return [Array] items, total_count
267
+ # @return [Array<(Array<Hash>, Integer)>] items, total_count
271
268
  def list_entities_limit_offset_total_count(
272
269
  api:,
273
270
  entity:,