aspera-cli 4.24.0 → 4.24.2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +19 -1
  4. data/README.md +1264 -941
  5. data/bin/ascli +20 -1
  6. data/bin/asession +23 -27
  7. data/lib/aspera/agent/base.rb +10 -21
  8. data/lib/aspera/agent/connect.rb +2 -3
  9. data/lib/aspera/agent/desktop.rb +2 -2
  10. data/lib/aspera/agent/direct.rb +49 -32
  11. data/lib/aspera/agent/factory.rb +31 -0
  12. data/lib/aspera/api/aoc.rb +79 -49
  13. data/lib/aspera/api/faspex.rb +212 -0
  14. data/lib/aspera/api/node.rb +99 -84
  15. data/lib/aspera/ascp/installation.rb +22 -21
  16. data/lib/aspera/ascp/management.rb +119 -23
  17. data/lib/aspera/assert.rb +14 -8
  18. data/lib/aspera/cli/extended_value.rb +15 -15
  19. data/lib/aspera/cli/formatter.rb +7 -5
  20. data/lib/aspera/cli/hints.rb +8 -0
  21. data/lib/aspera/cli/info.rb +4 -4
  22. data/lib/aspera/cli/main.rb +56 -71
  23. data/lib/aspera/cli/manager.rb +7 -4
  24. data/lib/aspera/cli/plugins/alee.rb +2 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +110 -186
  26. data/lib/aspera/cli/plugins/ats.rb +4 -4
  27. data/lib/aspera/cli/plugins/base.rb +335 -0
  28. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  29. data/lib/aspera/cli/plugins/config.rb +263 -221
  30. data/lib/aspera/cli/plugins/console.rb +15 -15
  31. data/lib/aspera/cli/plugins/cos.rb +2 -2
  32. data/lib/aspera/cli/plugins/factory.rb +78 -0
  33. data/lib/aspera/cli/plugins/faspex.rb +17 -20
  34. data/lib/aspera/cli/plugins/faspex5.rb +79 -193
  35. data/lib/aspera/cli/plugins/faspio.rb +14 -13
  36. data/lib/aspera/cli/plugins/httpgw.rb +13 -12
  37. data/lib/aspera/cli/plugins/node.rb +34 -32
  38. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  39. data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
  40. data/lib/aspera/cli/plugins/preview.rb +4 -4
  41. data/lib/aspera/cli/plugins/server.rb +15 -13
  42. data/lib/aspera/cli/plugins/shares.rb +18 -15
  43. data/lib/aspera/cli/sync_actions.rb +1 -1
  44. data/lib/aspera/cli/transfer_agent.rb +24 -20
  45. data/lib/aspera/cli/transfer_progress.rb +6 -6
  46. data/lib/aspera/cli/version.rb +3 -3
  47. data/lib/aspera/cli/wizard.rb +74 -65
  48. data/lib/aspera/colors.rb +6 -0
  49. data/lib/aspera/command_line_builder.rb +45 -50
  50. data/lib/aspera/command_line_converter.rb +2 -1
  51. data/lib/aspera/coverage.rb +1 -1
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +13 -9
  54. data/lib/aspera/faspex_gw.rb +6 -4
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/keychain/macos_security.rb +1 -1
  57. data/lib/aspera/log.rb +88 -37
  58. data/lib/aspera/nagios.rb +1 -1
  59. data/lib/aspera/oauth/base.rb +17 -10
  60. data/lib/aspera/oauth/factory.rb +8 -8
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/products/connect.rb +4 -3
  63. data/lib/aspera/products/desktop.rb +1 -4
  64. data/lib/aspera/products/other.rb +9 -1
  65. data/lib/aspera/products/transferd.rb +0 -1
  66. data/lib/aspera/rest.rb +126 -83
  67. data/lib/aspera/ssh.rb +3 -3
  68. data/lib/aspera/sync/args.schema.yaml +46 -3
  69. data/lib/aspera/sync/conf.schema.yaml +130 -94
  70. data/lib/aspera/sync/operations.rb +71 -74
  71. data/lib/aspera/temp_file_manager.rb +17 -5
  72. data/lib/aspera/transfer/error.rb +16 -7
  73. data/lib/aspera/transfer/parameters.rb +34 -20
  74. data/lib/aspera/transfer/resumer.rb +74 -0
  75. data/lib/aspera/transfer/spec.rb +4 -3
  76. data/lib/aspera/transfer/spec.schema.yaml +132 -51
  77. data/lib/aspera/transfer/spec_doc.rb +41 -35
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +6 -6
  80. data.tar.gz.sig +0 -0
  81. metadata +9 -7
  82. metadata.gz.sig +2 -2
  83. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  84. data/lib/aspera/cli/plugin.rb +0 -333
  85. data/lib/aspera/cli/plugin_factory.rb +0 -81
  86. data/lib/aspera/resumer.rb +0 -77
  87. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -18,26 +18,26 @@ module Aspera
18
18
  module Sync
19
19
  # builds command line arg for async and execute it
20
20
  module Operations
21
- # sync direction
21
+ # Sync direction
22
22
  DIRECTIONS = %i[push pull bidi].freeze
23
- # default direction for sync
24
- DEFAULT_DIRECTION = :push
23
+ # Default direction for sync
24
+ DEFAULT_DIRECTION = DIRECTIONS.first
25
25
 
26
26
  class << self
27
27
  # Set `remote_dir` in sync parameters based on transfer spec
28
- # @param params [Hash] sync parameters, old or new format
29
- # @param remote_dir_key [String] key to update in above hash
30
- # @param transfer_spec [Hash] transfer spec
31
- def update_remote_dir(sync_params, remote_dir_key, transfer_spec)
28
+ # @param params [Hash] Sync parameters, in `conf` or `args` format.
29
+ # @param remote_dir_key [String] Key to update in above hash
30
+ # @param transfer_spec [Hash] Transfer spec
31
+ def update_remote_dir(params, remote_dir_key, transfer_spec)
32
32
  if transfer_spec.dig(*%w[tags aspera node file_id])
33
33
  # in AoC, use gen4
34
- sync_params[remote_dir_key] = '/'
34
+ params[remote_dir_key] = '/'
35
35
  elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
36
36
  # TODO : something more generic, independent of Shares
37
37
  # in Shares, the actual folder on remote end is not always the same as the name of the share
38
38
  remote_key = transfer_spec['direction'].eql?('send') ? 'destination' : 'source'
39
39
  actual_remote = transfer_spec['paths']&.first&.[](remote_key)
40
- sync_params[remote_dir_key] = actual_remote if actual_remote
40
+ params[remote_dir_key] = actual_remote if actual_remote
41
41
  end
42
42
  nil
43
43
  end
@@ -70,34 +70,36 @@ module Aspera
70
70
  end
71
71
 
72
72
  # Get symbol of sync direction, defaulting to :push
73
- # @param params [Hash] sync parameters, old or new format
73
+ # @param params [Hash] Sync parameters, old or new format
74
74
  # @return [Symbol] direction symbol, one of :push, :pull, :bidi
75
75
  def direction_sym(params)
76
76
  (params['direction'] || DEFAULT_DIRECTION).to_sym
77
77
  end
78
78
 
79
79
  # Start the sync process
80
- # @param sync_params [Hash] sync parameters, old or new format
80
+ # @param params [Hash] Sync parameters, old or new format
81
+ # @param opt_ts [Hash] Optional transfer spec
81
82
  # @param &block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
82
- def start(sync_params, opt_ts = nil)
83
- Log.dump(:sync_params_initial, sync_params)
84
- Aspera.assert_type(sync_params, Hash)
83
+ def start(params, opt_ts = nil)
84
+ Log.dump(:sync_params_initial, params)
85
+ Aspera.assert_type(params, Hash)
86
+ Aspera.assert(PARAM_KEYS.any?{ |k| params.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
85
87
  env_args = {
86
88
  args: [],
87
89
  env: {}
88
90
  }
89
- if sync_params.key?('local')
91
+ if params.key?('local')
90
92
  # "conf" format
91
- Aspera.assert_type(sync_params['local'], Hash){'local'}
92
- remote = sync_params['remote']
93
+ Aspera.assert_type(params['local'], Hash){'local'}
94
+ remote = params['remote']
93
95
  Aspera.assert_type(remote, Hash){'remote'}
94
96
  Aspera.assert_type(remote['path'], String){'remote path'}
95
97
  # get transfer spec if possible, and feed back to new structure
96
98
  if block_given?
97
- transfer_spec = yield(direction_sym(sync_params), sync_params['local']['path'], remote['path'])
99
+ transfer_spec = yield(direction_sym(params), params['local']['path'], remote['path'])
98
100
  Log.dump(:auth_ts, transfer_spec)
99
101
  transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
100
- tspec_to_sync_info(transfer_spec, sync_params, CONF_SCHEMA)
102
+ tspec_to_sync_info(transfer_spec, params, CONF_SCHEMA)
101
103
  update_remote_dir(remote, 'path', transfer_spec)
102
104
  end
103
105
  remote['connect_mode'] ||= transfer_spec['wss_enabled'] ? 'ws' : 'ssh'
@@ -107,44 +109,42 @@ module Aspera
107
109
  remote['private_key_paths'].concat(add_certificates)
108
110
  end
109
111
  # '--exclusive-mgmt-port=12345', '--arg-err-path=-',
110
- env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
111
- Log.dump(:sync_conf, sync_params)
112
+ env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(params))}"]
113
+ Log.dump(:sync_conf, params)
112
114
  agent = Agent::Direct.new
113
115
  agent.start_and_monitor_process(session: {}, name: :async, **env_args)
114
- elsif sync_params.key?('sessions')
116
+ else
115
117
  # "args" format
116
118
  raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
117
- sync_params.keys.push('instance').uniq.sort.eql?(CMDLINE_PARAMS_KEYS)
118
- Aspera.assert_type(sync_params['sessions'], Array)
119
- Aspera.assert_type(sync_params['sessions'].first, Hash)
119
+ params.keys.push('instance').uniq.sort.eql?(CMDLINE_PARAMS_KEYS)
120
+ Aspera.assert_type(params['sessions'], Array)
121
+ Aspera.assert_type(params['sessions'].first, Hash)
120
122
  if block_given?
121
- sync_params['sessions'].each do |session|
123
+ params['sessions'].each do |session|
122
124
  Aspera.assert_type(session['local_dir'], String){'local_dir'}
123
125
  Aspera.assert_type(session['remote_dir'], String){'remote_dir'}
124
126
  transfer_spec = yield(direction_sym(session), session['local_dir'], session['remote_dir'])
125
127
  Log.dump(:auth_ts, transfer_spec)
126
128
  transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
127
- tspec_to_sync_info(transfer_spec, session, SESSION_SCHEMA)
129
+ tspec_to_sync_info(transfer_spec, session, ARGS_SESSION_SCHEMA)
128
130
  session['private_key_paths'] = Ascp::Installation.instance.aspera_token_ssh_key_paths(:rsa) if transfer_spec.key?('token')
129
131
  update_remote_dir(session, 'remote_dir', transfer_spec)
130
132
  end
131
133
  end
132
- if sync_params.key?('instance')
133
- Aspera.assert_type(sync_params['instance'], Hash)
134
- instance_builder = CommandLineBuilder.new(sync_params['instance'], INSTANCE_SCHEMA, CommandLineConverter)
134
+ if params.key?('instance')
135
+ Aspera.assert_type(params['instance'], Hash)
136
+ instance_builder = CommandLineBuilder.new(params['instance'], ARGS_INSTANCE_SCHEMA, CommandLineConverter)
135
137
  instance_builder.process_params
136
138
  instance_builder.add_env_args(env_args)
137
139
  end
138
- sync_params['sessions'].each do |session_params|
140
+ params['sessions'].each do |session_params|
139
141
  Aspera.assert_type(session_params, Hash)
140
142
  Aspera.assert(session_params.key?('name')){'session must contain at least: name'}
141
- session_builder = CommandLineBuilder.new(session_params, SESSION_SCHEMA, CommandLineConverter)
143
+ session_builder = CommandLineBuilder.new(session_params, ARGS_SESSION_SCHEMA, CommandLineConverter)
142
144
  session_builder.process_params
143
145
  session_builder.add_env_args(env_args)
144
146
  end
145
147
  Environment.secure_execute(exec: Ascp::Installation.instance.path(:async), **env_args)
146
- else
147
- raise Error, 'At least one of `local` or `sessions` must be present in async parameters'
148
148
  end
149
149
  return
150
150
  end
@@ -168,23 +168,24 @@ module Aspera
168
168
  end
169
169
 
170
170
  # Run `asyncadmin` to get status of sync session
171
- # @param sync_params [Hash] sync parameters in conf or args format
171
+ # @param params [Hash] sync parameters in conf or args format
172
172
  # @return [Hash] parsed output of asyncadmin
173
- def admin_status(sync_params)
173
+ def admin_status(params)
174
+ Aspera.assert(PARAM_KEYS.any?{ |k| params.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
174
175
  arguments = ['--quiet']
175
- if sync_params.key?('local')
176
+ if params.key?('local')
176
177
  # "conf" format
177
- arguments.push("--name=#{sync_params['name']}")
178
- if sync_params.key?('local_db_dir')
179
- arguments.push("--local-db-dir=#{sync_params['local_db_dir']}")
180
- elsif sync_params.dig('local', 'path')
181
- arguments.push("--local-dir=#{sync_params.dig('local', 'path')}")
178
+ arguments.push("--name=#{params['name']}")
179
+ if params.key?('local_db_dir')
180
+ arguments.push("--local-db-dir=#{params['local_db_dir']}")
181
+ elsif params.dig('local', 'path')
182
+ arguments.push("--local-dir=#{params.dig('local', 'path')}")
182
183
  else
183
184
  raise Error, 'Missing either local_db_dir or local.path'
184
185
  end
185
- elsif sync_params.key?('sessions')
186
+ else
186
187
  # "args" format
187
- session = sync_params['sessions'].first
188
+ session = params['sessions'].first
188
189
  arguments.push("--name=#{session['name']}")
189
190
  if session.key?('local_db_dir')
190
191
  arguments.push("--local-db-dir=#{session['local_db_dir']}")
@@ -193,30 +194,28 @@ module Aspera
193
194
  else
194
195
  raise Error, 'Missing either local_db_dir or local_dir'
195
196
  end
196
- else
197
- raise Error, 'At least one of `local` or `sessions` must be present in async parameters'
198
197
  end
199
198
  stdout = Environment.secure_capture(exec: ASYNC_ADMIN_EXECUTABLE, args: arguments)
200
199
  return parse_status(stdout)
201
200
  end
202
201
 
203
- # Find the local database folder based on sync_params
204
- # @param sync_params [Hash] sync parameters in conf or args format
205
- # @param exception [Bool] Raise exception in case of problem, else return nil
202
+ # Find the local database folder based on params
203
+ # @param params [Hash] sync parameters in conf or args format
206
204
  # @return [String, nil] path to "local DB dir", i.e. folder that contains folders that contain snap.db
207
- def local_db_folder(sync_params, exception: true)
208
- if sync_params.key?('local')
205
+ def local_db_folder(params)
206
+ Aspera.assert(PARAM_KEYS.any?{ |k| params.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
207
+ if params.key?('local')
209
208
  # "conf" format
210
- if sync_params.key?('local_db_dir')
211
- return sync_params['local_db_dir']
212
- elsif (local_path = sync_params.dig('local', 'path'))
209
+ if params.key?('local_db_dir')
210
+ return params['local_db_dir']
211
+ elsif (local_path = params.dig('local', 'path'))
213
212
  return local_path
214
213
  elsif exception
215
214
  raise Error, 'Missing either local_db_dir or local.path'
216
215
  end
217
- elsif sync_params.key?('sessions')
216
+ else
218
217
  # "args" format
219
- session = sync_params['sessions'].first
218
+ session = params['sessions'].first
220
219
  if session.key?('local_db_dir')
221
220
  return session['local_db_dir']
222
221
  elsif session.key?('local_dir')
@@ -224,26 +223,23 @@ module Aspera
224
223
  elsif exception
225
224
  raise Error, 'Missing either local_db_dir or local_dir'
226
225
  end
227
- elsif exception
228
- raise Error, 'At least one of `local` or `sessions` must be present in async parameters'
229
226
  end
230
227
  nil
231
228
  end
232
229
 
233
- def session_name(sync_params)
234
- if sync_params.key?('local')
230
+ def session_name(params)
231
+ Aspera.assert(PARAM_KEYS.any?{ |k| params.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
232
+ if params.key?('local')
235
233
  # "conf" format
236
- return sync_params['name']
237
- elsif sync_params.key?('sessions')
238
- # "args" format
239
- return sync_params['sessions'].first['name']
234
+ return params['name']
240
235
  else
241
- raise Error, 'At least one of `local` or `sessions` must be present in async parameters'
236
+ # "args" format
237
+ return params['sessions'].first['name']
242
238
  end
243
239
  end
244
240
 
245
- def session_db_file(sync_params)
246
- db_file = File.join(local_db_folder(sync_params), PRIVATE_FOLDER, session_name(sync_params), ASYNC_DB)
241
+ def session_db_file(params)
242
+ db_file = File.join(local_db_folder(params), PRIVATE_FOLDER, session_name(params), ASYNC_DB)
247
243
  Aspera.assert(File.exist?(db_file)){"Database file #{db_file} does not exist"}
248
244
  db_file
249
245
  end
@@ -281,19 +277,20 @@ module Aspera
281
277
  end
282
278
  # Private stuff:
283
279
  # Read JSON schema and mapping to command line options
284
- INSTANCE_SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'args')
285
- SESSION_SCHEMA = INSTANCE_SCHEMA['properties']['sessions']['items']
286
- INSTANCE_SCHEMA['properties'].delete('sessions')
280
+ ARGS_INSTANCE_SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'args')
281
+ ARGS_SESSION_SCHEMA = ARGS_INSTANCE_SCHEMA['properties']['sessions']['items']
282
+ ARGS_INSTANCE_SCHEMA['properties'].delete('sessions')
287
283
  CONF_SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'conf')
288
- CommandLineBuilder.adjust_properties_defaults(INSTANCE_SCHEMA['properties'])
289
- CommandLineBuilder.adjust_properties_defaults(SESSION_SCHEMA['properties'])
290
- CommandLineBuilder.adjust_properties_defaults(CONF_SCHEMA['properties'])
284
+ CommandLineBuilder.validate_schema(ARGS_INSTANCE_SCHEMA)
285
+ CommandLineBuilder.validate_schema(ARGS_SESSION_SCHEMA)
286
+ CommandLineBuilder.validate_schema(CONF_SCHEMA)
291
287
  CMDLINE_PARAMS_KEYS = %w[instance sessions].freeze
292
288
  ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
293
289
  PRIVATE_FOLDER = '.private-asp'
294
290
  ASYNC_DB = 'snap.db'
291
+ PARAM_KEYS = %w[local sessions].freeze
295
292
 
296
- private_constant :INSTANCE_SCHEMA, :SESSION_SCHEMA, :CONF_SCHEMA, :CMDLINE_PARAMS_KEYS, :ASYNC_ADMIN_EXECUTABLE, :PRIVATE_FOLDER, :ASYNC_DB
293
+ private_constant :ARGS_INSTANCE_SCHEMA, :ARGS_SESSION_SCHEMA, :CMDLINE_PARAMS_KEYS, :ASYNC_ADMIN_EXECUTABLE, :PRIVATE_FOLDER, :ASYNC_DB, :PARAM_KEYS
297
294
  end
298
295
  end
299
296
  end
@@ -17,17 +17,29 @@ module Aspera
17
17
  private_constant :SEC_IN_DAY, :FILE_LIST_AGE_MAX_SEC
18
18
 
19
19
  attr_accessor :cleanup_on_exit
20
+ attr_reader :global_temp
20
21
 
21
22
  def initialize
22
23
  @created_files = []
23
24
  @cleanup_on_exit = true
25
+ @global_temp = Etc.systmpdir
26
+ end
27
+
28
+ def global_temp=(value)
29
+ @global_temp = case value
30
+ when '@env' then Dir.tmpdir
31
+ when '@sys' then Etc.systmpdir
32
+ else value
33
+ end
24
34
  end
25
35
 
26
36
  def delete_file(filepath)
27
37
  File.delete(filepath) if @cleanup_on_exit
38
+ rescue => e
39
+ Log.log.warn{"Problem deleting file: #{filepath}: #{e.message}"}
28
40
  end
29
41
 
30
- # call this on process exit
42
+ # Call this on process exit
31
43
  def cleanup
32
44
  @created_files.each do |filepath|
33
45
  delete_file(filepath) if File.file?(filepath)
@@ -35,7 +47,7 @@ module Aspera
35
47
  @created_files = []
36
48
  end
37
49
 
38
- # ensure that provided folder exists, or create it, generate a unique filename
50
+ # Ensure that provided folder exists, or create it, generate a unique filename
39
51
  # @return path to that unique file
40
52
  def new_file_path_in_folder(temp_folder, prefix: nil, suffix: nil)
41
53
  FileUtils.mkdir_p(temp_folder)
@@ -44,7 +56,7 @@ module Aspera
44
56
  new_file
45
57
  end
46
58
 
47
- # same as above but in global temp folder, with user's name
59
+ # Same as above but in global temp folder, with user's name
48
60
  def new_file_path_global(prefix = nil, suffix: nil)
49
61
  username =
50
62
  begin
@@ -53,11 +65,11 @@ module Aspera
53
65
  'unknown_user'
54
66
  end
55
67
  prefix = [prefix, username].compact.join('-')
56
- new_file_path_in_folder(Etc.systmpdir, prefix: prefix, suffix: suffix)
68
+ new_file_path_in_folder(@global_temp, prefix: prefix, suffix: suffix)
57
69
  end
58
70
 
71
+ # Garbage collect undeleted files
59
72
  def cleanup_expired(temp_folder)
60
- # garbage collect undeleted files
61
73
  Dir.entries(temp_folder).each do |name|
62
74
  file_path = File.join(temp_folder, name)
63
75
  age_sec = (Time.now - File.stat(file_path).mtime).to_i
@@ -1,24 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/transfer/error_info'
3
+ require 'aspera/ascp/management'
4
4
 
5
5
  module Aspera
6
6
  module Transfer
7
- # error raised if transfer fails
7
+ # Error raised if transfer fails
8
8
  class Error < StandardError
9
+ # Error code like on management port
9
10
  attr_reader :err_code
10
11
 
11
- def initialize(message, err_code = nil)
12
- super(message)
13
- @err_code = err_code
12
+ # @param description [String] `Description` on management port
13
+ # @param code [Integer] `Description` on management port, use zero if unknown
14
+ def initialize(description, code: nil)
15
+ super(description)
16
+ @err_code = code.to_i
14
17
  end
15
18
 
19
+ # @return [Hash] Information on that error
16
20
  def info
17
- r = ERROR_INFO[@err_code] || {r: false, c: 'UNKNOWN', m: 'unknown', a: 'unknown'}
21
+ r = Ascp::Management::ERRORS[@err_code] || Ascp::Management::ERRORS[0]
18
22
  return r.merge({i: @err_code})
19
23
  end
20
24
 
21
- def retryable?; info[:r]; end
25
+ # Is that transfer error retryable ?
26
+ # @param message [String, nil] Optional actual message on management port
27
+ def retryable?
28
+ return false if @err_code.eql?(14) && message.eql?('Target address not available')
29
+ info[:r]
30
+ end
22
31
  end
23
32
  end
24
33
  end
@@ -18,11 +18,8 @@ require 'openssl'
18
18
 
19
19
  module Aspera
20
20
  module Transfer
21
- # translate transfer specification to ascp parameter list
21
+ # Translate transfer specification to `ascp` parameter list
22
22
  class Parameters
23
- # `ascp` options to provide a file list
24
- FILE_LIST_OPTIONS = ['--file-list', '--file-pair-list'].freeze
25
- private_constant :FILE_LIST_OPTIONS
26
23
  HTTP_FALLBACK_ACTIVATION_VALUES = ['1', 1, true, 'force'].freeze
27
24
 
28
25
  class << self
@@ -30,7 +27,6 @@ module Aspera
30
27
  def file_list_folder=(value)
31
28
  @file_list_folder = value
32
29
  return if @file_list_folder.nil?
33
-
34
30
  FileUtils.mkdir_p(@file_list_folder)
35
31
  TempFileManager.instance.cleanup_expired(@file_list_folder)
36
32
  end
@@ -42,14 +38,19 @@ module Aspera
42
38
  @file_list_folder ||= TempFileManager.instance.new_file_path_global('asession_filelists')
43
39
  end
44
40
 
45
- # file list is provided directly with ascp arguments
41
+ # File list is provided directly with ascp arguments
46
42
  # @columns ascp_args [Array,NilClass] ascp arguments
47
43
  def ascp_args_file_list?(ascp_args)
48
44
  ascp_args&.any?{ |i| FILE_LIST_OPTIONS.include?(i)}
49
45
  end
50
46
  end
51
47
 
52
- # @columns options [Hash] key: :wss: bool, :ascp_args: array of strings
48
+ # @param job_spec [Hash] Transfer spec
49
+ # @param ascp_args [Array] Other ascp args
50
+ # @param quiet [Bool] Remove ascp output
51
+ # @param trusted_certs [Array] Trusted certificates
52
+ # @param client_ssh_key [Symbol] :rsa or :dsa
53
+ # @param check_ignore_cb [Proc] Callback
53
54
  def initialize(
54
55
  job_spec,
55
56
  ascp_args: nil,
@@ -60,17 +61,17 @@ module Aspera
60
61
  check_ignore_cb: nil
61
62
  )
62
63
  @job_spec = job_spec
64
+ Aspera.assert_type(@job_spec, Hash)
63
65
  @ascp_args = ascp_args.nil? ? [] : ascp_args
66
+ Aspera.assert_type(@ascp_args, Array){'ascp_args'}
67
+ Aspera.assert(@ascp_args.all?(String)){'all ascp arguments must be String'}
64
68
  @wss = wss
65
69
  @quiet = quiet
66
70
  @trusted_certs = trusted_certs.nil? ? [] : trusted_certs
67
- @client_ssh_key = client_ssh_key.nil? ? :rsa : client_ssh_key.to_sym
68
- @check_ignore_cb = check_ignore_cb
69
- Aspera.assert_type(@job_spec, Hash)
70
- Aspera.assert_type(@ascp_args, Array){'ascp_args'}
71
- Aspera.assert(@ascp_args.all?(String)){'all ascp arguments must be String'}
72
71
  Aspera.assert_type(@trusted_certs, Array){'trusted_certs'}
72
+ @client_ssh_key = client_ssh_key.nil? ? :rsa : client_ssh_key.to_sym
73
73
  Aspera.assert_values(@client_ssh_key, Ascp::Installation::CLIENT_SSH_KEY_OPTIONS)
74
+ @check_ignore_cb = check_ignore_cb
74
75
  @builder = CommandLineBuilder.new(@job_spec, Spec::SCHEMA, CommandLineConverter)
75
76
  end
76
77
 
@@ -109,7 +110,7 @@ module Aspera
109
110
  Log.log.debug{"#{file_list_option}=\n#{File.read(file_list_file)}".red}
110
111
  end
111
112
  end
112
- @builder.add_command_line_options(["#{file_list_option}=#{file_list_file}"]) unless file_list_option.nil?
113
+ @builder.add_command_line_options("#{file_list_option}=#{file_list_file}") unless file_list_option.nil?
113
114
  end
114
115
 
115
116
  # @return the list of certificates (option `-i`) to use when token/ssh or wss are used
@@ -118,7 +119,7 @@ module Aspera
118
119
  # use web socket secure for session ?
119
120
  if @builder.read_param('wss_enabled') && (@wss || !@job_spec.key?('fasp_port'))
120
121
  # by default use web socket session if available, unless removed by user
121
- @builder.add_command_line_options(['--ws-connect'])
122
+ @builder.add_command_line_options('--ws-connect')
122
123
  # TODO: option to give order ssh,ws (legacy http is implied by ssh)
123
124
  # This will need to be cleaned up in aspera core
124
125
  @job_spec['ssh_port'] = @builder.read_param('wss_port')
@@ -148,7 +149,7 @@ module Aspera
148
149
  return certificates_to_use
149
150
  end
150
151
 
151
- # translate transfer spec to env vars and command line arguments for ascp
152
+ # Translate transfer spec to env vars and command line arguments for `ascp`
152
153
  def ascp_args
153
154
  env_args = {
154
155
  args: [],
@@ -156,18 +157,27 @@ module Aspera
156
157
  name: :ascp
157
158
  }
158
159
 
159
- # special cases
160
+ # Special cases
160
161
  @job_spec.delete('source_root') if @job_spec.key?('source_root') && @job_spec['source_root'].empty?
161
162
 
162
- # notify multi-session was already used, anyway it was deleted by agent direct
163
+ # Notify multi-session was already used, anyway it was deleted by agent direct
163
164
  Aspera.assert(!@builder.read_param('multi_session'))
164
165
 
165
- # add ssh or wss certificates
166
+ # Add ssh or wss certificates
166
167
  # (reverse, to keep order, as we unshift)
167
168
  remote_certificates&.reverse_each do |cert|
168
169
  env_args[:args].unshift('-i', cert)
169
170
  end
170
171
 
172
+ case (delete_source = @builder.read_param('delete_source'))
173
+ when true
174
+ DELETE_EQUIV.each{ |i| @job_spec[i] = true}
175
+ when false
176
+ DELETE_EQUIV.each{ |i| @job_spec.delete(i)}
177
+ when nil
178
+ else Aspera.error_unexpected_value(delete_source){'delete_source'}
179
+ end
180
+
171
181
  # process parameters as specified in table
172
182
  @builder.process_params
173
183
 
@@ -180,7 +190,7 @@ module Aspera
180
190
  base64_destination = true
181
191
  end
182
192
  # destination will be base64 encoded, put this before source path arguments
183
- @builder.add_command_line_options(['--dest64']) if base64_destination
193
+ @builder.add_command_line_options('--dest64') if base64_destination
184
194
  # optional arguments, at the end to override previous ones (to allow override)
185
195
  @builder.add_command_line_options(@ascp_args)
186
196
  # get list of source files to transfer and build arg for ascp
@@ -190,7 +200,7 @@ module Aspera
190
200
  # ascp4 does not support base64 encoding of destination
191
201
  destination_folder = Base64.strict_encode64(destination_folder) if base64_destination
192
202
  # destination MUST be last command line argument to ascp
193
- @builder.add_command_line_options([destination_folder])
203
+ @builder.add_command_line_options(destination_folder)
194
204
  @builder.add_env_args(env_args)
195
205
  env_args[:args].unshift('-q') if @quiet
196
206
  # add fallback cert and key as arguments if needed
@@ -203,6 +213,10 @@ module Aspera
203
213
  Log.log.debug{"ascp args: #{env_args}"}
204
214
  return env_args
205
215
  end
216
+ DELETE_EQUIV = %w[remove_after_transfer remove_empty_directories remove_empty_source_directory]
217
+ # `ascp` options to provide a file list
218
+ FILE_LIST_OPTIONS = ['--file-list', '--file-pair-list'].freeze
219
+ private_constant :DELETE_EQUIV, :FILE_LIST_OPTIONS
206
220
  end
207
221
  end
208
222
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
6
+ require 'aspera/transfer/error'
7
+
8
+ module Aspera
9
+ module Transfer
10
+ # Implements a simple resume policy
11
+ class Resumer
12
+ # @param iter_max [Integer] Maximum number of executions
13
+ # @param sleep_initial [Integer] Initial wait to re-execute
14
+ # @param sleep_factor [Integer] Multiplier
15
+ # @param sleep_max. [Integer] Max iterations
16
+ def initialize(
17
+ iter_max: 7,
18
+ sleep_initial: 2,
19
+ sleep_factor: 2,
20
+ sleep_max: 60
21
+ )
22
+ Aspera.assert_type(iter_max, Integer){k}
23
+ @iter_max = iter_max
24
+ Aspera.assert_type(sleep_initial, Integer){k}
25
+ @sleep_initial = sleep_initial
26
+ Aspera.assert_type(sleep_factor, Integer){k}
27
+ @sleep_factor = sleep_factor
28
+ Aspera.assert_type(sleep_max, Integer){k}
29
+ @sleep_max = sleep_max
30
+ end
31
+
32
+ # Calls block a number of times (resumes) until success or limit reached
33
+ # This is re-entrant, one resumer can handle multiple transfers in //
34
+ #
35
+ # @param block [Proc]
36
+ def execute_with_resume
37
+ Aspera.assert(block_given?)
38
+ # maximum of retry
39
+ remaining_resumes = @iter_max
40
+ sleep_seconds = @sleep_initial
41
+ Log.log.debug{"retries=#{remaining_resumes}"}
42
+ # try to send the file until ascp is successful
43
+ loop do
44
+ Log.log.debug('Transfer session starting')
45
+ begin
46
+ # Call provided block: execute transfer
47
+ yield
48
+ # Exit retry loop if success
49
+ break
50
+ rescue Error => e
51
+ Log.log.warn{"A transfer error occurred during transfer: #{e.message}"}
52
+ Log.log.debug{"Retryable ? #{e.retryable?}"}
53
+ # do not retry non-retryable
54
+ raise unless e.retryable?
55
+ # exit if we exceed the max number of retry
56
+ raise Error, "Maximum number of retry reached: #{@iter_max}" if remaining_resumes <= 0
57
+ end
58
+
59
+ # take this retry in account
60
+ remaining_resumes -= 1
61
+ Log.log.warn{"Resuming in #{sleep_seconds} seconds (retry left:#{remaining_resumes})"}
62
+
63
+ # wait a bit before retrying, maybe network condition will be better
64
+ sleep(sleep_seconds)
65
+
66
+ # increase retry period
67
+ sleep_seconds *= @sleep_factor
68
+ # cap value
69
+ sleep_seconds = @sleep_max if sleep_seconds > @sleep_max
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -5,7 +5,8 @@ require 'aspera/assert'
5
5
 
6
6
  module Aspera
7
7
  module Transfer
8
- # parameters for Transfer Spec
8
+ # Parameters for Transfer Spec
9
+ # Parameters are generated from JSON Schema.
9
10
  class Spec
10
11
  # default transfer username for access key based transfers
11
12
  ACCESS_KEY_TRANSFER_USER = 'xfer'
@@ -44,12 +45,12 @@ module Aspera
44
45
  end
45
46
 
46
47
  def fix_transferd_resume_policy(transfer_spec)
47
- # Fix discrepency in transfer spec
48
+ # Fix discrepancy in transfer spec
48
49
  transfer_spec['resume_policy'] = POLICY_FIX[transfer_spec['resume_policy']] if transfer_spec.key?('resume_policy')
49
50
  end
50
51
  end
51
52
  SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'spec')
52
- CommandLineBuilder.adjust_properties_defaults(SCHEMA['properties'])
53
+ CommandLineBuilder.validate_schema(SCHEMA, ascp: true)
53
54
  # define constants for enums of parameters: <parameter>_<enum>, e.g. CIPHER_AES_128, DIRECTION_SEND, ...
54
55
  SCHEMA['properties'].each do |name, description|
55
56
  next unless description['enum'].is_a?(Array)