aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
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