aspera-cli 4.14.0 → 4.16.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,33 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/cli/plugins/node'
4
-
4
+ require 'aspera/assert'
5
5
  module Aspera
6
6
  module Cli
7
7
  module Plugins
8
8
  # Plugin for Aspera Shares v1
9
9
  class Shares < Aspera::Cli::BasicAuthPlugin
10
+ API_BASE = 'node_api'
10
11
  class << self
11
- def detect(base_url)
12
- api = Rest.new({base_url: base_url})
13
- # Shares
12
+ def detect(address_or_url)
13
+ address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
14
+ api = Rest.new(base_url: address_or_url, redirect_max: 1)
15
+ found = false
14
16
  begin
15
17
  # shall fail: shares requires auth, but we check error message
16
18
  # TODO: use ping instead ?
17
- api.read('node_api/app')
19
+ api.read("#{API_BASE}/app")
18
20
  rescue RestCallError => e
19
21
  if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
20
- return {version: 'unknown'}
22
+ found = true
21
23
  end
22
24
  end
23
- nil
25
+ return nil unless found
26
+ version = 'unknown'
27
+ test_page = api.call({ operation: 'GET', subpath: 'login' })
28
+ if (m = test_page[:http].body.match(/\(v(1\..*)\)/))
29
+ version = m[1]
30
+ end
31
+ return {
32
+ version: version,
33
+ url: address_or_url
34
+ }
35
+ end
36
+
37
+ def wizard(object:, private_key_path: nil, pub_key_pem: nil)
38
+ options = object.options
39
+ return {
40
+ preset_value: {
41
+ url: options.get_option(:url, mandatory: true),
42
+ username: options.get_option(:username, mandatory: true),
43
+ password: options.get_option(:password, mandatory: true)
44
+ },
45
+ test_args: 'files br /'
46
+ }
24
47
  end
25
48
  end
26
49
 
27
50
  def initialize(env)
28
51
  super(env)
29
- options.declare(:type, 'Type of user/group for operations', values: %i[any local ldap saml], default: :any)
30
- options.parse_options!
31
52
  end
32
53
 
33
54
  SAML_IMPORT_MANDATORY = %w[id name_id].freeze
@@ -44,7 +65,7 @@ module Aspera
44
65
  nagios = Nagios.new
45
66
  begin
46
67
  Rest
47
- .new(base_url: options.get_option(:url, mandatory: true) + '/node_api')
68
+ .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{API_BASE}")
48
69
  .call(
49
70
  operation: 'GET',
50
71
  subpath: 'ping',
@@ -56,69 +77,74 @@ module Aspera
56
77
  end
57
78
  return nagios.result
58
79
  when :repository, :files
59
- api_shares_node = basic_auth_api('node_api')
80
+ api_shares_node = basic_auth_api(API_BASE)
60
81
  repo_command = options.get_next_command(Node::COMMANDS_SHARES)
61
- return Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_shares_node)).execute_action(repo_command)
82
+ return Node.new(@agents, api: api_shares_node).execute_action(repo_command)
62
83
  when :admin
63
84
  api_shares_admin = basic_auth_api('api/v1')
64
- admin_command = options.get_next_command(%i[user group share node].freeze)
85
+ admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
65
86
  case admin_command
66
87
  when :node
67
88
  return entity_action(api_shares_admin, 'data/nodes')
89
+ when :share
90
+ share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
91
+ case share_command
92
+ when *Plugin::ALL_OPS
93
+ return entity_command(share_command, api_shares_admin, 'data/shares')
94
+ # return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
95
+ when :user_permissions, :group_permissions
96
+ share_id = instance_identifier
97
+ return entity_action(api_shares_admin, "data/shares/#{share_id}/#{share_command}")
98
+ end
99
+ when :transfer_settings
100
+ xfer_settings_command = options.get_next_command(%i[show modify])
101
+ return entity_command(xfer_settings_command, api_shares_admin, 'data/transfer_settings', is_singleton: true)
68
102
  when :user, :group
69
103
  entity_type = admin_command
70
- entities_location = options.get_option(:type, mandatory: true)
104
+ entities_location = options.get_next_command(%i[all local ldap saml])
71
105
  entities_path = "data/#{entities_location}_#{entity_type}s"
72
106
  entity_action = nil
73
107
  case entities_location
74
- when :any
108
+ when :all
75
109
  entities_path = "data/#{entity_type}s"
76
110
  entity_action = %i[list show delete]
77
111
  entity_action.concat(USR_GRP_SETTINGS)
78
112
  entity_action.push(:users) if entity_type.eql?(:group)
79
113
  entity_action.freeze
80
114
  when :local
81
- entity_action = %i[list show create modify delete].freeze
115
+ entity_action = %i[list show delete create modify].freeze
82
116
  when :ldap
83
117
  entity_action = %i[add].freeze
84
118
  when :saml
85
119
  entity_action = %i[import].freeze
86
120
  end
87
121
  entity_verb = options.get_next_command(entity_action)
88
- # entity_path = "#{entities_path}/#{instance_identifier}" if %i[app_authorizations share_permissions].include?(entity_verb)
89
122
  case entity_verb
90
- when *Plugin::ALL_OPS
123
+ when *Plugin::ALL_OPS # list, show, delete, create, modify
91
124
  display_fields = entity_type.eql?(:user) ? %w[id username first_name last_name email] : nil
92
- display_fields.push(:directory_user) if entity_type.eql?(:user) && entities_location.eql?(:any)
125
+ display_fields.push(:directory_user) if entity_type.eql?(:user) && entities_location.eql?(:all)
93
126
  return entity_command(entity_verb, api_shares_admin, entities_path, display_fields: display_fields)
94
- when :import
95
- return do_bulk_operation(value_create_modify(type: :bulk_hash), 'created') do |entity_parameters|
127
+ when *USR_GRP_SETTINGS # transfer_settings, app_authorizations, share_permissions
128
+ group_id = instance_identifier
129
+ entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
130
+ return entity_action(api_shares_admin, entities_path, is_singleton: !entity_verb.eql?(:share_permissions))
131
+ when :import # saml
132
+ return do_bulk_operation(command: entity_verb, descr: 'user information') do |entity_parameters|
96
133
  entity_parameters = entity_parameters.transform_keys{|k|k.gsub(/\s+/, '_').downcase}
97
- raise 'expecting Hash' unless entity_parameters.is_a?(Hash)
134
+ assert_type(entity_parameters, Hash)
98
135
  SAML_IMPORT_MANDATORY.each{|p|raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
99
136
  entity_parameters.each_key do |p|
100
137
  raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
101
138
  end
102
139
  api_shares_admin.create("#{entities_path}/import", entity_parameters)[:data]
103
140
  end
104
- when :add
105
- return do_bulk_operation(value_create_modify(type: :bulk_hash), 'created') do |entity_name|
106
- raise "expecting string (name), have #{entity_name.class}" unless entity_name.is_a?(String)
141
+ when :add # ldap
142
+ return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
107
143
  api_shares_admin.create(entities_path, {entity_type=>entity_name})[:data]
108
144
  end
109
- when *USR_GRP_SETTINGS
110
- group_id = instance_identifier
111
- entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
112
- return entity_action(api_shares_admin, entities_path, is_singleton: !entity_verb.eql?(:share_permissions))
113
- end
114
- when :share
115
- share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Plugin::ALL_OPS))
116
- case share_command
117
- when *Plugin::ALL_OPS
118
- return entity_command(share_command, api_shares_admin, 'data/shares')
119
- # return {type: :object_list, data: all_shares, fields: %w[id name status status_message]}
120
- when :user_permissions, :group_permissions
121
- return entity_action(api_shares_admin, "data/shares/#{instance_identifier}/#{share_command}")
145
+ when :users # group
146
+ raise "TODO, not implemented"
147
+ else error_unexpected_value(entity_verb)
122
148
  end
123
149
  end
124
150
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/fasp/sync'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ # Module for sync actions
9
+ module SyncActions
10
+ SIMPLE_ARGUMENTS_SYNC = {
11
+ direction: Aspera::Fasp::Sync::DIRECTIONS,
12
+ local_dir: String,
13
+ remote_dir: String
14
+ }.stringify_keys.freeze
15
+
16
+ class << self
17
+ def declare_options(options)
18
+ options.declare(:sync_info, 'Information for sync instance and sessions', types: Hash)
19
+ end
20
+ end
21
+
22
+ def execute_sync_action(&block)
23
+ assert(block){'No block given'}
24
+ command = options.get_next_command(%i[start admin])
25
+ # try to get 3 arguments as simple arguments
26
+ case command
27
+ when :start
28
+ simple_session_args = {}
29
+ SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
30
+ value = options.get_next_argument(
31
+ arg,
32
+ type: check.is_a?(Class) ? check : nil,
33
+ expected: check.is_a?(Class) ? :single : check,
34
+ mandatory: false)
35
+ break if value.nil?
36
+ simple_session_args[arg] = value.to_s
37
+ end
38
+ async_params = nil
39
+ if simple_session_args.empty?
40
+ async_params = options.get_option(:sync_info, mandatory: true)
41
+ else
42
+ raise Cli::BadArgument,
43
+ "Provide zero or 3 arguments: #{SIMPLE_ARGUMENTS_SYNC.keys.join(',')}" unless simple_session_args.keys.sort == SIMPLE_ARGUMENTS_SYNC.keys.sort
44
+ async_params = options.get_option(
45
+ :sync_info,
46
+ mandatory: false,
47
+ default: {'sessions' => [{'name' => File.basename(simple_session_args['local_dir'])}]})
48
+ assert_type(async_params, Hash){'sync_info'}
49
+ assert_type(async_params['sessions'], Array){'sync_info[sessions]'}
50
+ assert_type(async_params['sessions'].first, Hash){'sync_info[sessions][0]'}
51
+ async_params['sessions'].first.merge!(simple_session_args)
52
+ end
53
+ Log.log.debug{Log.dump('async_params', async_params)}
54
+ Aspera::Fasp::Sync.start(async_params, &block)
55
+ return Main.result_success
56
+ when :admin
57
+ command2 = options.get_next_command([:status])
58
+ case command2
59
+ when :status
60
+ sync_session_name = options.get_next_argument('name of sync session', mandatory: false, type: String)
61
+ async_params = options.get_option(:sync_info, mandatory: true)
62
+ return {type: :single_object, data: Aspera::Fasp::Sync.admin_status(async_params, sync_session_name)}
63
+ end # command2
64
+ end # command
65
+ end # execute_action
66
+ end # SyncActions
67
+ end # Cli
68
+ end # Aspera
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/fasp/agent_base'
3
4
  require 'aspera/fasp/transfer_spec'
4
- require 'aspera/cli/listener/logger'
5
- require 'aspera/cli/listener/progress_multi'
6
5
  require 'aspera/cli/info'
6
+ require 'aspera/log'
7
+ require 'aspera/assert'
7
8
 
8
9
  module Aspera
9
10
  module Cli
@@ -16,7 +17,7 @@ module Aspera
16
17
  # special value for --sources : read file list from transfer spec (--ts)
17
18
  FILE_LIST_FROM_TRANSFER_SPEC = '@ts'
18
19
  FILE_LIST_OPTIONS = [FILE_LIST_FROM_ARGS, FILE_LIST_FROM_TRANSFER_SPEC, 'Array'].freeze
19
- DEFAULT_TRANSFER_NOTIF_TMPL = <<~END_OF_TEMPLATE
20
+ DEFAULT_TRANSFER_NOTIFY_TEMPLATE = <<~END_OF_TEMPLATE
20
21
  From: <%=from_name%> <<%=from_email%>>
21
22
  To: <<%=to%>>
22
23
  Subject: <%=subject%>
@@ -29,8 +30,8 @@ module Aspera
29
30
  private_constant :FILE_LIST_FROM_ARGS,
30
31
  :FILE_LIST_FROM_TRANSFER_SPEC,
31
32
  :FILE_LIST_OPTIONS,
32
- :DEFAULT_TRANSFER_NOTIF_TMPL
33
- TRANSFER_AGENTS = %i[direct node connect httpgw trsdk].freeze
33
+ :DEFAULT_TRANSFER_NOTIFY_TEMPLATE
34
+ TRANSFER_AGENTS = Fasp::AgentBase.agent_list.freeze
34
35
 
35
36
  class << self
36
37
  # @return :success if all sessions statuses returned by "start" are success
@@ -43,15 +44,15 @@ module Aspera
43
44
  end
44
45
 
45
46
  # @param env external objects: option manager, config file manager
46
- def initialize(opt_mgr, config)
47
+ def initialize(opt_mgr, config_plugin)
47
48
  @opt_mgr = opt_mgr
48
- @config = config
49
+ @config = config_plugin
49
50
  # command line can override transfer spec
50
- @transfer_spec_cmdline = {'create_dir' => true}
51
+ @transfer_spec_command_line = {'create_dir' => true}
52
+ # options for transfer agent
51
53
  @transfer_info = {}
52
54
  # the currently selected transfer agent
53
55
  @agent = nil
54
- @progress_listener = Listener::ProgressMulti.new
55
56
  # source/destination pair, like "paths" of transfer spec
56
57
  @transfer_paths = nil
57
58
  @opt_mgr.declare(:ts, 'Override transfer spec values', types: Hash, handler: {o: self, m: :option_transfer_spec})
@@ -59,73 +60,68 @@ module Aspera
59
60
  @opt_mgr.declare(:sources, "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})")
60
61
  @opt_mgr.declare(:src_type, 'Type of file list', values: %i[list pair], default: :list)
61
62
  @opt_mgr.declare(:transfer, 'Type of transfer agent', values: TRANSFER_AGENTS, default: :direct)
62
- @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :option_transfer_info})
63
- @opt_mgr.declare(:progress, 'Type of progress bar', values: %i[none native multi], default: :native)
63
+ @opt_mgr.declare(:transfer_info, 'Parameters for transfer agent', types: Hash, handler: {o: self, m: :transfer_info})
64
64
  @opt_mgr.parse_options!
65
65
  end
66
66
 
67
- def option_transfer_spec; @transfer_spec_cmdline; end
67
+ def option_transfer_spec; @transfer_spec_command_line; end
68
68
 
69
69
  # multiple option are merged
70
70
  def option_transfer_spec=(value)
71
- raise 'option ts shall be a Hash' unless value.is_a?(Hash)
72
- @transfer_spec_cmdline.deep_merge!(value)
71
+ assert_type(value, Hash){'ts'}
72
+ @transfer_spec_command_line.deep_merge!(value)
73
73
  end
74
74
 
75
75
  # add other transfer spec parameters
76
- def option_transfer_spec_deep_merge(ts); @transfer_spec_cmdline.deep_merge!(ts); end
76
+ def option_transfer_spec_deep_merge(ts); @transfer_spec_command_line.deep_merge!(ts); end
77
77
 
78
78
  # @return [Hash] transfer spec with updated values from command line, including removed values
79
79
  def updated_ts(transfer_spec={})
80
- transfer_spec.deep_merge!(@transfer_spec_cmdline)
80
+ transfer_spec.deep_merge!(@transfer_spec_command_line)
81
81
  # recursively remove values that are nil (user wants to delete)
82
82
  transfer_spec.deep_do { |hash, key, value, _unused| hash.delete(key) if value.nil?}
83
83
  return transfer_spec
84
84
  end
85
85
 
86
- def option_transfer_info; @transfer_info; end
86
+ attr_reader :transfer_info
87
87
 
88
88
  # multiple option are merged
89
- def option_transfer_info=(value)
90
- raise 'option transfer_info shall be a Hash' unless value.is_a?(Hash)
89
+ def transfer_info=(value)
91
90
  @transfer_info.deep_merge!(value)
92
91
  end
93
92
 
94
93
  def agent_instance=(instance)
95
94
  @agent = instance
96
- @agent.add_listener(Listener::Logger.new)
97
- # use local progress bar if asked so, or if native and non local ascp (because only local ascp has native progress bar)
98
- if @opt_mgr.get_option(:progress, mandatory: true).eql?(:multi) ||
99
- (@opt_mgr.get_option(:progress, mandatory: true).eql?(:native) && !instance.class.to_s.eql?('Aspera::Fasp::AgentDirect'))
100
- @agent.add_listener(@progress_listener)
101
- end
102
95
  end
103
96
 
104
97
  # analyze options and create new agent if not already created or set
105
- def set_agent_by_options
106
- return nil unless @agent.nil?
98
+ def agent_instance
99
+ return @agent unless @agent.nil?
107
100
  agent_type = @opt_mgr.get_option(:transfer, mandatory: true)
108
101
  # agent plugin is loaded on demand to avoid loading unnecessary dependencies
109
102
  require "aspera/fasp/agent_#{agent_type}"
110
- agent_options = @opt_mgr.get_option(:transfer_info)
111
- raise CliBadArgument, "the transfer agent configuration shall be Hash, not #{agent_options.class} (#{agent_options}), "\
112
- 'e.g. use @json:<json>' unless agent_options.is_a?(Hash)
113
- # special case: use default node
114
- if agent_type.eql?(:node) && agent_options.empty?
115
- param_set_name = @config.get_plugin_default_config_name(:node)
116
- raise CliBadArgument, "No default node configured. Please specify #{Manager.option_name_to_line(:transfer_info)}" if param_set_name.nil?
117
- agent_options = @config.preset_by_name(param_set_name)
118
- end
119
- # special case: native progress bar
120
- if agent_type.eql?(:direct) && @opt_mgr.get_option(:progress, mandatory: true).eql?(:native)
121
- agent_options[:quiet] = false
103
+ # set keys as symbols
104
+ agent_options = @opt_mgr.get_option(:transfer_info).symbolize_keys
105
+ # special cases
106
+ case agent_type
107
+ when :node
108
+ if agent_options.empty?
109
+ param_set_name = @config.get_plugin_default_config_name(:node)
110
+ raise Cli::BadArgument, "No default node configured. Please specify #{Manager.option_name_to_line(:transfer_info)}" if param_set_name.nil?
111
+ agent_options = @config.preset_by_name(param_set_name).symbolize_keys
112
+ end
113
+ when :direct
114
+ # by default do not display ascp native progress bar
115
+ agent_options[:quiet] = true unless agent_options.key?(:quiet)
116
+ agent_options[:check_ignore] = ->(host, port){@config.ignore_cert?(host, port)}
117
+ agent_options[:trusted_certs] = @config.trusted_cert_locations(files_only: true) unless agent_options.key?(:trusted_certs)
122
118
  end
123
- # normalize after getting from user or default node
124
- agent_options = agent_options.symbolize_keys
119
+ agent_options[:progress] = @config.progress_bar
125
120
  # get agent instance
126
121
  new_agent = Kernel.const_get("Aspera::Fasp::Agent#{agent_type.capitalize}").new(agent_options)
127
122
  self.agent_instance = new_agent
128
- return nil
123
+ Log.log.debug{"transfer agent is a #{@agent.class}"}
124
+ return @agent
129
125
  end
130
126
 
131
127
  # return destination folder for transfers
@@ -135,13 +131,13 @@ module Aspera
135
131
  dest_folder = @opt_mgr.get_option(:to_folder)
136
132
  # do not expand path, if user wants to expand path: user @path:
137
133
  return dest_folder unless dest_folder.nil?
138
- dest_folder = @transfer_spec_cmdline['destination_root']
134
+ dest_folder = @transfer_spec_command_line['destination_root']
139
135
  return dest_folder unless dest_folder.nil?
140
136
  # default: / on remote, . on local
141
137
  case direction.to_s
142
138
  when Fasp::TransferSpec::DIRECTION_SEND then dest_folder = '/'
143
139
  when Fasp::TransferSpec::DIRECTION_RECEIVE then dest_folder = '.'
144
- else raise "wrong direction: #{direction}"
140
+ else error_unexpected_value(direction)
145
141
  end
146
142
  return dest_folder
147
143
  end
@@ -161,7 +157,7 @@ module Aspera
161
157
  # return cache if set
162
158
  return @transfer_paths unless @transfer_paths.nil?
163
159
  # start with lower priority : get paths from transfer spec on command line
164
- @transfer_paths = @transfer_spec_cmdline['paths'] if @transfer_spec_cmdline.key?('paths')
160
+ @transfer_paths = @transfer_spec_command_line['paths'] if @transfer_spec_command_line.key?('paths')
165
161
  # is there a source list option ?
166
162
  file_list = @opt_mgr.get_option(:sources)
167
163
  case file_list
@@ -169,34 +165,35 @@ module Aspera
169
165
  Log.log.debug('getting file list as parameters')
170
166
  # get remaining arguments
171
167
  file_list = @opt_mgr.get_next_argument('source file list', expected: :multiple)
172
- raise CliBadArgument, 'specify at least one file on command line or use '\
168
+ raise Cli::BadArgument, 'specify at least one file on command line or use ' \
173
169
  "--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !file_list.is_a?(Array) || file_list.empty?
174
170
  when FILE_LIST_FROM_TRANSFER_SPEC
175
171
  Log.log.debug('assume list provided in transfer spec')
176
172
  special_case_direct_with_list =
177
173
  @opt_mgr.get_option(:transfer, mandatory: true).eql?(:direct) &&
178
- Fasp::Parameters.ts_has_ascp_file_list(@transfer_spec_cmdline, @opt_mgr.get_option(:transfer_info))
179
- raise CliBadArgument, 'transfer spec on command line must have sources' if @transfer_paths.nil? && !special_case_direct_with_list
174
+ Fasp::Parameters.ts_has_ascp_file_list(@transfer_spec_command_line, @opt_mgr.get_option(:transfer_info))
175
+ raise Cli::BadArgument, 'transfer spec on command line must have sources' if @transfer_paths.nil? && !special_case_direct_with_list
180
176
  # here we assume check of sources is made in transfer agent
181
177
  return @transfer_paths
182
178
  when Array
183
179
  Log.log.debug('getting file list as extended value')
184
- raise CliBadArgument, 'sources must be a Array of String' if !file_list.reject{|f|f.is_a?(String)}.empty?
180
+ raise Cli::BadArgument, 'sources must be a Array of String' if !file_list.reject{|f|f.is_a?(String)}.empty?
185
181
  else
186
- raise CliBadArgument, "sources must be a Array, not #{file_list.class}"
182
+ raise Cli::BadArgument, "sources must be a Array, not #{file_list.class}"
187
183
  end
188
184
  # here, file_list is an Array or String
189
185
  if !@transfer_paths.nil?
190
186
  Log.log.warn('--sources overrides paths from --ts')
191
187
  end
192
- case @opt_mgr.get_option(:src_type, mandatory: true)
188
+ source_type=@opt_mgr.get_option(:src_type, mandatory: true)
189
+ case source_type
193
190
  when :list
194
191
  # when providing a list, just specify source
195
192
  @transfer_paths = file_list.map{|i|{'source' => i}}
196
193
  when :pair
197
- raise CliBadArgument, "When using pair, provide an even number of paths: #{file_list.length}" unless file_list.length.even?
194
+ assert(file_list.length.even?, exception_class: Cli::BadArgument){"When using pair, provide an even number of paths: #{file_list.length}"}
198
195
  @transfer_paths = file_list.each_slice(2).to_a.map{|s, d|{'source' => s, 'destination' => d}}
199
- else raise 'Unsupported src_type'
196
+ else error_unexpected_value(source_type)
200
197
  end
201
198
  Log.log.debug{"paths=#{@transfer_paths}"}
202
199
  return @transfer_paths
@@ -207,44 +204,44 @@ module Aspera
207
204
  # @param rest_token [Rest] if oauth token regeneration supported
208
205
  def start(transfer_spec, rest_token: nil)
209
206
  # check parameters
210
- raise 'transfer_spec must be hash' unless transfer_spec.is_a?(Hash)
207
+ assert_type(transfer_spec, Hash){'transfer_spec'}
211
208
  # process :src option
212
209
  case transfer_spec['direction']
213
210
  when Fasp::TransferSpec::DIRECTION_RECEIVE
214
211
  # init default if required in any case
215
- @transfer_spec_cmdline['destination_root'] ||= destination_folder(transfer_spec['direction'])
212
+ @transfer_spec_command_line['destination_root'] ||= destination_folder(transfer_spec['direction'])
216
213
  when Fasp::TransferSpec::DIRECTION_SEND
217
214
  if transfer_spec.dig('tags', Fasp::TransferSpec::TAG_RESERVED, 'node', 'access_key')
218
215
  # gen4
219
- @transfer_spec_cmdline.delete('destination_root') if @transfer_spec_cmdline.key?('destination_root_id')
216
+ @transfer_spec_command_line.delete('destination_root') if @transfer_spec_command_line.key?('destination_root_id')
220
217
  elsif transfer_spec.key?('token')
221
218
  # gen3
222
219
  # in that case, destination is set in return by application (API/upload_setup)
223
220
  # but to_folder was used in initial API call
224
- @transfer_spec_cmdline.delete('destination_root')
221
+ @transfer_spec_command_line.delete('destination_root')
225
222
  else
226
223
  # init default if required
227
- @transfer_spec_cmdline['destination_root'] ||= destination_folder(transfer_spec['direction'])
224
+ @transfer_spec_command_line['destination_root'] ||= destination_folder(transfer_spec['direction'])
228
225
  end
229
226
  end
230
227
  # update command line paths, unless destination already has one
231
- @transfer_spec_cmdline['paths'] = transfer_spec['paths'] || ts_source_paths
228
+ @transfer_spec_command_line['paths'] = transfer_spec['paths'] || ts_source_paths
232
229
  # updated transfer spec with command line
233
230
  updated_ts(transfer_spec)
231
+ # if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
232
+ if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
233
+ transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', true)
234
+ end
234
235
  # create transfer agent
235
- set_agent_by_options
236
- Log.log.debug{"transfer agent is a #{@agent.class}"}
237
- @agent.start_transfer(transfer_spec, token_regenerator: rest_token)
236
+ agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
238
237
  # list of: :success or "error message string"
239
- result = @agent.wait_for_transfers_completion
240
- @progress_listener.reset
241
- Fasp::AgentBase.validate_status_list(result)
238
+ result = agent_instance.wait_for_completion
242
239
  send_email_transfer_notification(transfer_spec, result)
243
240
  return result
244
241
  end
245
242
 
246
243
  def send_email_transfer_notification(transfer_spec, statuses)
247
- return if @opt_mgr.get_option(:notif_to).nil?
244
+ return if @opt_mgr.get_option(:notify_to).nil?
248
245
  global_status = self.class.session_status(statuses)
249
246
  email_vars = {
250
247
  global_transfer_status: global_status,
@@ -252,7 +249,7 @@ module Aspera
252
249
  body: "Transfer is: #{global_status}",
253
250
  ts: transfer_spec
254
251
  }
255
- @config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIF_TMPL, values: email_vars)
252
+ @config.send_email_template(email_template_default: DEFAULT_TRANSFER_NOTIFY_TEMPLATE, values: email_vars)
256
253
  end
257
254
 
258
255
  # shut down if agent requires it
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/log'
4
+ require 'aspera/assert'
5
+ require 'ruby-progressbar'
6
+
7
+ module Aspera
8
+ module Cli
9
+ # progress bar for transfers, supports multi-session
10
+ class TransferProgress
11
+ def initialize
12
+ reset
13
+ end
14
+
15
+ def reset
16
+ @progress_bar = nil
17
+ # key is session id
18
+ @sessions = {}
19
+ @completed = false
20
+ @title = ''
21
+ end
22
+
23
+ def total(key)
24
+ @sessions.values.inject(0){|m, s|m + s[key]}
25
+ end
26
+
27
+ def event(session_id:, type:, info: nil)
28
+ Log.log.debug{"progress: #{type} #{session_id} #{info}"}
29
+ assert(!session_id.nil? || type.eql?(:pre_start)){'session_id is nil'}
30
+ return if @completed
31
+ if @progress_bar.nil?
32
+ @progress_bar = ProgressBar.create(
33
+ format: '%t %a %B %p%% %r Mbps %E',
34
+ rate_scale: lambda{|rate|rate / Environment::BYTES_PER_MEBIBIT},
35
+ title: '',
36
+ total: nil)
37
+ end
38
+ need_increment = true
39
+ case type
40
+ when :pre_start
41
+ @title = info
42
+ when :session_start
43
+ raise "Session #{session_id} already started" if @sessions[session_id]
44
+ @sessions[session_id] = {
45
+ job_size: 0, # total size of transfer (pre-calc)
46
+ current: 0
47
+ }
48
+ @title = ''
49
+ when :session_size
50
+ @sessions[session_id][:job_size] = info.to_i
51
+ current_total = total(:job_size)
52
+ @progress_bar.total = current_total unless current_total.eql?(@progress_bar.total) || current_total < @progress_bar.progress
53
+ when :transfer
54
+ if !@progress_bar.total.nil?
55
+ need_increment = false
56
+ @sessions[session_id][:current] = info.to_i
57
+ current_total = total(:current)
58
+ @progress_bar.progress = current_total unless @progress_bar.progress.eql?(current_total)
59
+ end
60
+ when :end
61
+ @title = ''
62
+ @completed = true
63
+ @progress_bar.finish
64
+ else
65
+ raise "Unknown event type #{type}"
66
+ end
67
+ new_title = @sessions.length < 2 ? @title : "[#{@sessions.length}] #{@title}"
68
+ @progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
69
+ @progress_bar.increment if need_increment && !@completed
70
+ end
71
+ end
72
+ end
73
+ end
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # for beta add extension : .beta1
6
6
  # for dev version add extension : .pre
7
- VERSION = '4.14.0'
7
+ VERSION = '4.16.0'
8
8
  end
9
9
  end
data/lib/aspera/colors.rb CHANGED
@@ -14,10 +14,12 @@ class String
14
14
  # it adds control chars to set color (and reset at the end).
15
15
  VT_STYLES = {
16
16
  bold: 1,
17
+ dim: 2,
17
18
  italic: 3,
18
19
  underline: 4,
19
20
  blink: 5,
20
21
  reverse_color: 7,
22
+ invisible: 8,
21
23
  black: 30,
22
24
  red: 31,
23
25
  green: 32,
@@ -38,7 +40,7 @@ class String
38
40
  private_constant :VT_STYLES
39
41
  # defines methods to String, one per entry in VT_STYLES
40
42
  VT_STYLES.each do |name, code|
41
- if $stderr.tty?
43
+ if $stdout.tty?
42
44
  begin_seq = vt_cmd(code)
43
45
  end_code = 0 # by default reset all
44
46
  if code <= 7 then code + 20