aspera-cli 4.24.1 → 4.25.0.pre

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 (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -745
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +1281 -720
  6. data/bin/ascli +20 -1
  7. data/bin/asession +23 -27
  8. data/lib/aspera/agent/base.rb +10 -21
  9. data/lib/aspera/agent/connect.rb +2 -3
  10. data/lib/aspera/agent/desktop.rb +2 -2
  11. data/lib/aspera/agent/direct.rb +49 -32
  12. data/lib/aspera/agent/factory.rb +31 -0
  13. data/lib/aspera/api/aoc.rb +134 -76
  14. data/lib/aspera/api/cos_node.rb +3 -2
  15. data/lib/aspera/api/faspex.rb +213 -0
  16. data/lib/aspera/api/node.rb +107 -94
  17. data/lib/aspera/ascmd.rb +1 -2
  18. data/lib/aspera/ascp/installation.rb +73 -58
  19. data/lib/aspera/ascp/management.rb +119 -23
  20. data/lib/aspera/assert.rb +39 -11
  21. data/lib/aspera/cli/error.rb +4 -2
  22. data/lib/aspera/cli/extended_value.rb +91 -67
  23. data/lib/aspera/cli/formatter.rb +62 -27
  24. data/lib/aspera/cli/hints.rb +8 -0
  25. data/lib/aspera/cli/info.rb +4 -4
  26. data/lib/aspera/cli/main.rb +76 -84
  27. data/lib/aspera/cli/manager.rb +352 -248
  28. data/lib/aspera/cli/plugins/alee.rb +5 -4
  29. data/lib/aspera/cli/plugins/aoc.rb +175 -195
  30. data/lib/aspera/cli/plugins/ats.rb +4 -4
  31. data/lib/aspera/cli/plugins/base.rb +343 -0
  32. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  33. data/lib/aspera/cli/plugins/config.rb +283 -269
  34. data/lib/aspera/cli/plugins/console.rb +27 -22
  35. data/lib/aspera/cli/plugins/cos.rb +3 -3
  36. data/lib/aspera/cli/plugins/factory.rb +78 -0
  37. data/lib/aspera/cli/plugins/faspex.rb +49 -46
  38. data/lib/aspera/cli/plugins/faspex5.rb +113 -225
  39. data/lib/aspera/cli/plugins/faspio.rb +19 -18
  40. data/lib/aspera/cli/plugins/httpgw.rb +14 -13
  41. data/lib/aspera/cli/plugins/node.rb +162 -149
  42. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  43. data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
  44. data/lib/aspera/cli/plugins/preview.rb +30 -50
  45. data/lib/aspera/cli/plugins/server.rb +21 -21
  46. data/lib/aspera/cli/plugins/shares.rb +45 -47
  47. data/lib/aspera/cli/sync_actions.rb +50 -39
  48. data/lib/aspera/cli/transfer_agent.rb +35 -49
  49. data/lib/aspera/cli/transfer_progress.rb +6 -6
  50. data/lib/aspera/cli/version.rb +3 -3
  51. data/lib/aspera/cli/wizard.rb +70 -55
  52. data/lib/aspera/colors.rb +6 -0
  53. data/lib/aspera/command_line_builder.rb +59 -61
  54. data/lib/aspera/command_line_converter.rb +2 -1
  55. data/lib/aspera/coverage.rb +2 -2
  56. data/lib/aspera/data_repository.rb +1 -1
  57. data/lib/aspera/environment.rb +51 -41
  58. data/lib/aspera/faspex_gw.rb +7 -5
  59. data/lib/aspera/faspex_postproc.rb +1 -1
  60. data/lib/aspera/keychain/factory.rb +1 -2
  61. data/lib/aspera/keychain/macos_security.rb +1 -1
  62. data/lib/aspera/log.rb +37 -9
  63. data/lib/aspera/markdown.rb +31 -0
  64. data/lib/aspera/nagios.rb +7 -6
  65. data/lib/aspera/oauth/base.rb +25 -28
  66. data/lib/aspera/oauth/factory.rb +9 -9
  67. data/lib/aspera/oauth/url_json.rb +2 -1
  68. data/lib/aspera/oauth/web.rb +2 -2
  69. data/lib/aspera/preview/file_types.rb +23 -37
  70. data/lib/aspera/products/connect.rb +7 -6
  71. data/lib/aspera/products/desktop.rb +1 -4
  72. data/lib/aspera/products/other.rb +9 -1
  73. data/lib/aspera/products/transferd.rb +0 -1
  74. data/lib/aspera/rest.rb +168 -113
  75. data/lib/aspera/rest_error_analyzer.rb +4 -4
  76. data/lib/aspera/ssh.rb +7 -4
  77. data/lib/aspera/ssl.rb +41 -0
  78. data/lib/aspera/sync/args.schema.yaml +46 -3
  79. data/lib/aspera/sync/conf.schema.yaml +307 -123
  80. data/lib/aspera/sync/database.rb +2 -1
  81. data/lib/aspera/sync/operations.rb +135 -79
  82. data/lib/aspera/temp_file_manager.rb +17 -5
  83. data/lib/aspera/transfer/error.rb +16 -7
  84. data/lib/aspera/transfer/parameters.rb +35 -22
  85. data/lib/aspera/transfer/resumer.rb +74 -0
  86. data/lib/aspera/transfer/spec.rb +5 -5
  87. data/lib/aspera/transfer/spec.schema.yaml +170 -59
  88. data/lib/aspera/transfer/spec_doc.rb +49 -43
  89. data/lib/aspera/uri_reader.rb +2 -2
  90. data/lib/aspera/web_auth.rb +6 -6
  91. data/lib/transferd_pb.rb +2 -2
  92. data.tar.gz.sig +0 -0
  93. metadata +26 -11
  94. metadata.gz.sig +0 -0
  95. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  96. data/lib/aspera/cli/plugin.rb +0 -333
  97. data/lib/aspera/cli/plugin_factory.rb +0 -81
  98. data/lib/aspera/resumer.rb +0 -77
  99. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -8,7 +8,7 @@ else
8
8
  require 'sqlite3'
9
9
  end
10
10
 
11
- # A wrapper class that provides common API for Ruby and JRuby
11
+ # A wrapper class that provides common API for sqlite in both Ruby and JRuby
12
12
  class SqLite3Wrapper
13
13
  def initialize(db_path)
14
14
  @db_path = db_path
@@ -52,6 +52,7 @@ end
52
52
 
53
53
  module Aspera
54
54
  module Sync
55
+ # Access `async` sqlite database
55
56
  class Database
56
57
  def initialize(db_path)
57
58
  @db = SqLite3Wrapper.new(db_path)
@@ -18,26 +18,28 @@ 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
+
26
+ SCP_REMOTE_REGEX = /\A(?:(?:(?<user>[^@:\s]+)@)?(?<host>[^:\s]+):)?(?<path>.+)\z/
25
27
 
26
28
  class << self
27
29
  # 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(params, remote_dir_key, transfer_spec)
30
+ # @param sync_info [Hash] Sync parameters, in `conf` or `args` format.
31
+ # @param remote_dir_key [String] Key to update in above hash
32
+ # @param transfer_spec [Hash] Transfer spec
33
+ def update_remote_dir(sync_info, remote_dir_key, transfer_spec)
32
34
  if transfer_spec.dig(*%w[tags aspera node file_id])
33
35
  # in AoC, use gen4
34
- params[remote_dir_key] = '/'
36
+ sync_info[remote_dir_key] = '/'
35
37
  elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
36
38
  # TODO : something more generic, independent of Shares
37
39
  # in Shares, the actual folder on remote end is not always the same as the name of the share
38
40
  remote_key = transfer_spec['direction'].eql?('send') ? 'destination' : 'source'
39
41
  actual_remote = transfer_spec['paths']&.first&.[](remote_key)
40
- params[remote_dir_key] = actual_remote if actual_remote
42
+ sync_info[remote_dir_key] = actual_remote if actual_remote
41
43
  end
42
44
  nil
43
45
  end
@@ -70,36 +72,36 @@ module Aspera
70
72
  end
71
73
 
72
74
  # Get symbol of sync direction, defaulting to :push
73
- # @param params [Hash] Sync parameters, old or new format
75
+ # @param sync_info [Hash] Sync parameters, `conf` or `args` format
74
76
  # @return [Symbol] direction symbol, one of :push, :pull, :bidi
75
- def direction_sym(params)
76
- (params['direction'] || DEFAULT_DIRECTION).to_sym
77
+ def direction_sym(sync_info)
78
+ (sync_info['direction'] || DEFAULT_DIRECTION).to_sym
77
79
  end
78
80
 
79
81
  # Start the sync process
80
- # @param params [Hash] Sync parameters, old or new format
81
- # @param opt_ts [Hash] Optional transfer spec
82
- # @param &block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
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'}
82
+ # @param sync_info [Hash] Sync parameters, old or new format
83
+ # @param opt_ts [Hash] Optional transfer spec
84
+ # @param &block [nil, Proc] block to generate transfer spec, takes: `direction` (one of DIRECTIONS), `local_dir`, `remote_dir`
85
+ def start(sync_info, opt_ts = nil)
86
+ Log.dump(:sync_params_initial, sync_info)
87
+ Aspera.assert_type(sync_info, Hash)
88
+ Aspera.assert(PARAM_KEYS.any?{ |k| sync_info.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
87
89
  env_args = {
88
90
  args: [],
89
91
  env: {}
90
92
  }
91
- if params.key?('local')
92
- # "conf" format
93
- Aspera.assert_type(params['local'], Hash){'local'}
94
- remote = params['remote']
93
+ if sync_info.key?('local')
94
+ # `conf` format
95
+ Aspera.assert_type(sync_info['local'], Hash){'local'}
96
+ remote = sync_info['remote']
95
97
  Aspera.assert_type(remote, Hash){'remote'}
96
98
  Aspera.assert_type(remote['path'], String){'remote path'}
97
99
  # get transfer spec if possible, and feed back to new structure
98
100
  if block_given?
99
- transfer_spec = yield(direction_sym(params), params['local']['path'], remote['path'])
101
+ transfer_spec = yield(direction_sym(sync_info), sync_info['local']['path'], remote['path'])
100
102
  Log.dump(:auth_ts, transfer_spec)
101
103
  transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
102
- tspec_to_sync_info(transfer_spec, params, CONF_SCHEMA)
104
+ tspec_to_sync_info(transfer_spec, sync_info, CONF_SCHEMA)
103
105
  update_remote_dir(remote, 'path', transfer_spec)
104
106
  end
105
107
  remote['connect_mode'] ||= transfer_spec['wss_enabled'] ? 'ws' : 'ssh'
@@ -109,38 +111,38 @@ module Aspera
109
111
  remote['private_key_paths'].concat(add_certificates)
110
112
  end
111
113
  # '--exclusive-mgmt-port=12345', '--arg-err-path=-',
112
- env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(params))}"]
113
- Log.dump(:sync_conf, params)
114
+ env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_info))}"]
115
+ Log.dump(:sync_conf, sync_info)
114
116
  agent = Agent::Direct.new
115
117
  agent.start_and_monitor_process(session: {}, name: :async, **env_args)
116
118
  else
117
- # "args" format
119
+ # `args` format
118
120
  raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
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)
121
+ sync_info.keys.push('instance').uniq.sort.eql?(CMDLINE_PARAMS_KEYS)
122
+ Aspera.assert_type(sync_info['sessions'], Array)
123
+ Aspera.assert_type(sync_info['sessions'].first, Hash)
122
124
  if block_given?
123
- params['sessions'].each do |session|
125
+ sync_info['sessions'].each do |session|
124
126
  Aspera.assert_type(session['local_dir'], String){'local_dir'}
125
127
  Aspera.assert_type(session['remote_dir'], String){'remote_dir'}
126
128
  transfer_spec = yield(direction_sym(session), session['local_dir'], session['remote_dir'])
127
129
  Log.dump(:auth_ts, transfer_spec)
128
130
  transfer_spec.deep_merge!(opt_ts) unless opt_ts.nil?
129
- tspec_to_sync_info(transfer_spec, session, SESSION_SCHEMA)
131
+ tspec_to_sync_info(transfer_spec, session, ARGS_SESSION_SCHEMA)
130
132
  session['private_key_paths'] = Ascp::Installation.instance.aspera_token_ssh_key_paths(:rsa) if transfer_spec.key?('token')
131
133
  update_remote_dir(session, 'remote_dir', transfer_spec)
132
134
  end
133
135
  end
134
- if params.key?('instance')
135
- Aspera.assert_type(params['instance'], Hash)
136
- instance_builder = CommandLineBuilder.new(params['instance'], INSTANCE_SCHEMA, CommandLineConverter)
136
+ if sync_info.key?('instance')
137
+ Aspera.assert_type(sync_info['instance'], Hash)
138
+ instance_builder = CommandLineBuilder.new(sync_info['instance'], ARGS_INSTANCE_SCHEMA, CommandLineConverter)
137
139
  instance_builder.process_params
138
140
  instance_builder.add_env_args(env_args)
139
141
  end
140
- params['sessions'].each do |session_params|
142
+ sync_info['sessions'].each do |session_params|
141
143
  Aspera.assert_type(session_params, Hash)
142
144
  Aspera.assert(session_params.key?('name')){'session must contain at least: name'}
143
- session_builder = CommandLineBuilder.new(session_params, SESSION_SCHEMA, CommandLineConverter)
145
+ session_builder = CommandLineBuilder.new(session_params, ARGS_SESSION_SCHEMA, CommandLineConverter)
144
146
  session_builder.process_params
145
147
  session_builder.add_env_args(env_args)
146
148
  end
@@ -168,24 +170,24 @@ module Aspera
168
170
  end
169
171
 
170
172
  # Run `asyncadmin` to get status of sync session
171
- # @param params [Hash] sync parameters in conf or args format
173
+ # @param sync_info [Hash] sync parameters in conf or args format
172
174
  # @return [Hash] parsed output of asyncadmin
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'}
175
+ def admin_status(sync_info)
176
+ Aspera.assert(PARAM_KEYS.any?{ |k| sync_info.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
175
177
  arguments = ['--quiet']
176
- if params.key?('local')
177
- # "conf" format
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')}")
178
+ if sync_info.key?('local')
179
+ # `conf` format
180
+ arguments.push("--name=#{sync_info['name']}")
181
+ if sync_info.key?('local_db_dir')
182
+ arguments.push("--local-db-dir=#{sync_info['local_db_dir']}")
183
+ elsif sync_info.dig('local', 'path')
184
+ arguments.push("--local-dir=#{sync_info.dig('local', 'path')}")
183
185
  else
184
186
  raise Error, 'Missing either local_db_dir or local.path'
185
187
  end
186
188
  else
187
- # "args" format
188
- session = params['sessions'].first
189
+ # `args` format
190
+ session = sync_info['sessions'].first
189
191
  arguments.push("--name=#{session['name']}")
190
192
  if session.key?('local_db_dir')
191
193
  arguments.push("--local-db-dir=#{session['local_db_dir']}")
@@ -199,23 +201,23 @@ module Aspera
199
201
  return parse_status(stdout)
200
202
  end
201
203
 
202
- # Find the local database folder based on params
203
- # @param params [Hash] sync parameters in conf or args format
204
- # @return [String, nil] path to "local DB dir", i.e. folder that contains folders that contain snap.db
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')
208
- # "conf" format
209
- if params.key?('local_db_dir')
210
- return params['local_db_dir']
211
- elsif (local_path = params.dig('local', 'path'))
204
+ # Find the local database folder based on sync_info
205
+ # @param sync_info [Hash] sync parameters in conf or args format
206
+ # @return [String, nil] Path to "local DB dir", i.e. folder that contains folders that contain `snap.db`
207
+ def local_db_folder(sync_info)
208
+ Aspera.assert(PARAM_KEYS.any?{ |k| sync_info.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
209
+ if sync_info.key?('local')
210
+ # `conf` format
211
+ if sync_info.key?('local_db_dir')
212
+ return sync_info['local_db_dir']
213
+ elsif (local_path = sync_info.dig('local', 'path'))
212
214
  return local_path
213
215
  elsif exception
214
216
  raise Error, 'Missing either local_db_dir or local.path'
215
217
  end
216
218
  else
217
- # "args" format
218
- session = params['sessions'].first
219
+ # `args` format
220
+ session = sync_info['sessions'].first
219
221
  if session.key?('local_db_dir')
220
222
  return session['local_db_dir']
221
223
  elsif session.key?('local_dir')
@@ -227,19 +229,19 @@ module Aspera
227
229
  nil
228
230
  end
229
231
 
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')
233
- # "conf" format
234
- return params['name']
232
+ def session_name(sync_info)
233
+ Aspera.assert(PARAM_KEYS.any?{ |k| sync_info.key?(k)}, type: Error){'At least one of `local` or `sessions` must be present in async parameters'}
234
+ if sync_info.key?('local')
235
+ # `conf` format
236
+ return sync_info['name']
235
237
  else
236
- # "args" format
237
- return params['sessions'].first['name']
238
+ # `args` format
239
+ return sync_info['sessions'].first['name']
238
240
  end
239
241
  end
240
242
 
241
- def session_db_file(params)
242
- db_file = File.join(local_db_folder(params), PRIVATE_FOLDER, session_name(params), ASYNC_DB)
243
+ def session_db_file(sync_info)
244
+ db_file = File.join(local_db_folder(sync_info), PRIVATE_FOLDER, session_name(sync_info), ASYNC_DB)
243
245
  Aspera.assert(File.exist?(db_file)){"Database file #{db_file} does not exist"}
244
246
  db_file
245
247
  end
@@ -274,23 +276,77 @@ module Aspera
274
276
  end
275
277
  end
276
278
  end
279
+
280
+ # Search given option in JSON Schema tree
281
+ # @param schema [Hash] JSON Schema tree (root or sub-tree)
282
+ # @param path [Array] Path to subtree
283
+ # @param option [String] Option to search
284
+ # @return [Array,nil] with path/schema for that option
285
+ def find_option(schema, path, option)
286
+ if %w[x-cli-option x-cli-short].any?{ |i| schema[i].eql?(option)}
287
+ Log.log.debug('Special') if schema['x-cli-special']
288
+ return [path, schema]
289
+ end
290
+ if schema['type'].eql?('object')
291
+ schema['properties']&.each do |name, props|
292
+ res = find_option(props, path + [name], option)
293
+ return res unless res.nil?
294
+ end
295
+ end
296
+ return
297
+ end
298
+
299
+ # Translate `async` native command line arguments to `conf` JSON
300
+ def args_to_conf(args)
301
+ result = {}
302
+ while args.any?
303
+ option = args.shift
304
+ if option =~ /^(--[^=]+)=(.*)$/
305
+ option = ::Regexp.last_match(1) # "--toto"
306
+ args.unshift(::Regexp.last_match(2))
307
+ end
308
+ if option.eql?('--preserve-time') || option.eql?('-t')
309
+ args.unshift('--preserve-creation-time') if Environment.instance.os.eql?(Environment::OS_WINDOWS)
310
+ option = '--preserve-modification-time'
311
+ end
312
+ if option.eql?('--remote') || option.eql?('-r')
313
+ value = args.first
314
+ if (m = SCP_REMOTE_REGEX.match(value))
315
+ if m[:host]
316
+ args.shift
317
+ args.unshift("--host=#{m[:host]}")
318
+ args.unshift("--user=#{m[:user]}") if m[:user]
319
+ args.unshift(m[:path])
320
+ end
321
+ end
322
+ end
323
+ path, props = find_option(CONF_SCHEMA, [], option)
324
+ raise "Option not found: #{option}" if path.nil?
325
+ last_key = path.pop
326
+ # navigate in the current result to insert the value
327
+ current = result
328
+ path.each do |key|
329
+ current[key] ||= {}
330
+ current = current[key]
331
+ end
332
+ current[last_key] = props['x-cli-switch'] ? true : args.shift
333
+ end
334
+ return result
335
+ end
277
336
  end
278
337
  # Private stuff:
279
338
  # Read JSON schema and mapping to command line options
280
- INSTANCE_SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'args')
281
- SESSION_SCHEMA = INSTANCE_SCHEMA['properties']['sessions']['items']
282
- INSTANCE_SCHEMA['properties'].delete('sessions')
283
- CONF_SCHEMA = CommandLineBuilder.read_schema(__FILE__, 'conf')
284
- CommandLineBuilder.adjust_properties_defaults(INSTANCE_SCHEMA['properties'])
285
- CommandLineBuilder.adjust_properties_defaults(SESSION_SCHEMA['properties'])
286
- CommandLineBuilder.adjust_properties_defaults(CONF_SCHEMA['properties'])
339
+ ARGS_INSTANCE_SCHEMA = CommandLineBuilder.read_schema(__dir__, 'args')
340
+ ARGS_SESSION_SCHEMA = ARGS_INSTANCE_SCHEMA['properties']['sessions']['items']
341
+ ARGS_INSTANCE_SCHEMA['properties'].delete('sessions')
342
+ CONF_SCHEMA = CommandLineBuilder.read_schema(__dir__, 'conf')
287
343
  CMDLINE_PARAMS_KEYS = %w[instance sessions].freeze
288
344
  ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
289
345
  PRIVATE_FOLDER = '.private-asp'
290
346
  ASYNC_DB = 'snap.db'
291
347
  PARAM_KEYS = %w[local sessions].freeze
292
348
 
293
- private_constant :INSTANCE_SCHEMA, :SESSION_SCHEMA, :CONF_SCHEMA, :CMDLINE_PARAMS_KEYS, :ASYNC_ADMIN_EXECUTABLE, :PRIVATE_FOLDER, :ASYNC_DB, :PARAM_KEYS
349
+ private_constant :ARGS_INSTANCE_SCHEMA, :ARGS_SESSION_SCHEMA, :CMDLINE_PARAMS_KEYS, :ASYNC_ADMIN_EXECUTABLE, :PRIVATE_FOLDER, :ASYNC_DB, :PARAM_KEYS
294
350
  end
295
351
  end
296
352
  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,16 @@ 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_array_all(@ascp_args, String){'ascp_args'}
64
67
  @wss = wss
65
68
  @quiet = quiet
66
69
  @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
70
  Aspera.assert_type(@trusted_certs, Array){'trusted_certs'}
71
+ @client_ssh_key = client_ssh_key.nil? ? :rsa : client_ssh_key.to_sym
73
72
  Aspera.assert_values(@client_ssh_key, Ascp::Installation::CLIENT_SSH_KEY_OPTIONS)
73
+ @check_ignore_cb = check_ignore_cb
74
74
  @builder = CommandLineBuilder.new(@job_spec, Spec::SCHEMA, CommandLineConverter)
75
75
  end
76
76
 
@@ -109,7 +109,7 @@ module Aspera
109
109
  Log.log.debug{"#{file_list_option}=\n#{File.read(file_list_file)}".red}
110
110
  end
111
111
  end
112
- @builder.add_command_line_options(["#{file_list_option}=#{file_list_file}"]) unless file_list_option.nil?
112
+ @builder.add_command_line_options("#{file_list_option}=#{file_list_file}") unless file_list_option.nil?
113
113
  end
114
114
 
115
115
  # @return the list of certificates (option `-i`) to use when token/ssh or wss are used
@@ -118,7 +118,7 @@ module Aspera
118
118
  # use web socket secure for session ?
119
119
  if @builder.read_param('wss_enabled') && (@wss || !@job_spec.key?('fasp_port'))
120
120
  # by default use web socket session if available, unless removed by user
121
- @builder.add_command_line_options(['--ws-connect'])
121
+ @builder.add_command_line_options('--ws-connect')
122
122
  # TODO: option to give order ssh,ws (legacy http is implied by ssh)
123
123
  # This will need to be cleaned up in aspera core
124
124
  @job_spec['ssh_port'] = @builder.read_param('wss_port')
@@ -148,7 +148,7 @@ module Aspera
148
148
  return certificates_to_use
149
149
  end
150
150
 
151
- # translate transfer spec to env vars and command line arguments for ascp
151
+ # Translate transfer spec to env vars and command line arguments for `ascp`
152
152
  def ascp_args
153
153
  env_args = {
154
154
  args: [],
@@ -156,18 +156,27 @@ module Aspera
156
156
  name: :ascp
157
157
  }
158
158
 
159
- # special cases
159
+ # Special cases
160
160
  @job_spec.delete('source_root') if @job_spec.key?('source_root') && @job_spec['source_root'].empty?
161
161
 
162
- # notify multi-session was already used, anyway it was deleted by agent direct
162
+ # Notify multi-session was already used, anyway it was deleted by agent direct
163
163
  Aspera.assert(!@builder.read_param('multi_session'))
164
164
 
165
- # add ssh or wss certificates
165
+ # Add ssh or wss certificates
166
166
  # (reverse, to keep order, as we unshift)
167
167
  remote_certificates&.reverse_each do |cert|
168
168
  env_args[:args].unshift('-i', cert)
169
169
  end
170
170
 
171
+ case (delete_source = @builder.read_param('delete_source'))
172
+ when true
173
+ DELETE_EQUIV.each{ |i| @job_spec[i] = true}
174
+ when false
175
+ DELETE_EQUIV.each{ |i| @job_spec.delete(i)}
176
+ when nil
177
+ else Aspera.error_unexpected_value(delete_source){'delete_source'}
178
+ end
179
+
171
180
  # process parameters as specified in table
172
181
  @builder.process_params
173
182
 
@@ -180,7 +189,7 @@ module Aspera
180
189
  base64_destination = true
181
190
  end
182
191
  # destination will be base64 encoded, put this before source path arguments
183
- @builder.add_command_line_options(['--dest64']) if base64_destination
192
+ @builder.add_command_line_options('--dest64') if base64_destination
184
193
  # optional arguments, at the end to override previous ones (to allow override)
185
194
  @builder.add_command_line_options(@ascp_args)
186
195
  # get list of source files to transfer and build arg for ascp
@@ -190,7 +199,7 @@ module Aspera
190
199
  # ascp4 does not support base64 encoding of destination
191
200
  destination_folder = Base64.strict_encode64(destination_folder) if base64_destination
192
201
  # destination MUST be last command line argument to ascp
193
- @builder.add_command_line_options([destination_folder])
202
+ @builder.add_command_line_options(destination_folder)
194
203
  @builder.add_env_args(env_args)
195
204
  env_args[:args].unshift('-q') if @quiet
196
205
  # add fallback cert and key as arguments if needed
@@ -198,11 +207,15 @@ module Aspera
198
207
  env_args[:args].unshift('-Y', Ascp::Installation.instance.path(:fallback_private_key))
199
208
  env_args[:args].unshift('-I', Ascp::Installation.instance.path(:fallback_certificate))
200
209
  end
201
- # disable redis in client
202
- env_args[:env]['ASPERA_TEST_REDIS_DISABLE'] = 'true'
210
+ # disable redis in client, only for ascp, this makes ascp4 fail
211
+ env_args[:env]['ASPERA_TEST_REDIS_DISABLE'] = 'true' if env_args[:name].eql?(:ascp)
203
212
  Log.log.debug{"ascp args: #{env_args}"}
204
213
  return env_args
205
214
  end
215
+ DELETE_EQUIV = %w[remove_after_transfer remove_empty_directories remove_empty_source_directory]
216
+ # `ascp` options to provide a file list
217
+ FILE_LIST_OPTIONS = ['--file-list', '--file-pair-list'].freeze
218
+ private_constant :DELETE_EQUIV, :FILE_LIST_OPTIONS
206
219
  end
207
220
  end
208
221
  end