aspera-cli 4.22.0 → 4.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +405 -364
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +1856 -961
  6. data/bin/ascli +2 -1
  7. data/bin/asession +4 -4
  8. data/lib/aspera/agent/base.rb +4 -0
  9. data/lib/aspera/agent/connect.rb +20 -18
  10. data/lib/aspera/agent/desktop.rb +14 -11
  11. data/lib/aspera/agent/direct.rb +39 -31
  12. data/lib/aspera/agent/httpgw.rb +2 -2
  13. data/lib/aspera/agent/node.rb +9 -11
  14. data/lib/aspera/agent/transferd.rb +18 -11
  15. data/lib/aspera/api/aoc.rb +53 -43
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +23 -22
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +35 -21
  20. data/lib/aspera/ascp/installation.rb +43 -43
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +55 -24
  23. data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
  24. data/lib/aspera/cli/error.rb +1 -1
  25. data/lib/aspera/cli/extended_value.rb +28 -29
  26. data/lib/aspera/cli/formatter.rb +191 -168
  27. data/lib/aspera/cli/hints.rb +38 -4
  28. data/lib/aspera/cli/main.rb +139 -108
  29. data/lib/aspera/cli/manager.rb +51 -31
  30. data/lib/aspera/cli/plugin.rb +149 -78
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +217 -88
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +105 -227
  35. data/lib/aspera/cli/plugins/console.rb +49 -18
  36. data/lib/aspera/cli/plugins/cos.rb +4 -4
  37. data/lib/aspera/cli/plugins/faspex.rb +45 -51
  38. data/lib/aspera/cli/plugins/faspex5.rb +162 -163
  39. data/lib/aspera/cli/plugins/faspio.rb +6 -5
  40. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  41. data/lib/aspera/cli/plugins/node.rb +233 -247
  42. data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
  43. data/lib/aspera/cli/plugins/preview.rb +26 -29
  44. data/lib/aspera/cli/plugins/server.rb +29 -28
  45. data/lib/aspera/cli/plugins/shares.rb +40 -28
  46. data/lib/aspera/cli/sync_actions.rb +101 -80
  47. data/lib/aspera/cli/transfer_agent.rb +55 -58
  48. data/lib/aspera/cli/transfer_progress.rb +29 -20
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +160 -0
  51. data/lib/aspera/colors.rb +13 -8
  52. data/lib/aspera/command_line_builder.rb +28 -22
  53. data/lib/aspera/command_line_converter.rb +31 -0
  54. data/lib/aspera/data_repository.rb +1 -0
  55. data/lib/aspera/environment.rb +144 -100
  56. data/lib/aspera/faspex_gw.rb +1 -1
  57. data/lib/aspera/faspex_postproc.rb +3 -2
  58. data/lib/aspera/hash_ext.rb +1 -1
  59. data/lib/aspera/id_generator.rb +10 -10
  60. data/lib/aspera/keychain/base.rb +18 -0
  61. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  62. data/lib/aspera/keychain/factory.rb +9 -3
  63. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  64. data/lib/aspera/keychain/macos_security.rb +13 -13
  65. data/lib/aspera/log.rb +70 -20
  66. data/lib/aspera/nagios.rb +5 -6
  67. data/lib/aspera/node_simulator.rb +12 -7
  68. data/lib/aspera/oauth/base.rb +6 -2
  69. data/lib/aspera/oauth/factory.rb +25 -18
  70. data/lib/aspera/oauth/jwt.rb +13 -1
  71. data/lib/aspera/oauth/url_json.rb +3 -3
  72. data/lib/aspera/oauth/web.rb +5 -3
  73. data/lib/aspera/persistency_folder.rb +2 -2
  74. data/lib/aspera/preview/file_types.rb +43 -35
  75. data/lib/aspera/preview/generator.rb +26 -13
  76. data/lib/aspera/preview/terminal.rb +10 -7
  77. data/lib/aspera/preview/utils.rb +11 -9
  78. data/lib/aspera/products/connect.rb +2 -1
  79. data/lib/aspera/products/desktop.rb +1 -1
  80. data/lib/aspera/products/other.rb +2 -2
  81. data/lib/aspera/products/transferd.rb +8 -6
  82. data/lib/aspera/proxy_auto_config.rb +1 -1
  83. data/lib/aspera/rest.rb +46 -28
  84. data/lib/aspera/rest_call_error.rb +1 -1
  85. data/lib/aspera/rest_error_analyzer.rb +1 -0
  86. data/lib/aspera/resumer.rb +1 -1
  87. data/lib/aspera/secret_hider.rb +46 -40
  88. data/lib/aspera/ssh.rb +14 -4
  89. data/lib/aspera/sync/args.schema.yaml +102 -0
  90. data/lib/aspera/sync/conf.schema.yaml +701 -0
  91. data/lib/aspera/sync/database.rb +83 -0
  92. data/lib/aspera/{transfer/sync.rb → sync/operations.rb} +145 -68
  93. data/lib/aspera/temp_file_manager.rb +4 -2
  94. data/lib/aspera/timer_limiter.rb +7 -5
  95. data/lib/aspera/transfer/error.rb +1 -1
  96. data/lib/aspera/transfer/error_info.rb +1 -2
  97. data/lib/aspera/transfer/faux_file.rb +11 -10
  98. data/lib/aspera/transfer/parameters.rb +6 -5
  99. data/lib/aspera/transfer/spec.rb +15 -1
  100. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  101. data/lib/aspera/transfer/spec_doc.rb +34 -16
  102. data/lib/aspera/transfer/uri.rb +5 -5
  103. data/lib/aspera/uri_reader.rb +14 -10
  104. data/lib/aspera/web_auth.rb +2 -2
  105. data/lib/aspera/web_server_simple.rb +2 -2
  106. data.tar.gz.sig +0 -0
  107. metadata +15 -15
  108. metadata.gz.sig +0 -0
  109. data/examples/dascli +0 -30
  110. data/examples/get_proto_file.rb +0 -8
  111. data/examples/proxy.pac +0 -60
  112. data/lib/aspera/transfer/convert.rb +0 -29
  113. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -13
  114. data/lib/aspera/transfer/sync_session.schema.yaml +0 -79
@@ -9,6 +9,9 @@ require 'aspera/assert'
9
9
  require 'aspera/environment'
10
10
  require 'zlib'
11
11
  require 'base64'
12
+ require 'openssl'
13
+ require 'pathname'
14
+ require 'net/ssh/buffer'
12
15
 
13
16
  module Aspera
14
17
  module Api
@@ -35,6 +38,7 @@ module Aspera
35
38
  HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
36
39
  SCOPE_USER = 'user:all'
37
40
  SCOPE_ADMIN = 'admin:all'
41
+ # / in cloud
38
42
  PATH_SEPARATOR = '/'
39
43
 
40
44
  # register node special token decoder
@@ -49,6 +53,35 @@ module Aspera
49
53
  attr_accessor :use_standard_ports
50
54
  # set to false to bypass cache in redis
51
55
  attr_accessor :use_node_cache
56
+ attr_reader :use_dynamic_key
57
+
58
+ # set private key to be used
59
+ # @param pem_content [String] PEM encoded private key
60
+ def use_dynamic_key=(pem_content)
61
+ Aspera.assert_type(pem_content, String)
62
+ @dynamic_key = OpenSSL::PKey.read(pem_content)
63
+ end
64
+
65
+ # Adds fields `public_keys` in provided Hash, if dynamic key is set.
66
+ # @param h [Hash] Hash to add public key to
67
+ def add_public_key(h)
68
+ if @dynamic_key
69
+ ssh_key = Net::SSH::Buffer.from(:key, @dynamic_key)
70
+ # get pub key in OpenSSH public key format (authorized_keys)
71
+ h['public_keys'] = [
72
+ ssh_key.read_string,
73
+ Base64.strict_encode64(ssh_key.to_s)
74
+ ].join(' ')
75
+ end
76
+ return h
77
+ end
78
+
79
+ # Adds fields `ssh_private_key` in provided Hash, if dynamic key is set.
80
+ # @param h [Hash] Hash to add private key to
81
+ def add_private_key(h)
82
+ h['ssh_private_key'] = @dynamic_key.to_pem if @dynamic_key
83
+ return h
84
+ end
52
85
 
53
86
  # For access keys: provide expression to match entry in folder
54
87
  # @param match_expression one of supported types
@@ -60,7 +93,7 @@ module Aspera
60
93
  when String
61
94
  return ->(f){File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
62
95
  when NilClass then return ->(_){true}
63
- else Aspera.error_unexpected_value(match_expression.class.name, exception_class: Cli::BadArgument)
96
+ else Aspera.error_unexpected_value(match_expression.class.name, type: Cli::BadArgument)
64
97
  end
65
98
  end
66
99
 
@@ -68,6 +101,13 @@ module Aspera
68
101
  return file_matcher(options.get_next_argument('filter', validation: MATCH_TYPES, mandatory: false))
69
102
  end
70
103
 
104
+ # @return [Array] containing folder + inside folder/file
105
+ def split_folder(path)
106
+ folder = path.split(PATH_SEPARATOR)
107
+ inside = folder.pop
108
+ [folder.join(PATH_SEPARATOR), inside]
109
+ end
110
+
71
111
  # node API scopes
72
112
  def token_scope(access_key, scope)
73
113
  return [SCOPE_NODE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
@@ -115,7 +155,7 @@ module Aspera
115
155
  def bearer_headers(bearer_auth, access_key: nil)
116
156
  # if username is not provided, use the access key from the token
117
157
  if access_key.nil?
118
- access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.bearer_extract(bearer_auth))['scope'])[:access_key]
158
+ access_key = Node.decode_scope(Node.decode_bearer_token(OAuth::Factory.bearer_token(bearer_auth))['scope'])[:access_key]
119
159
  Aspera.assert(!access_key.nil?)
120
160
  end
121
161
  return {
@@ -135,6 +175,7 @@ module Aspera
135
175
  def initialize(app_info: nil, add_tspec: nil, **rest_args)
136
176
  # init Rest
137
177
  super(**rest_args)
178
+ @dynamic_key = nil
138
179
  @app_info = app_info
139
180
  # this is added to transfer spec, for instance to add tags (COS)
140
181
  @add_tspec = add_tspec
@@ -150,14 +191,15 @@ module Aspera
150
191
  end
151
192
 
152
193
  # Call node API, possibly adding cache control header, as globally specified
153
- def read_with_cache(subpath, query=nil)
194
+ def read_with_cache(subpath, query = nil)
154
195
  headers = {'Accept' => Rest::MIME_JSON}
155
196
  headers[HEADER_X_CACHE_CONTROL] = 'no-cache' unless self.class.use_node_cache
156
197
  return call(
157
198
  operation: 'GET',
158
199
  subpath: subpath,
159
200
  headers: headers,
160
- query: query)[:data]
201
+ query: query
202
+ )[:data]
161
203
  end
162
204
 
163
205
  # update transfer spec with special additional tags
@@ -173,10 +215,11 @@ module Aspera
173
215
  return @app_info[:api].node_api_from(
174
216
  node_id: node_id,
175
217
  workspace_id: @app_info[:workspace_id],
176
- workspace_name: @app_info[:workspace_name])
218
+ workspace_name: @app_info[:workspace_name]
219
+ )
177
220
  end
178
221
  Log.log.warn{"Cannot resolve link with node id #{node_id}, no resolver"}
179
- return nil
222
+ return
180
223
  end
181
224
 
182
225
  # Check if a link entry in folder has target information
@@ -201,12 +244,12 @@ module Aspera
201
244
  # @param state [Object] state object sent to processing method
202
245
  # @param top_file_id [String] file id to start at (default = access key root file id)
203
246
  # @param top_file_path [String] path of top folder (default = /)
204
- def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/', query: nil)
247
+ def process_folder_tree(method_sym:, state:, top_file_id:, top_file_path: '/')
205
248
  Aspera.assert(!top_file_path.nil?){'top_file_path not set'}
206
249
  Log.log.debug{"process_folder_tree: node=#{@app_info ? @app_info[:node_info]['id'] : 'nil'}, file id=#{top_file_id}, path=#{top_file_path}"}
207
250
  # start at top folder
208
251
  folders_to_explore = [{id: top_file_id, path: top_file_path}]
209
- Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
252
+ Log.dump(:folders_to_explore, folders_to_explore)
210
253
  until folders_to_explore.empty?
211
254
  # consume first in job list
212
255
  current_item = folders_to_explore.shift
@@ -220,12 +263,10 @@ module Aspera
220
263
  Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
221
264
  []
222
265
  end
223
- Log.log.debug{Log.dump(:folder_contents, folder_contents)}
266
+ Log.dump(:folder_contents, folder_contents)
224
267
  folder_contents.each do |entry|
225
268
  if entry.key?('error')
226
- if entry['error'].is_a?(Hash) && entry['error'].key?('user_message')
227
- Log.log.error(entry['error']['user_message'])
228
- end
269
+ Log.log.error(entry['error']['user_message']) if entry['error'].is_a?(Hash) && entry['error'].key?('user_message')
229
270
  next
230
271
  end
231
272
  current_path = File.join(current_item[:path], entry['name'])
@@ -242,7 +283,8 @@ module Aspera
242
283
  method_sym: method_sym,
243
284
  state: state,
244
285
  top_file_id: entry['target_id'],
245
- top_file_path: current_path)
286
+ top_file_path: current_path
287
+ )
246
288
  end
247
289
  end
248
290
  end
@@ -255,7 +297,7 @@ module Aspera
255
297
  # @param path [String] file or folder path (end with "/" is like setting process_last_link)
256
298
  # @param process_last_link [Boolean] if true, follow the last link
257
299
  # @return [Hash] {.api,.file_id}
258
- def resolve_api_fid(top_file_id, path, process_last_link=false)
300
+ def resolve_api_fid(top_file_id, path, process_last_link = false)
259
301
  Aspera.assert_type(top_file_id, String)
260
302
  Aspera.assert_type(path, String)
261
303
  process_last_link ||= path.end_with?(PATH_SEPARATOR)
@@ -268,6 +310,50 @@ module Aspera
268
310
  return resolve_state[:result]
269
311
  end
270
312
 
313
+ # Given a list of paths, finds a common root and list of sub-paths
314
+ # @param top_file_id [String] Root file id
315
+ # @param paths [Array(Hash)] List of paths
316
+ # @return [Array] size=2: apfid, paths (Array(Hash))
317
+ def resolve_api_fid_paths(top_file_id, paths)
318
+ Aspera.assert_type(paths, Array)
319
+ Aspera.assert(paths.size.positive?)
320
+ split_sources = paths.map{ |p| Pathname(p['source']).each_filename.to_a}
321
+ root = []
322
+ split_sources.map(&:size).min.times do |i|
323
+ parts = split_sources.map{ |s| s[i]}
324
+ break unless parts.uniq.size == 1
325
+ root << parts.first
326
+ end
327
+ source_folder = File.join(root)
328
+ source_paths = paths.each_with_index.map do |p, i|
329
+ m = {'source' => File.join(split_sources[i][root.size..])}
330
+ m['destination'] = p['destination'] if p.key?('destination')
331
+ m
332
+ end
333
+ apifid = resolve_api_fid(top_file_id, source_folder, true)
334
+ # If a single item
335
+ if source_paths.size.eql?(1)
336
+ # Get precise info in this element
337
+ file_info = apifid[:api].read("files/#{apifid[:file_id]}")
338
+ source_paths =
339
+ case file_info['type']
340
+ when 'file'
341
+ # if the single source is a file, we need to split into folder path and filename
342
+ src_dir_elements = source_folder.split(Api::Node::PATH_SEPARATOR)
343
+ filename = src_dir_elements.pop
344
+ apifid = resolve_api_fid(top_file_id, src_dir_elements.join(Api::Node::PATH_SEPARATOR), true)
345
+ # filename is the last one, source folder is what remains
346
+ [{'source' => filename}]
347
+ when 'link', 'folder'
348
+ # single source is 'folder' or 'link'
349
+ # TODO: add this ? , 'destination'=>file_info['name']
350
+ [{'source' => '.'}]
351
+ else Aspera.error_unexpected_value(file_info['type']){'source type'}
352
+ end
353
+ end
354
+ [apifid, source_paths]
355
+ end
356
+
271
357
  def find_files(top_file_id, test_lambda)
272
358
  Log.log.debug{"find_files: file id=#{top_file_id}"}
273
359
  find_state = {found: [], test_lambda: test_lambda}
@@ -305,7 +391,7 @@ module Aspera
305
391
  # @param file_id destination or source folder (id)
306
392
  # @param direction one of Transfer::Spec::DIRECTION_SEND, Transfer::Spec::DIRECTION_RECEIVE
307
393
  # @param ts_merge additional transfer spec to merge
308
- def transfer_spec_gen4(file_id, direction, ts_merge=nil)
394
+ def transfer_spec_gen4(file_id, direction, ts_merge = nil)
309
395
  ak_name = nil
310
396
  ak_token = nil
311
397
  case auth_params[:type]
@@ -347,9 +433,7 @@ module Aspera
347
433
  # by default: same address as node API
348
434
  transfer_spec['remote_host'] = URI.parse(base_url).host
349
435
  # AoC allows specification of other url
350
- if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
351
- transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
352
- end
436
+ transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url'] if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
353
437
  info = read('info')
354
438
  # get the transfer user from info on access key
355
439
  transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
@@ -395,10 +479,8 @@ module Aspera
395
479
  if state[:process_last_link]
396
480
  # we found it
397
481
  other_node = nil
398
- if entry_has_link_information(entry)
399
- other_node = node_id_to_node(entry['target_node_id'])
400
- end
401
- raise 'Cannot resolve link' if other_node.nil?
482
+ other_node = node_id_to_node(entry['target_node_id']) if entry_has_link_information(entry)
483
+ raise Error, 'Cannot resolve link' if other_node.nil?
402
484
  state[:result] = {api: other_node, file_id: entry['target_id']}
403
485
  else
404
486
  # we found it but we do not process the link
data/lib/aspera/ascmd.rb CHANGED
@@ -10,7 +10,7 @@ module Aspera
10
10
  # execute: "ascmd -h" to get syntax
11
11
  # Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
12
12
  class AsCmd
13
- # number of arguments for each operation
13
+ # number of arguments for each operation (to allow splitting into batches)
14
14
  OPS_ARGS = {
15
15
  cp: 2,
16
16
  df: 0,
@@ -23,17 +23,23 @@ module Aspera
23
23
  rm: 1
24
24
  }.freeze
25
25
 
26
- # protocol is based on Type-Length-Value
27
- # type start at one, but array index start at zero
26
+ # Protocol is based on Type-Length-Value
27
+ # Type start at one, but array index start at zero
28
28
  ENUM_START = 1
29
29
 
30
- # description of result structures (see ascmdtypes.h).
30
+ # Description of result structures (see ascmdtypes.h).
31
31
  # Base types are big endian
32
32
  # key = name of type
33
- # index in array `fields` is the type (minus ENUM_START)
33
+ # index in array `fields` is: the numerical type of TLV - `ENUM_START`. i.e. add `ENUM_START` to index, to get `T`
34
34
  # decoding always start at `result`
35
35
  # some fields have special handling indicated by `special`
36
36
  # field_list, list_tlv_list, list_tlv_restart are composed with a list of TLV
37
+ # decode:
38
+ # - :base : value
39
+ # - :buffer_list : an array of {btype,buffer}
40
+ # - :field_list : a hash, or array
41
+ # :check: ???
42
+ # rubocop:disable Layout/FirstHashElementLineBreak
37
43
  TYPES_DESCR = {
38
44
  result: {decode: :field_list,
39
45
  fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :list_tlv_list}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
@@ -67,6 +73,7 @@ module Aspera
67
73
  zstr: {decode: :base, unpack: 'Z*'},
68
74
  blist: {decode: :buffer_list}
69
75
  }.freeze
76
+ # rubocop:enable Layout/FirstHashElementLineBreak
70
77
 
71
78
  private_constant :TYPES_DESCR, :ENUM_START, :OPS_ARGS
72
79
 
@@ -79,17 +86,25 @@ module Aspera
79
86
  end
80
87
 
81
88
  # execute an "as" command on a remote server
89
+ # Version 2 allows use of reverse proxy with multiple addresses.
82
90
  # @param [Symbol] one of OPERATIONS
83
91
  # @param [Array] parameters for "as" command
84
92
  # @return result of command, type depends on command (bool, array, hash)
85
- def execute_single(action_sym, arguments)
93
+ def execute_single(action_sym, arguments, version: 1, host: nil)
86
94
  arguments = [] if arguments.nil?
87
95
  Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
88
96
  Aspera.assert_type(action_sym, Symbol)
89
97
  Aspera.assert_type(arguments, Array)
90
98
  Aspera.assert(arguments.all?(String), 'arguments must be strings')
99
+ remote_cmd = 'ascmd'
91
100
  # lines of commands (String's)
92
101
  command_lines = []
102
+ if version.eql?(2)
103
+ cmd = "as_session_init --protocol=#{version}"
104
+ cmd += " --host=#{host}" if host
105
+ command_lines.push(cmd)
106
+ remote_cmd += ' -V2'
107
+ end
93
108
  # add "as_" command
94
109
  main_command = "as_#{action_sym}"
95
110
  arg_batches =
@@ -114,11 +129,11 @@ module Aspera
114
129
  stdin_input = command_lines.join("\n")
115
130
  Log.log.trace1{"execute_single:#{stdin_input}"}
116
131
  # execute, get binary output
117
- byte_buffer = @command_executor.execute('ascmd', stdin_input).unpack('C*')
118
- raise 'ERROR: empty answer from server' if byte_buffer.empty?
132
+ byte_buffer = @command_executor.execute(remote_cmd, input: stdin_input).unpack('C*')
133
+ Aspera.assert(!byte_buffer.empty?){'empty answer from server'}
119
134
  # get hash or table result
120
135
  result = self.class.parse(byte_buffer, :result)
121
- raise 'ERROR: unparsed bytes remaining' unless byte_buffer.empty?
136
+ Aspera.assert(byte_buffer.empty?){'unparsed bytes remaining'}
122
137
  # get and delete info,always present in results
123
138
  system_info = result[:info]
124
139
  result.delete(:info)
@@ -129,7 +144,7 @@ module Aspera
129
144
  result[:dir].each do |file|
130
145
  if file.key?(:smode)
131
146
  # Converts the first character of the file mode (see 'man ls') into a type.
132
- file[:type] = case file[:smode][0, 1]; when 'd' then:directory; when '-' then:file; when 'l' then:link; else; :other; end # rubocop:disable Style/Semicolon
147
+ file[:type] = case file[:smode][0, 1]; when 'd' then:directory; when '-' then:file; when 'l' then:link; else; :other; end
133
148
  end
134
149
  end
135
150
  end
@@ -145,27 +160,26 @@ module Aspera
145
160
  return result
146
161
  end
147
162
 
148
- # This exception is raised when +ascmd+ returns an error.
163
+ # This exception is raised when `ascmd` returns an error.
149
164
  class Error < StandardError
150
- def initialize(errno, errstr, cmd, arguments)
151
- super(); @errno = errno; @errstr = errstr; @command = cmd; @arguments = arguments; end # rubocop:disable Style/Semicolon
152
-
165
+ # rubocop:disable Style/Semicolon
166
+ def initialize(errno, errstr, cmd, arguments); super(); @errno = errno; @errstr = errstr; @command = cmd; @arguments = arguments; end
153
167
  def message; "ascmd: #{@errstr} (#{@errno})"; end
154
168
  def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
169
+ # rubocop:enable Style/Semicolon
155
170
  end
156
171
 
157
172
  class << self
158
- # get description of structure's field, @param struct_name, @param typed_buffer provides field name
173
+ # Get description of structure's field, @param struct_name, @param typed_buffer provides field name
159
174
  def field_description(struct_name, typed_buffer)
160
175
  result = TYPES_DESCR[struct_name][:fields][typed_buffer[:btype] - ENUM_START]
161
176
  raise "Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}" if result.nil?
162
177
  return result
163
178
  end
164
179
 
165
- # decodes the provided buffer as provided type name
166
- # @return a decoded type.
167
- # :base : value, :buffer_list : an array of {btype,buffer}, :field_list : a hash, or array
168
- def parse(buffer, type_name, indent_level=nil)
180
+ # Decodes the provided buffer as provided type name
181
+ # @return a decoded type: single, Array, or Hash
182
+ def parse(buffer, type_name, indent_level = nil)
169
183
  indent_level = (indent_level || -1) + 1
170
184
  type_descr = TYPES_DESCR[type_name]
171
185
  raise "Unexpected type #{type_name}" if type_descr.nil?
@@ -174,7 +188,7 @@ module Aspera
174
188
  case type_descr[:decode]
175
189
  when :base
176
190
  num_bytes = type_name.eql?(:zstr) ? buffer.length : type_descr[:size]
177
- raise 'ERROR:not enough bytes' if buffer.length < num_bytes
191
+ Aspera.assert(buffer.length >= num_bytes){'not enough bytes'}
178
192
  byte_array = buffer.shift(num_bytes)
179
193
  byte_array = [byte_array] unless byte_array.is_a?(Array)
180
194
  result = byte_array.pack('C*').unpack1(type_descr[:unpack])
@@ -187,7 +201,7 @@ module Aspera
187
201
  until buffer.empty?
188
202
  btype = parse(buffer, :int8, indent_level)
189
203
  length = parse(buffer, :int32, indent_level)
190
- raise 'ERROR:not enough bytes' if buffer.length < length
204
+ Aspera.assert(buffer.length >= length){'not enough bytes'}
191
205
  value = buffer.shift(length)
192
206
  result.push({btype: btype, buffer: value})
193
207
  Log.log.trace1{"#{' .' * indent_level}:buffer_list[#{result.length - 1}] #{result.last}"}
@@ -32,6 +32,7 @@ module Aspera
32
32
  # Installation.instance.ascp_path=""
33
33
  class Installation
34
34
  include Singleton
35
+
35
36
  DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
36
37
  <?xml version='1.0' encoding='UTF-8'?>
37
38
  <CONF version="2">
@@ -43,12 +44,12 @@ module Aspera
43
44
  </default>
44
45
  </CONF>
45
46
  END_OF_CONFIG_FILE
46
- # all ascp files (in SDK)
47
- EXE_FILES = %i[ascp ascp4 async].freeze
48
- FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
49
- TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
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'
50
51
  # filename for ascp with optional extension (Windows)
51
- private_constant :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL
52
+ private_constant :DEFAULT_ASPERA_CONF, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
52
53
  # options for SSH client private key
53
54
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
54
55
 
@@ -98,7 +99,7 @@ module Aspera
98
99
 
99
100
  # @return [Hash] with key = file name (String), and value = path to file
100
101
  def file_paths
101
- return FILES.each_with_object({}) do |v, m|
102
+ return SDK_FILES.each_with_object({}) do |v, m|
102
103
  m[v.to_s] =
103
104
  begin
104
105
  path(v)
@@ -114,19 +115,23 @@ module Aspera
114
115
  return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
115
116
  end
116
117
 
117
- # get path of one resource file of currently activated product
118
+ # Get path of one resource file of currently activated product
118
119
  # keys and certs are generated locally... (they are well known values, arch. independent)
120
+ # @param k [Symbol] key of the resource file
121
+ # @return [String, nil] Full path to the resource file or nil if not found
119
122
  def path(k)
120
- file_is_optional = false
123
+ file_is_required = true
121
124
  case k
122
125
  when *EXE_FILES
123
- file_is_optional = k.eql?(:async)
124
- use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
125
- # NOTE: that there might be a .exe at the end
126
- file = @path_to_ascp.gsub('ascp', k.to_s)
127
- when :transferd
128
- file_is_optional = true
129
- file = Products::Transferd.transferd_path
126
+ file_is_required = k.eql?(:ascp)
127
+ file = if k.eql?(:transferd)
128
+ Products::Transferd.transferd_path
129
+ else
130
+ # ensure at least ascp is found
131
+ use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
132
+ # NOTE: that there might be a .exe at the end
133
+ @path_to_ascp.gsub('ascp', k.to_s)
134
+ end
130
135
  when :ssh_private_dsa, :ssh_private_rsa
131
136
  # assume last 3 letters are type
132
137
  type = k.to_s[-3..-1].to_sym
@@ -149,8 +154,8 @@ module Aspera
149
154
  file = k.eql?(:fallback_certificate) ? file_cert : file_key
150
155
  else Aspera.error_unexpected_value(k)
151
156
  end
152
- return nil if file_is_optional && !File.exist?(file)
153
- Aspera.assert(File.exist?(file), exception_class: Errno::ENOENT){"#{k} not found (#{file})"}
157
+ return unless file_is_required || File.exist?(file)
158
+ Aspera.assert(File.exist?(file), type: Errno::ENOENT){"#{k} not found (#{file})"}
154
159
  return file
155
160
  end
156
161
 
@@ -167,7 +172,7 @@ module Aspera
167
172
  when :dsa_rsa, :rsa
168
173
  types.to_s.split('_').map{ |i| Installation.instance.path("ssh_private_#{i}".to_sym)}
169
174
  when :per_client
170
- raise 'Not yet implemented'
175
+ Aspera.error_not_implemented
171
176
  end
172
177
  end
173
178
 
@@ -178,8 +183,9 @@ module Aspera
178
183
 
179
184
  # Check that specified path is ascp and get version
180
185
  def get_exe_version(exe_path, vers_arg)
181
- raise 'ERROR: nil arg' if exe_path.nil?
182
- return nil unless File.exist?(exe_path)
186
+ Aspera.assert_type(exe_path, String)
187
+ Aspera.assert_type(vers_arg, String)
188
+ return unless File.exist?(exe_path)
183
189
  exe_version = nil
184
190
  cmd_out = %x("#{exe_path}" #{vers_arg})
185
191
  raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
@@ -196,6 +202,8 @@ module Aspera
196
202
  last_line = ''
197
203
  while (line = stderr.gets)
198
204
  line.chomp!
205
+ # skip lines that may have accents
206
+ next unless line.valid_encoding?
199
207
  last_line = line
200
208
  case line
201
209
  when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
@@ -211,12 +219,10 @@ module Aspera
211
219
  data['product_name'] = Regexp.last_match(1)
212
220
  data['product_version'] = Regexp.last_match(2)
213
221
  when /^LOG Initializing FASP version ([^,]+),/
214
- data['sdk_ascp_version'] = Regexp.last_match(1)
222
+ data['ascp_version'] = Regexp.last_match(1)
215
223
  end
216
224
  end
217
- if !thread.value.exitstatus.eql?(1) && !data.key?('root')
218
- raise last_line
219
- end
225
+ raise last_line if !thread.value.exitstatus.eql?(1) && !data.key?('root')
220
226
  end
221
227
  return data
222
228
  end
@@ -226,9 +232,9 @@ module Aspera
226
232
  data = {}
227
233
  File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
228
234
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
229
- data['openssldir'] = m[1]
235
+ data['ascp_openssl_dir'] = m[1]
230
236
  elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
231
- data['openssl_version'] = m[1]
237
+ data['ascp_openssl_version'] = m[1]
232
238
  end
233
239
  end if File.file?(ascp_path)
234
240
  return data
@@ -245,7 +251,7 @@ module Aspera
245
251
  # @return the url for download of SDK archive for the given platform and version
246
252
  def sdk_url_for_platform(platform: nil, version: nil)
247
253
  locations = sdk_locations
248
- platform = Environment.architecture if platform.nil?
254
+ platform = Environment.instance.architecture if platform.nil?
249
255
  locations = locations.select{ |l| l['platform'].eql?(platform)}
250
256
  raise "No SDK for platform: #{platform}" if locations.empty?
251
257
  version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
@@ -256,7 +262,7 @@ module Aspera
256
262
 
257
263
  # @param &block called with entry information
258
264
  def extract_archive_files(sdk_archive_path)
259
- raise 'missing block' unless block_given?
265
+ Aspera.assert(block_given?){'missing block'}
260
266
  case sdk_archive_path
261
267
  # Windows and Mac use zip
262
268
  when /\.zip$/
@@ -327,21 +333,15 @@ module Aspera
327
333
  end
328
334
  end
329
335
  return unless with_exe
330
- # ensure license file are generated so that ascp invocation for version works
331
- path(:aspera_license)
332
- path(:aspera_conf)
333
- sdk_ascp_file = Environment.exe_file('ascp')
334
- sdk_ascp_path = File.join(folder, sdk_ascp_file)
335
- raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
336
- EXE_FILES.each do |exe_sym|
337
- exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
338
- Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
336
+ # ensure necessary files are there, or generate them
337
+ SDK_FILES.each do |file_id_sym|
338
+ file_path = path(file_id_sym)
339
+ if file_path && EXE_FILES.include?(file_id_sym)
340
+ Environment.restrict_file_access(file_path, mode: 0o755) if File.exist?(file_path)
341
+ end
339
342
  end
340
- sdk_ascp_version = get_ascp_version(sdk_ascp_path)
341
- transferd_exe_path = Products::Transferd.transferd_path
342
- Log.log.warn{"No #{transferd_exe_path} in SDK archive"} unless File.exist?(transferd_exe_path)
343
- Environment.restrict_file_access(transferd_exe_path, mode: 0o755) if File.exist?(transferd_exe_path)
344
- transferd_version = get_exe_version(transferd_exe_path, 'version')
343
+ sdk_ascp_version = get_ascp_version(path(:ascp))
344
+ transferd_version = get_exe_version(path(:transferd), 'version')
345
345
  sdk_name = 'IBM Aspera Transfer SDK'
346
346
  sdk_version = transferd_version || sdk_ascp_version
347
347
  File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
@@ -359,7 +359,7 @@ module Aspera
359
359
  @path_to_ascp = nil
360
360
  @sdk_dir = nil
361
361
  @found_products = nil
362
- @transferd_urls = TRANSFER_SDK_LOCATION_URL
362
+ @transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
363
363
  end
364
364
 
365
365
  public
@@ -184,7 +184,8 @@ module Aspera
184
184
  ChunkSize
185
185
  PostTransferValidation
186
186
  OverwritePolicyCap
187
- ExtraCreatePolicy]
187
+ ExtraCreatePolicy
188
+ ]
188
189
  # Management port start message
189
190
  MGT_HEADER = 'FASPMGR 2'
190
191
  # empty line is separator to end event information
@@ -252,17 +253,17 @@ module Aspera
252
253
  # begin event
253
254
  @event_build = {}
254
255
  when /^([^:]+): (.*)$/
255
- raise 'mgt port: unexpected line: data without header' if @event_build.nil?
256
+ Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: data without header'}
256
257
  # event field
257
258
  @event_build[Regexp.last_match(1)] = Regexp.last_match(2)
258
259
  when MGT_FRAME_SEPARATOR
259
- raise 'mgt port: unexpected line: end frame without header' if @event_build.nil?
260
+ Aspera.assert_type(@event_build, Hash){'mgt port: unexpected line: end frame without header'}
260
261
  @last_event = @event_build
261
262
  @event_build = nil
262
263
  return @last_event
263
264
  else Aspera.error_unexpected_value(line){'mgt port'}
264
265
  end
265
- return nil
266
+ return
266
267
  end
267
268
  end
268
269
  end