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
@@ -33,25 +33,12 @@ module Aspera
33
33
  class Installation
34
34
  include Singleton
35
35
 
36
- DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
37
- <?xml version='1.0' encoding='UTF-8'?>
38
- <CONF version="2">
39
- <default>
40
- <file_system>
41
- <resume_suffix>.aspera-ckpt</resume_suffix>
42
- <partial_file_suffix>.partial</partial_file_suffix>
43
- </file_system>
44
- </default>
45
- </CONF>
46
- END_OF_CONFIG_FILE
47
- # all executable files from SDK
48
- EXE_FILES = %i[ascp ascp4 async transferd].freeze
49
- SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
50
- TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
51
- # filename for ascp with optional extension (Windows)
52
- private_constant :DEFAULT_ASPERA_CONF, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
53
36
  # options for SSH client private key
54
37
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
38
+ # prefix
39
+ USE_PRODUCT_PREFIX = 'product:'
40
+ # policy for product selection
41
+ FIRST_FOUND = 'FIRST'
55
42
 
56
43
  # Loads YAML from cloud with locations of SDK archives for all platforms
57
44
  # @return location structure
@@ -66,12 +53,13 @@ module Aspera
66
53
  end
67
54
  end
68
55
 
69
- # set ascp executable path
56
+ # set ascp executable "location"
70
57
  def ascp_path=(v)
71
58
  Aspera.assert_type(v, String)
72
- Aspera.assert(!v.empty?){'ascp path cannot be empty: check your config file'}
73
- Aspera.assert(File.exist?(v)){"No such file: [#{v}]"}
74
- @path_to_ascp = v
59
+ Aspera.assert(!v.empty?){'ascp location cannot be empty: check your config file'}
60
+ @ascp_location = v
61
+ @ascp_path = nil
62
+ return
75
63
  end
76
64
 
77
65
  def ascp_path
@@ -88,13 +76,12 @@ module Aspera
88
76
  def use_ascp_from_product(product_name)
89
77
  if product_name.eql?(FIRST_FOUND)
90
78
  pl = installed_products.first
91
- raise "no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
79
+ raise "No Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf transferd install" if pl.nil?
92
80
  else
93
81
  pl = installed_products.find{ |i| i[:name].eql?(product_name)}
94
- raise "no such product installed: #{product_name}" if pl.nil?
82
+ raise "No such product installed: #{product_name}" if pl.nil?
95
83
  end
96
- self.ascp_path = pl[:ascp_path]
97
- Log.log.debug{"ascp_path=#{@path_to_ascp}"}
84
+ @ascp_path = pl[:ascp_path]
98
85
  end
99
86
 
100
87
  # @return [Hash] with key = file name (String), and value = path to file
@@ -111,7 +98,9 @@ module Aspera
111
98
  end
112
99
  end
113
100
 
101
+ # TODO: if using another product than SDK, should use files from there
114
102
  def check_or_create_sdk_file(filename, force: false, &block)
103
+ FileUtils.mkdir_p(Products::Transferd.sdk_directory)
115
104
  return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
116
105
  end
117
106
 
@@ -127,10 +116,18 @@ module Aspera
127
116
  file = if k.eql?(:transferd)
128
117
  Products::Transferd.transferd_path
129
118
  else
130
- # ensure at least ascp is found
131
- use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
119
+ # find ascp when needed
120
+ if @ascp_path.nil?
121
+ if @ascp_location.start_with?(USE_PRODUCT_PREFIX)
122
+ use_ascp_from_product(@ascp_location[USE_PRODUCT_PREFIX.length..-1])
123
+ else
124
+ @ascp_path = @ascp_location
125
+ end
126
+ Aspera.assert(File.exist?(@ascp_path)){"No such file: [#{@ascp_path}]"}
127
+ Log.log.debug{"ascp_path=#{@ascp_path}"}
128
+ end
132
129
  # NOTE: that there might be a .exe at the end
133
- @path_to_ascp.gsub('ascp', k.to_s)
130
+ @ascp_path.gsub('ascp', k.to_s)
134
131
  end
135
132
  when :ssh_private_dsa, :ssh_private_rsa
136
133
  # assume last 3 letters are type
@@ -195,7 +192,9 @@ module Aspera
195
192
  return exe_version
196
193
  end
197
194
 
198
- def ascp_pvcl_info
195
+ # Extract some stings from ascp logs
196
+ # Folder, PVCL, version, license information
197
+ def ascp_info_from_log
199
198
  data = {}
200
199
  # read PATHs from ascp directly, and pvcl modules as well
201
200
  Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
@@ -227,8 +226,9 @@ module Aspera
227
226
  return data
228
227
  end
229
228
 
230
- # extract some stings from ascp binary
231
- def ascp_ssl_info
229
+ # Extract some stings from ascp binary
230
+ # Openssl information
231
+ def ascp_info_from_file
232
232
  data = {}
233
233
  File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
234
234
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
@@ -243,17 +243,17 @@ module Aspera
243
243
  # information for `ascp info`
244
244
  def ascp_info
245
245
  ascp_data = file_paths
246
- ascp_data.merge!(ascp_pvcl_info)
247
- ascp_data.merge!(ascp_ssl_info)
246
+ ascp_data.merge!(ascp_info_from_log)
247
+ ascp_data.merge!(ascp_info_from_file)
248
248
  return ascp_data
249
249
  end
250
250
 
251
251
  # @return the url for download of SDK archive for the given platform and version
252
252
  def sdk_url_for_platform(platform: nil, version: nil)
253
- locations = sdk_locations
253
+ all_locations = sdk_locations
254
254
  platform = Environment.instance.architecture if platform.nil?
255
- locations = locations.select{ |l| l['platform'].eql?(platform)}
256
- raise "No SDK for platform: #{platform}" if locations.empty?
255
+ locations = all_locations.select{ |l| l['platform'].eql?(platform)}
256
+ raise "No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}" if locations.empty?
257
257
  version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
258
258
  info = locations.select{ |entry| entry['version'].eql?(version)}
259
259
  raise "No such version: #{version} for #{platform}" if info.empty?
@@ -271,7 +271,9 @@ module Aspera
271
271
  Zip::File.open(sdk_archive_path) do |zip_file|
272
272
  zip_file.each do |entry|
273
273
  next if entry.name.end_with?('/')
274
- yield(entry.name, entry.get_input_stream, nil)
274
+ entry.get_input_stream do |io|
275
+ yield(entry.name, io, nil)
276
+ end
275
277
  end
276
278
  end
277
279
  # Other Unixes use tar.gz
@@ -296,7 +298,7 @@ module Aspera
296
298
  # @param folder [String] Destination folder path
297
299
  # @param backup [Bool] If destination folder exists, then rename
298
300
  # @param with_exe [Bool] If false, only retrieves files, but do not generate or restrict access
299
- # @param &block [Proc] A lambda that receives a file path from archive and tells detination sub folder(end with /) or file, or nil to not extract
301
+ # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
300
302
  # @return ascp version (from execution)
301
303
  def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
302
304
  url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
@@ -308,6 +310,7 @@ module Aspera
308
310
  Products::Transferd::RUNTIME_FOLDERS.any?{ |i| name.match?(%r{^[^/]*/#{i}/})} ? '/' : nil
309
311
  end
310
312
  end
313
+ FileUtils.mkdir_p(folder)
311
314
  # rename old install
312
315
  if backup && !Dir.empty?(folder)
313
316
  Log.log.warn('Previous install exists, renaming folder.')
@@ -333,7 +336,7 @@ module Aspera
333
336
  end
334
337
  end
335
338
  return unless with_exe
336
- # ensure necessary files are there, or generate them
339
+ # Ensure necessary files are there, or generate them
337
340
  SDK_FILES.each do |file_id_sym|
338
341
  file_path = path(file_id_sym)
339
342
  if file_path && EXE_FILES.include?(file_id_sym)
@@ -352,11 +355,27 @@ module Aspera
352
355
 
353
356
  private
354
357
 
355
- # policy for product selection
356
- FIRST_FOUND = 'FIRST'
358
+ DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
359
+ <?xml version='1.0' encoding='UTF-8'?>
360
+ <CONF version="2">
361
+ <default>
362
+ <file_system>
363
+ <resume_suffix>.aspera-ckpt</resume_suffix>
364
+ <partial_file_suffix>.partial</partial_file_suffix>
365
+ </file_system>
366
+ </default>
367
+ </CONF>
368
+ END_OF_CONFIG_FILE
369
+ # all executable files from SDK
370
+ EXE_FILES = %i[ascp ascp4 async transferd].freeze
371
+ SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
372
+ TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
373
+ # filename for ascp with optional extension (Windows)
374
+ private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
357
375
 
358
376
  def initialize
359
- @path_to_ascp = nil
377
+ @ascp_path = nil
378
+ @ascp_location = nil
360
379
  @sdk_dir = nil
361
380
  @found_products = nil
362
381
  @transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
@@ -366,22 +385,18 @@ module Aspera
366
385
 
367
386
  # @return the list of installed products in format of product_locations_on_current_os
368
387
  def installed_products
369
- if @found_products.nil?
370
- # :expected M app name is taken from the manifest if present, else defaults to this value
371
- # :app_root M main folder for the application
372
- # :log_root O location of log files (Linux uses syslog)
373
- # :run_root O only for Connect Client, location of http port file
374
- # :sub_bin O subfolder with executables, default : bin
375
- scan_locations = Products::Transferd.locations.concat(
376
- Products::Desktop.locations,
377
- Products::Connect.locations,
378
- Products::Other::LOCATION_ON_THIS_OS
379
- )
380
- # .each {|item| item.deep_do {|h, _k, _v, _m|h.freeze}}.freeze
381
- # search installed products: with ascp
382
- @found_products = Products::Other.find(scan_locations)
383
- end
384
- return @found_products
388
+ return @found_products unless @found_products.nil?
389
+ # :expected M app name is taken from the manifest if present, else defaults to this value
390
+ # :app_root M main folder for the application
391
+ # :log_root O location of log files (Linux uses syslog)
392
+ # :run_root O only for Connect Client, location of http port file
393
+ # :sub_bin O subfolder with executables, default : bin
394
+ scan_locations = Products::Transferd.locations +
395
+ Products::Desktop.locations +
396
+ Products::Connect.locations +
397
+ Products::Other::LOCATION_ON_THIS_OS
398
+ # search installed products: with ascp
399
+ @found_products = Products::Other.find(scan_locations)
385
400
  end
386
401
  end
387
402
  end
@@ -6,6 +6,90 @@ module Aspera
6
6
  module Ascp
7
7
  # processing of ascp management port events
8
8
  class Management
9
+ # from https://www.google.com/search?q=FASP+error+codes
10
+ # Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
11
+ # rubocop:disable Layout/FirstHashElementLineBreak
12
+ ERRORS = {
13
+ # id retry-able mnemo/code message additional info
14
+ 0 => {r: false, c: 'UNKNOWN', m: 'unknown', a: 'unknown'},
15
+ 1 => {r: false, c: 'FASP_PROTO', m: 'Generic fasp(tm) protocol error', a: 'fasp(tm) error'},
16
+ 2 => {r: false, c: 'ASCP', m: 'Generic SCP error', a: 'ASCP error'},
17
+ 3 => {r: false, c: 'AMBIGUOUS_TARGET', m: 'Target incorrectly specified', a: 'Ambiguous target'},
18
+ 4 => {r: false, c: 'NO_SUCH_FILE', m: 'No such file or directory', a: 'No such file or directory'},
19
+ 5 => {r: false, c: 'NO_PERMS', m: 'Insufficient permission to read or write', a: 'Insufficient permissions'},
20
+ 6 => {r: false, c: 'NOT_DIR', m: 'Target is not a directory', a: 'Target must be a directory'},
21
+ 7 => {r: false, c: 'IS_DIR', m: 'File is a directory - expected regular file', a: 'Expected regular file'},
22
+ 8 => {r: false, c: 'USAGE', m: 'Incorrect usage of scp command', a: 'Incorrect usage of Aspera scp command'},
23
+ 9 => {r: false, c: 'LIC_DUP', m: 'Duplicate license', a: 'Duplicate license'},
24
+ 10 => {r: false, c: 'LIC_RATE_EXCEEDED', m: 'Rate exceeds the cap imposed by license', a: 'Rate exceeds cap imposed by license'},
25
+ 11 => {r: false, c: 'INTERNAL_ERROR', m: 'Internal error (unexpected error)', a: 'Internal error'},
26
+ 12 => {r: true, c: 'TRANSFER_ERROR', m: 'Error establishing control connection',
27
+ a: 'Error establishing SSH connection (check SSH port and firewall)'},
28
+ 13 => {r: true, c: 'TRANSFER_TIMEOUT', m: 'Timeout establishing control connection',
29
+ a: 'Timeout establishing SSH connection (check SSH port and firewall)'},
30
+ 14 => {r: true, c: 'CONNECTION_ERROR', m: 'Error establishing data connection',
31
+ a: 'Error establishing UDP connection (check UDP port and firewall)'},
32
+ 15 => {r: true, c: 'CONNECTION_TIMEOUT', m: 'Timeout establishing data connection',
33
+ a: 'Timeout establishing UDP connection (check UDP port and firewall)'},
34
+ 16 => {r: true, c: 'CONNECTION_LOST', m: 'Connection lost', a: 'Connection lost'},
35
+ 17 => {r: true, c: 'RCVR_SEND_ERROR', m: 'Receiver fails to send feedback', a: 'Network failure (receiver can\'t send feedback)'},
36
+ 18 => {r: true, c: 'RCVR_RECV_ERROR', m: 'Receiver fails to receive data packets', a: 'Network failure (receiver can\'t receive UDP data)'},
37
+ 19 => {r: false, c: 'AUTH', m: 'Authentication failure', a: 'Authentication failure'},
38
+ 20 => {r: false, c: 'NOTHING', m: 'Nothing to transfer', a: 'Nothing to transfer'},
39
+ 21 => {r: false, c: 'NOT_REGULAR', m: 'Not a regular file (special file)', a: 'Not a regular file'},
40
+ 22 => {r: false, c: 'FILE_TABLE_OVR', m: 'File table overflow', a: 'File table overflow'},
41
+ 23 => {r: true, c: 'TOO_MANY_FILES', m: 'Too many files open', a: 'Too many files open'},
42
+ 24 => {r: false, c: 'FILE_TOO_BIG', m: 'File too big for file system', a: 'File too big for filesystem'},
43
+ 25 => {r: false, c: 'NO_SPACE_LEFT', m: 'No space left on disk', a: 'No space left on disk'},
44
+ 26 => {r: false, c: 'READ_ONLY_FS', m: 'Read only file system', a: 'Read only filesystem'},
45
+ 27 => {r: false, c: 'SOME_FILE_ERRS', m: 'Some individual files failed', a: 'One or more files failed'},
46
+ 28 => {r: false, c: 'USER_CANCEL', m: 'Cancelled by user', a: 'Cancelled by user'},
47
+ 29 => {r: false, c: 'LIC_NOLIC', m: 'License not found or unable to access', a: 'Unable to access license info'},
48
+ 30 => {r: false, c: 'LIC_EXPIRED', m: 'License expired', a: 'License expired'},
49
+ 31 => {r: false, c: 'SOCK_SETUP', m: 'Unable to setup socket (create, bind, etc ...)', a: 'Unable to set up socket'},
50
+ 32 => {r: true, c: 'OUT_OF_MEMORY', m: 'Out of memory, unable to allocate', a: 'Out of memory'},
51
+ 33 => {r: true, c: 'THREAD_SPAWN', m: 'Can\'t spawn thread', a: 'Unable to spawn thread'},
52
+ 34 => {r: false, c: 'UNAUTHORIZED', m: 'Unauthorized by external auth server', a: 'Unauthorized'},
53
+ 35 => {r: true, c: 'DISK_READ', m: 'Error reading source file from disk', a: 'Disk read error'},
54
+ 36 => {r: true, c: 'DISK_WRITE', m: 'Error writing to disk', a: 'Disk write error'},
55
+ 37 => {r: true, c: 'AUTHORIZATION', m: 'Used interchangeably with ERR_UNAUTHORIZED', a: 'Authorization failure'},
56
+ 38 => {r: false, c: 'LIC_ILLEGAL', m: 'Operation not permitted by license', a: 'Operation not permitted by license'},
57
+ 39 => {r: true, c: 'PEER_ABORTED_SESSION', m: 'Remote peer terminated session', a: 'Peer aborted session'},
58
+ 40 => {r: true, c: 'DATA_TRANSFER_TIMEOUT', m: 'Transfer stalled, timed out', a: 'Data transfer stalled, timed out'},
59
+ 41 => {r: false, c: 'BAD_PATH', m: 'Path violates docroot containment', a: 'File location is outside \'docroot\' hierarchy'},
60
+ 42 => {r: false, c: 'ALREADY_EXISTS', m: 'File or directory already exists', a: 'File or directory already exists'},
61
+ 43 => {r: false, c: 'STAT_FAILS', m: 'Cannot stat file', a: 'Cannot collect details about file or directory'},
62
+ 44 => {r: true, c: 'PMTU_BRTT_ERROR', m: 'UDP session initiation fatal error', a: 'UDP session initiation fatal error'},
63
+ 45 => {r: true, c: 'BWMEAS_ERROR', m: 'Bandwidth measurement fatal error', a: 'Bandwidth measurement fatal error'},
64
+ 46 => {r: false, c: 'VLINK_ERROR', m: 'Virtual link error', a: 'Virtual link error'},
65
+ 47 => {r: false, c: 'CONNECTION_ERROR_HTTP', m: 'Error establishing HTTP connection',
66
+ a: 'Error establishing HTTP connection (check HTTP port and firewall)'},
67
+ 48 => {r: false, c: 'FILE_ENCRYPTION_ERROR', m: 'File encryption error, e.g. corrupt file',
68
+ a: 'File encryption/decryption error, e.g. corrupt file'},
69
+ 49 => {r: false, c: 'FILE_DECRYPTION_PASS', m: 'File encryption/decryption error, e.g. corrupt file', a: 'File decryption error, bad passphrase'},
70
+ 50 => {r: false, c: 'BAD_CONFIGURATION', m: 'Aspera.conf contains invalid data and was rejected', a: 'Invalid configuration'},
71
+ 51 => {r: false, c: 'INSECURE_CONNECTION', m: 'Remote-host key check failure', a: 'Remote host is not who we expected'},
72
+ 52 => {r: false, c: 'START_VALIDATION_FAILED', m: 'File start validation failed', a: 'File start validation failed'},
73
+ 53 => {r: false, c: 'STOP_VALIDATION_FAILED', m: 'File stop validation failed', a: 'File stop validation failed'},
74
+ 54 => {r: false, c: 'THRESHOLD_VALIDATION_FAILED', m: 'File threshold validation failed', a: 'File threshold validation failed'},
75
+ 55 => {r: false, c: 'FILEPATH_TOO_LONG', m: 'File path/name too long for underlying file system', a: 'File path exceeds underlying file system limit'},
76
+ 56 => {r: false, c: 'ILLEGAL_CHARS_IN_PATH', m: 'Windows path contains illegal characters',
77
+ a: 'Path being written to Windows file system contains illegal characters'},
78
+ 57 => {r: false, c: 'CHUNK_MUST_MATCH_ALIGNMENT', m: 'Chunk size/start must be aligned with storage', a: 'Chunk size/start must be aligned with storage'},
79
+ 58 => {r: false, c: 'VALIDATION_SESSION_ABORT', m: 'Session aborted to due to validation error', a: 'Session aborted to due validation error'},
80
+ 59 => {r: false, c: 'REMOTE_STORAGE_ERROR', m: 'Remote storage errored', a: 'Remote storage errored'},
81
+ 60 => {r: false, c: 'LUA_SCRIPT_ABORTED_SESSION', m: 'Session aborted due to Lua script abort', a: 'Session aborted due to Lua script abort'},
82
+ 61 => {r: true, c: 'SSEAR_RETRYABLE', m: 'Transfer failed because of a retryable Encryption at Rest error',
83
+ a: 'Transfer failed because of a retryable Encryption at Rest error'},
84
+ 62 => {r: false, c: 'SSEAR_FATAL', m: 'Transfer failed because of a fatal Encryption at Rest error',
85
+ a: 'Transfer failed because of a fatal Encryption at Rest error'},
86
+ 63 => {r: false, c: 'LINK_LOOP', m: 'Path refers to a symbolic link loop', a: 'Path refers to a symbolic link loop'},
87
+ 64 => {r: false, c: 'CANNOT_RENAME_PARTIAL_FILES', m: 'Can\'t rename a partial file', a: 'Can\'t rename a partial file.'},
88
+ 65 => {r: false, c: 'CIPHER_NON_COMPAT_FIPS', m: 'Can\'t use this cipher with FIPS mode enabled', a: 'Can\'t use this cipher with FIPS mode enabled'},
89
+ 66 => {r: false, c: 'PEER_REQUIRES_FIPS', m: 'Peer rejects cipher due to FIPS mode enabled on peer',
90
+ a: 'Peer rejects cipher due to FIPS mode enabled on peer'}
91
+ }.freeze
92
+ # rubocop:enable Layout/FirstHashElementLineBreak
9
93
  # cspell: disable
10
94
  OPERATIONS = %w[
11
95
  NOP
@@ -187,7 +271,8 @@ module Aspera
187
271
  ExtraCreatePolicy
188
272
  ]
189
273
  # Management port start message
190
- MGT_HEADER = 'FASPMGR 2'
274
+ PROT_VERSION = '2'
275
+ MGT_HEADER = "FASPMGR #{PROT_VERSION}"
191
276
  # empty line is separator to end event information
192
277
  MGT_FRAME_SEPARATOR = ''
193
278
  # fields description for JSON generation
@@ -204,33 +289,44 @@ module Aspera
204
289
  # cspell: enable
205
290
 
206
291
  class << self
207
- # translates mgt port event into (enhanced) typed event
208
- def enhanced_event_format(event)
209
- return event.keys.each_with_object({}) do |e, h|
210
- new_name =
211
- case e
212
- when 'Elapsedusec' then 'elapsed_usec'
213
- when 'Bytescont' then 'bytes_cont'
214
- else e.capital_to_snake
215
- end
216
- h[new_name] =
217
- if INTEGER_FIELDS.include?(e) then event[e].to_i
218
- elsif BOOLEAN_FIELDS.include?(e) then event[e].eql?(BOOLEAN_TRUE)
219
- else
220
- event[e]
221
- end
222
- end
292
+ # translate native event name to snake case
293
+ def field_native_to_snake(name)
294
+ case name
295
+ when 'Elapsedusec' then 'elapsed_usec'
296
+ when 'Bytescont' then 'bytes_cont'
297
+ else name.capital_to_snake
298
+ end
223
299
  end
224
300
 
225
- # build command to send on management port
226
- # @param data [Hash] {'type'=>'START','source'=>_path_,'destination'=>_path_}
301
+ # translate snake case event name to native
302
+ # @param name [String] Field name
303
+ def field_snake_to_native(name)
304
+ field = name.delete('_')
305
+ result = PARAMETERS.find{ |w| w.casecmp?(field)}
306
+ raise "No such field: #{name}" if result.nil?
307
+ result
308
+ end
309
+
310
+ # Translates mgt port event into (enhanced) typed event
311
+ def event_native_to_snake(event)
312
+ event.each_with_object({}) do |(key, value), h|
313
+ h[field_native_to_snake(key)] =
314
+ if INTEGER_FIELDS.include?(key) then value.to_i
315
+ elsif BOOLEAN_FIELDS.include?(key) then value.eql?(BOOLEAN_TRUE)
316
+ else
317
+ value
318
+ end
319
+ end
320
+ end
321
+
322
+ # Build command to send on management port
323
+ # @param data [Hash] e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
324
+ # @return [String] frame to send on management port
227
325
  def command_to_stream(data)
228
- # TODO: translate enhanced to capitalized ?
229
326
  data
230
- .keys
231
- .map{ |k| "#{k.capitalize}: #{data[k]}"}
327
+ .map{ |key, value| "#{field_snake_to_native(key)}: #{value}"}
232
328
  .unshift(MGT_HEADER)
233
- .push('', '')
329
+ .push(MGT_FRAME_SEPARATOR, '')
234
330
  .join("\n")
235
331
  end
236
332
  end
data/lib/aspera/assert.rb CHANGED
@@ -5,6 +5,10 @@ module Aspera
5
5
  class Error < StandardError
6
6
  end
7
7
 
8
+ # Any problem with parameter values
9
+ class ParameterError < Error
10
+ end
11
+
8
12
  # Error that shall not happen, else it's a bug
9
13
  class InternalError < Error
10
14
  end
@@ -12,8 +16,10 @@ module Aspera
12
16
  # An expected condition was not met
13
17
  class AssertError < Error
14
18
  end
19
+
15
20
  class << self
16
- # Replace `raise`, allows sending exception, or just error log, when type is `:error`
21
+ # Replaces `raise` in assertion
22
+ # Allows sending exception, or just error log, when type is `:error`
17
23
  # @param type [Exception,Symbol] Send to log if symbol, else raise exception
18
24
  # @param message [String] Message for error.
19
25
  def report_error(type, message)
@@ -28,32 +34,54 @@ module Aspera
28
34
  # @param assertion [Bool] Must be true
29
35
  # @param info [String,nil] Fixed message in case assert fails, else use `block`
30
36
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
31
- # @param block [Proc] Produces a string that desribes the problem for complex messages
37
+ # @param block [Proc] Produces a string that describes the problem for complex messages
32
38
  # The block is executed in the context of the Aspera module
33
39
  def assert(assertion, info = nil, type: AssertError)
34
40
  raise InternalError, 'bad assert: both info and block given' unless info.nil? || !block_given?
35
41
  return if assertion
36
42
  message = 'assertion failed'
37
43
  info = yield if block_given?
38
- message = "#{message}: #{info}" if info
39
- message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
44
+ message = type.eql?(AssertError) ? "#{message}: #{info}" : info if info
45
+ # message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
40
46
  report_error(type, message)
41
47
  end
42
48
 
43
49
  # Assert that value has the given type
44
- # @param value [Object] The value to check
45
- # @param classes [Class, Array] The expected type(s)
50
+ # @param value [Object] The value to check
51
+ # @param classes [Class, Array] The expected type(s)
46
52
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
47
- # @param block [Proc] Additional description in front of message
53
+ # @param block [Proc] Additional description in front of message
48
54
  def assert_type(value, *classes, type: AssertError)
49
- assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have #{value.inspect}"}
55
+ assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have (#{value.class})#{value.inspect}"}
56
+ end
57
+
58
+ # Assert that all value of array are of the same specified type
59
+ # @param array [Array] The array to check
60
+ # @param klass [Class] The expected type of elements
61
+ # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
62
+ # @param block [Proc] Additional description in front of message
63
+ def assert_array_all(array, klass, type: AssertError)
64
+ assert_type(array, Array, type: type)
65
+ assert(array.all?(klass), type: type){"#{"#{yield}: " if block_given?}expecting all as #{klass}, but have #{array.map(&:class).uniq}"}
66
+ end
67
+
68
+ # Assert value is Hash, keys have type, and Values have type
69
+ # @param hash [Hash] The hash to check
70
+ # @param key_class [Class] The expected type of keys (or nil)
71
+ # @param value_class [Class] The expected type of values (or nil)
72
+ # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
73
+ # @param block [Proc] Additional description in front of message
74
+ def assert_hash_all(hash, key_class, value_class, type: AssertError)
75
+ assert_type(hash, Hash, type: type)
76
+ assert_array_all(hash.keys, key_class, type: AssertError){"#{"#{yield}: " if block_given?}keys"} unless key_class.nil?
77
+ assert_array_all(hash.values, value_class, type: AssertError){"#{"#{yield}: " if block_given?}values"} unless value_class.nil?
50
78
  end
51
79
 
52
80
  # Assert that value is one of the given values
53
- # @param value [any] value to check
54
- # @param values [Array] accepted values
81
+ # @param value [any] Value to check
82
+ # @param values [Array] Accepted values
55
83
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
56
- # @param block [Proc] Additional description in front of message
84
+ # @param block [Proc] Additional description in front of message
57
85
  def assert_values(value, values, type: AssertError)
58
86
  assert(values.include?(value), type: type) do
59
87
  val_list = values.inspect
@@ -6,11 +6,13 @@ module Aspera
6
6
  class Error < StandardError; end
7
7
  # Raised when an unexpected argument is provided.
8
8
  class BadArgument < Error; end
9
+ class MissingArgument < Error; end
9
10
  class NoSuchElement < Error; end
10
11
 
11
12
  class BadIdentifier < Error
12
- def initialize(res_type, res_id)
13
- super("#{res_type} with identifier #{res_id} not found")
13
+ def initialize(res_type, res_id, field: 'identifier', count: 0)
14
+ msg = count.eql?(0) ? 'not found' : "found #{count}"
15
+ super("#{res_type} with #{field}=#{res_id}: #{msg}")
14
16
  end
15
17
  end
16
18
  end