aspera-cli 4.19.0 → 4.20.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +20 -0
  4. data/CONTRIBUTING.md +16 -4
  5. data/README.md +344 -164
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +65 -76
  8. data/examples/build_exec_rubyc +40 -0
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +23 -24
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +126 -97
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +15 -10
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +99 -42
  25. data/lib/aspera/ascp/management.rb +3 -2
  26. data/lib/aspera/ascp/products.rb +12 -0
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +27 -17
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +5 -0
  33. data/lib/aspera/cli/plugin.rb +15 -29
  34. data/lib/aspera/cli/plugins/alee.rb +3 -3
  35. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  36. data/lib/aspera/cli/plugins/ats.rb +16 -14
  37. data/lib/aspera/cli/plugins/config.rb +53 -45
  38. data/lib/aspera/cli/plugins/console.rb +3 -3
  39. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  40. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  41. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  42. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  43. data/lib/aspera/cli/plugins/node.rb +153 -95
  44. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  45. data/lib/aspera/cli/plugins/preview.rb +8 -9
  46. data/lib/aspera/cli/plugins/server.rb +5 -9
  47. data/lib/aspera/cli/plugins/shares.rb +2 -2
  48. data/lib/aspera/cli/sync_actions.rb +2 -2
  49. data/lib/aspera/cli/transfer_agent.rb +12 -14
  50. data/lib/aspera/cli/transfer_progress.rb +35 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +3 -4
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +34 -18
  55. data/lib/aspera/faspex_gw.rb +2 -2
  56. data/lib/aspera/json_rpc.rb +1 -1
  57. data/lib/aspera/keychain/macos_security.rb +7 -12
  58. data/lib/aspera/log.rb +3 -4
  59. data/lib/aspera/oauth/base.rb +39 -45
  60. data/lib/aspera/oauth/factory.rb +11 -4
  61. data/lib/aspera/oauth/generic.rb +4 -8
  62. data/lib/aspera/oauth/jwt.rb +3 -3
  63. data/lib/aspera/oauth/url_json.rb +1 -2
  64. data/lib/aspera/oauth/web.rb +5 -2
  65. data/lib/aspera/persistency_action_once.rb +16 -8
  66. data/lib/aspera/preview/utils.rb +5 -16
  67. data/lib/aspera/rest.rb +100 -76
  68. data/lib/aspera/transfer/faux_file.rb +4 -4
  69. data/lib/aspera/transfer/parameters.rb +14 -16
  70. data/lib/aspera/transfer/spec.rb +12 -12
  71. data/lib/aspera/transfer/sync.rb +1 -5
  72. data/lib/aspera/transfer/uri.rb +1 -1
  73. data/lib/aspera/uri_reader.rb +1 -1
  74. data/lib/aspera/web_auth.rb +166 -17
  75. data/lib/aspera/web_server_simple.rb +4 -3
  76. data/lib/transfer_pb.rb +84 -0
  77. data/lib/transfer_services_pb.rb +82 -0
  78. data.tar.gz.sig +0 -0
  79. metadata +24 -5
  80. metadata.gz.sig +0 -0
@@ -33,6 +33,8 @@ module Aspera
33
33
  ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
34
34
  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
35
35
  HEADER_X_TOTAL_COUNT = 'X-Total-Count'
36
+ HEADER_X_CACHE_CONTROL = 'X-Aspera-Cache-Control'
37
+ HEADER_X_NEXT_ITER_TOKEN = 'X-Aspera-Next-Iteration-Token'
36
38
  SCOPE_USER = 'user:all'
37
39
  SCOPE_ADMIN = 'admin:all'
38
40
  PATH_SEPARATOR = '/'
@@ -42,9 +44,17 @@ module Aspera
42
44
 
43
45
  # class instance variable, access with accessors on class
44
46
  @use_standard_ports = true
47
+ @use_node_cache = true
45
48
 
46
49
  class << self
47
50
  attr_accessor :use_standard_ports
51
+ attr_accessor :use_node_cache
52
+
53
+ def cache_control_headers
54
+ h = {'Accept' => 'application/json'}
55
+ h[HEADER_X_CACHE_CONTROL] = 'no-cache' unless use_node_cache
56
+ h
57
+ end
48
58
 
49
59
  # For access keys: provide expression to match entry in folder
50
60
  def file_matcher(match_expression)
@@ -146,6 +156,11 @@ module Aspera
146
156
  end
147
157
  end
148
158
 
159
+ # Call node API, possibly adding cache control header, as globally specified
160
+ def read_with_cache(subpath, query=nil)
161
+ return call(operation: 'GET', subpath: subpath, headers: self.class.cache_control_headers, query: query)[:data]
162
+ end
163
+
149
164
  # update transfer spec with special additional tags
150
165
  def add_tspec_info(tspec)
151
166
  tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
@@ -171,7 +186,7 @@ module Aspera
171
186
  def entry_has_link_information(entry)
172
187
  # if target information is missing in folder, try to get it on entry
173
188
  if entry['target_node_id'].nil? || entry['target_id'].nil?
174
- link_entry = read("files/#{entry['id']}")[:data]
189
+ link_entry = read("files/#{entry['id']}")
175
190
  entry['target_node_id'] = link_entry['target_node_id']
176
191
  entry['target_id'] = link_entry['target_id']
177
192
  end
@@ -200,13 +215,19 @@ module Aspera
200
215
  # get folder content
201
216
  folder_contents =
202
217
  begin
203
- read("files/#{current_item[:id]}/files")[:data]
218
+ read("files/#{current_item[:id]}/files")
204
219
  rescue StandardError => e
205
220
  Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
206
221
  []
207
222
  end
208
223
  Log.log.debug{Log.dump(:folder_contents, folder_contents)}
209
224
  folder_contents.each do |entry|
225
+ 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
229
+ next
230
+ end
210
231
  relative_path = File.join(current_item[:path], entry['name'])
211
232
  Log.log.debug{"process_folder_tree: checking #{relative_path}"}
212
233
  # call block, continue only if method returns true
@@ -228,16 +249,16 @@ module Aspera
228
249
  end
229
250
  end
230
251
 
231
- # Navigate the path from given file id
252
+ # Navigate the path from given file id on current node, and return the node and file id of target.
253
+ # If the path ends with a "/" or process_last_link is true then if the last item in path is a link, it is followed.
232
254
  # @param top_file_id [String] id initial file id
233
- # @param path [String] file path
255
+ # @param path [String] file or folder path (end with "/" is like setting process_last_link)
256
+ # @param process_last_link [Boolean] if true, follow the last link
234
257
  # @return [Hash] {.api,.file_id}
235
- def resolve_api_fid(top_file_id, path)
258
+ def resolve_api_fid(top_file_id, path, process_last_link=false)
236
259
  Aspera.assert_type(top_file_id, String)
237
260
  Aspera.assert_type(path, String)
238
- # if last element is a link and followed by "/", we list the content of that folder, else we return the link
239
- process_last_link = path.end_with?(PATH_SEPARATOR)
240
- # keep only non-empty elements
261
+ process_last_link ||= path.end_with?(PATH_SEPARATOR)
241
262
  path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
242
263
  return {api: self, file_id: top_file_id} if path_elements.empty?
243
264
  resolve_state = {path: path_elements, result: nil, process_last_link: process_last_link}
@@ -265,7 +286,7 @@ module Aspera
265
286
  full_spec = create(
266
287
  'files/download_setup',
267
288
  {transfer_requests: [{transfer_request: {paths: [{source: '/'}]}}]}
268
- )[:data]['transfer_specs'].first['transfer_spec']
289
+ )['transfer_specs'].first['transfer_spec']
269
290
  # set available fields
270
291
  @std_t_spec_cache = Transfer::Spec::TRANSPORT_FIELDS.each_with_object({}) do |i, h|
271
292
  h[i] = full_spec[i] if full_spec.key?(i)
@@ -317,13 +338,13 @@ module Aspera
317
338
  if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
318
339
  transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
319
340
  end
320
- info = read('info')[:data]
341
+ info = read('info')
321
342
  # get the transfer user from info on access key
322
343
  transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
323
344
  # get settings from name.value array to hash key.value
324
345
  settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
325
346
  # check WSS ports
326
- %w[wss_enabled wss_port].each do |i|
347
+ Transfer::Spec::WSS_FIELDS.each do |i|
327
348
  transfer_spec[i] = settings[i] if settings.key?(i)
328
349
  end if settings.is_a?(Hash)
329
350
  else
@@ -380,7 +401,7 @@ module Aspera
380
401
  return true
381
402
  end
382
403
 
383
- def process_find_files(entry, _path, state)
404
+ def process_find_files(entry, path, state)
384
405
  state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
385
406
  # test all files deeply
386
407
  return true
data/lib/aspera/ascmd.rb CHANGED
@@ -22,7 +22,54 @@ module Aspera
22
22
  mv: 2,
23
23
  rm: 1
24
24
  }.freeze
25
- private_constant :OPS_ARGS
25
+
26
+ # protocol is based on Type-Length-Value
27
+ # type start at one, but array index start at zero
28
+ ENUM_START = 1
29
+
30
+ # description of result structures (see ascmdtypes.h).
31
+ # Base types are big endian
32
+ # key = name of type
33
+ # index in array `fields` is the type (minus ENUM_START)
34
+ # decoding always start at `result`
35
+ # some fields have special handling indicated by `special`
36
+ # field_list, list_tlv_list, list_tlv_restart are composed with a list of TLV
37
+ TYPES_DESCR = {
38
+ result: {decode: :field_list,
39
+ 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},
40
+ {name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
41
+ {name: :df, is_a: :mnt, special: :list_tlv_restart}, {name: :md5sum, is_a: :md5sum}]},
42
+ stat: {decode: :field_list,
43
+ fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
44
+ {name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
45
+ {name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
46
+ {name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
47
+ {name: :errstr, is_a: :zstr}]},
48
+ info: {decode: :field_list,
49
+ fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
50
+ {name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
51
+ {name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :list_multiple}, {name: :browse_caps, is_a: :zstr},
52
+ {name: :protocol, is_a: :zstr}]},
53
+ size: {decode: :field_list,
54
+ fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
55
+ {name: :failed_dcount, is_a: :int32}]},
56
+ error: {decode: :field_list,
57
+ fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
58
+ mnt: {decode: :field_list,
59
+ fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
60
+ {name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
61
+ {name: :errstr, is_a: :zstr}]},
62
+ md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
63
+ int8: {decode: :base, unpack: 'C', size: 1},
64
+ int32: {decode: :base, unpack: 'L>', size: 4},
65
+ int64: {decode: :base, unpack: 'Q>', size: 8},
66
+ epoch: {decode: :base, unpack: 'Q>', size: 8},
67
+ zstr: {decode: :base, unpack: 'Z*'},
68
+ blist: {decode: :buffer_list}
69
+ }.freeze
70
+
71
+ private_constant :TYPES_DESCR, :ENUM_START, :OPS_ARGS
72
+
26
73
  # list of supported actions
27
74
  OPERATIONS = OPS_ARGS.keys.freeze
28
75
 
@@ -55,6 +102,7 @@ module Aspera
55
102
  arg_batches.each do |args|
56
103
  command = [main_command]
57
104
  # enclose arguments in double quotes, protect backslash and double quotes
105
+ # ascmd uses space as token separator, and optional quotes ('") or \ to escape
58
106
  args.each do |v|
59
107
  command.push(%Q{"#{v.gsub(/["\\]/){|s|"\\#{s}"}}"})
60
108
  end
@@ -106,47 +154,6 @@ module Aspera
106
154
  def extended_message; "ascmd: errno=#{@errno} errstr=\"#{@errstr}\" command=#{@command} arguments=#{@arguments&.join(',')}"; end
107
155
  end
108
156
 
109
- # description of result structures (see ascmdtypes.h). Base types are big endian
110
- # key = name of type
111
- TYPES_DESCR = {
112
- result: {decode: :field_list,
113
- fields: [{name: :file, is_a: :stat}, {name: :dir, is_a: :stat, special: :sub_struct}, {name: :size, is_a: :size}, {name: :error, is_a: :error},
114
- {name: :info, is_a: :info}, {name: :success, is_a: nil, special: :return_true}, {name: :exit, is_a: nil},
115
- {name: :df, is_a: :mnt, special: :restart_on_first}, {name: :md5sum, is_a: :md5sum}]},
116
- stat: {decode: :field_list,
117
- fields: [{name: :name, is_a: :zstr}, {name: :size, is_a: :int64}, {name: :mode, is_a: :int32, check: nil}, {name: :zmode, is_a: :zstr},
118
- {name: :uid, is_a: :int32, check: nil}, {name: :zuid, is_a: :zstr}, {name: :gid, is_a: :int32, check: nil}, {name: :zgid, is_a: :zstr},
119
- {name: :ctime, is_a: :epoch}, {name: :zctime, is_a: :zstr}, {name: :mtime, is_a: :epoch}, {name: :zmtime, is_a: :zstr},
120
- {name: :atime, is_a: :epoch}, {name: :zatime, is_a: :zstr}, {name: :symlink, is_a: :zstr}, {name: :errno, is_a: :int32},
121
- {name: :errstr, is_a: :zstr}]},
122
- info: {decode: :field_list,
123
- fields: [{name: :platform, is_a: :zstr}, {name: :version, is_a: :zstr}, {name: :lang, is_a: :zstr}, {name: :territory, is_a: :zstr},
124
- {name: :codeset, is_a: :zstr}, {name: :lc_ctype, is_a: :zstr}, {name: :lc_numeric, is_a: :zstr}, {name: :lc_time, is_a: :zstr},
125
- {name: :lc_all, is_a: :zstr}, {name: :dev, is_a: :zstr, special: :multiple}, {name: :browse_caps, is_a: :zstr},
126
- {name: :protocol, is_a: :zstr}]},
127
- size: {decode: :field_list,
128
- fields: [{name: :size, is_a: :int64}, {name: :fcount, is_a: :int32}, {name: :dcount, is_a: :int32}, {name: :failed_fcount, is_a: :int32},
129
- {name: :failed_dcount, is_a: :int32}]},
130
- error: {decode: :field_list,
131
- fields: [{name: :errno, is_a: :int32}, {name: :errstr, is_a: :zstr}]},
132
- mnt: {decode: :field_list,
133
- fields: [{name: :fs, is_a: :zstr}, {name: :dir, is_a: :zstr}, {name: :is_a, is_a: :zstr}, {name: :total, is_a: :int64},
134
- {name: :used, is_a: :int64}, {name: :free, is_a: :int64}, {name: :fcount, is_a: :int64}, {name: :errno, is_a: :int32},
135
- {name: :errstr, is_a: :zstr}]},
136
- md5sum: {decode: :field_list, fields: [{name: :md5sum, is_a: :zstr}]},
137
- int8: {decode: :base, unpack: 'C', size: 1},
138
- int32: {decode: :base, unpack: 'L>', size: 4},
139
- int64: {decode: :base, unpack: 'Q>', size: 8},
140
- epoch: {decode: :base, unpack: 'Q>', size: 8},
141
- zstr: {decode: :base, unpack: 'Z*'},
142
- blist: {decode: :buffer_list}
143
- }.freeze
144
-
145
- # protocol enum start at one, but array index start at zero
146
- ENUM_START = 1
147
-
148
- private_constant :TYPES_DESCR, :ENUM_START
149
-
150
157
  class << self
151
158
  # get description of structure's field, @param struct_name, @param typed_buffer provides field name
152
159
  def field_description(struct_name, typed_buffer)
@@ -175,6 +182,7 @@ module Aspera
175
182
  Log.log.trace1{"#{' .' * indent_level}-> base:#{byte_array} -> #{result}"}
176
183
  result = Time.at(result) if type_name.eql?(:epoch)
177
184
  when :buffer_list
185
+ # return a list of type_buffer
178
186
  result = []
179
187
  until buffer.empty?
180
188
  btype = parse(buffer, :int8, indent_level)
@@ -193,16 +201,16 @@ module Aspera
193
201
  field_info = field_description(type_name, typed_buffer)
194
202
  Log.log.trace1{"#{' .' * indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green}
195
203
  case field_info[:special]
196
- when nil
204
+ when nil # normal case
197
205
  result[field_info[:name]] = parse(typed_buffer[:buffer], field_info[:is_a], indent_level)
198
- when :return_true
206
+ when :return_true # nothing to parse, just return true
199
207
  result[field_info[:name]] = true
200
- when :sub_struct
201
- result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
202
- when :multiple
208
+ when :list_multiple # field appears multiple times, and is an array of values (base type)
203
209
  result[field_info[:name]] ||= []
204
210
  result[field_info[:name]].push(parse(typed_buffer[:buffer], field_info[:is_a], indent_level))
205
- when :restart_on_first
211
+ when :list_tlv_list # field is an array of values in a list of buffers
212
+ result[field_info[:name]] = parse(typed_buffer[:buffer], :blist, indent_level).map{|r|parse(r[:buffer], field_info[:is_a], indent_level)}
213
+ when :list_tlv_restart # field is an array of values, but a new value is started on index 1
206
214
  fl = result[field_info[:name]] = []
207
215
  parse(typed_buffer[:buffer], :blist, indent_level).map do |tb|
208
216
  fl.push({}) if tb[:btype].eql?(ENUM_START)
@@ -5,16 +5,16 @@ require 'aspera/environment'
5
5
  require 'aspera/data_repository'
6
6
  require 'aspera/ascp/products'
7
7
  require 'aspera/log'
8
+ require 'aspera/rest'
8
9
  require 'aspera/assert'
9
10
  require 'aspera/web_server_simple'
10
11
  require 'English'
11
12
  require 'singleton'
12
13
  require 'xmlsimple'
13
- require 'zlib'
14
14
  require 'base64'
15
15
  require 'fileutils'
16
16
  require 'openssl'
17
-
17
+ require 'yaml'
18
18
  module Aspera
19
19
  module Ascp
20
20
  # Singleton that tells where to find ascp and other local resources (keys..) , using the "path(:name)" method.
@@ -28,7 +28,7 @@ module Aspera
28
28
  include Singleton
29
29
  # protobuf generated files from sdk
30
30
  EXT_RUBY_PROTOBUF = '_pb.rb'
31
- RB_SDK_FOLDER = 'lib'
31
+ RB_SDK_SUBFOLDER = 'lib'
32
32
  DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
33
33
  <?xml version='1.0' encoding='UTF-8'?>
34
34
  <CONF version="2">
@@ -43,9 +43,13 @@ module Aspera
43
43
  # all ascp files (in SDK)
44
44
  EXE_FILES = %i[ascp ascp4 async].freeze
45
45
  FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
46
- private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_FOLDER, :DEFAULT_ASPERA_CONF, :FILES
46
+ TRANSFER_SDK_LOCATION_URL = 'https://ibm.biz/sdk_location'
47
+ FILE_SCHEME_PREFIX = 'file:///'
48
+ SDK_ARCHIVE_FOLDERS = ['/bin/', '/aspera/'].freeze
49
+ private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_SUBFOLDER, :DEFAULT_ASPERA_CONF, :FILES, :TRANSFER_SDK_LOCATION_URL, :FILE_SCHEME_PREFIX
47
50
  # options for SSH client private key
48
51
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
52
+
49
53
  # set ascp executable path
50
54
  def ascp_path=(v)
51
55
  @path_to_ascp = v
@@ -55,12 +59,6 @@ module Aspera
55
59
  path(:ascp)
56
60
  end
57
61
 
58
- def sdk_ruby_folder
59
- ruby_pb_folder = File.join(sdk_folder, RB_SDK_FOLDER)
60
- FileUtils.mkdir_p(ruby_pb_folder)
61
- return ruby_pb_folder
62
- end
63
-
64
62
  # location of SDK files
65
63
  def sdk_folder=(v)
66
64
  Log.log.debug{"sdk_folder=#{v}"}
@@ -83,7 +81,7 @@ module Aspera
83
81
  def use_ascp_from_product(product_name)
84
82
  if product_name.eql?(FIRST_FOUND)
85
83
  pl = Products.installed_products.first
86
- raise "no FASP installation found\nPlease check manual on how to install FASP." if pl.nil?
84
+ raise "ascp found: no Aspera transfer module or SDK found.\nRefer to the manual or install SDK with command:\nascli conf ascp install" if pl.nil?
87
85
  else
88
86
  pl = Products.installed_products.find{|i|i[:name].eql?(product_name)}
89
87
  raise "no such product installed: #{product_name}" if pl.nil?
@@ -177,7 +175,7 @@ module Aspera
177
175
  return nil unless File.exist?(exe_path)
178
176
  exe_version = nil
179
177
  cmd_out = %x("#{exe_path}" #{vers_arg})
180
- raise "An error occurred when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
178
+ raise "An error occurred when testing #{exe_path}: #{cmd_out}" unless $CHILD_STATUS == 0
181
179
  # get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
182
180
  m = cmd_out.match(/ version ([0-9.]+)/)
183
181
  exe_version = m[1].gsub(/\.$/, '') unless m.nil?
@@ -229,54 +227,113 @@ module Aspera
229
227
  return data
230
228
  end
231
229
 
230
+ # information for `ascp info`
232
231
  def ascp_info
233
- files = file_paths
234
- return files.merge(ascp_pvcl_info).merge(ascp_ssl_info)
232
+ ascp_data = file_paths
233
+ ascp_data.merge!(ascp_pvcl_info)
234
+ ascp_data['sdk_locations'] = TRANSFER_SDK_LOCATION_URL
235
+ ascp_data.merge!(ascp_ssl_info)
236
+ return ascp_data
237
+ end
238
+
239
+ # Loads YAML from cloud with locations of SDK archives for all platforms
240
+ # @return location structure
241
+ def sdk_locations
242
+ yaml_text = Aspera::Rest.new(base_url: TRANSFER_SDK_LOCATION_URL, redirect_max: 3).call(operation: 'GET')[:data]
243
+ YAML.load(yaml_text)
244
+ end
245
+
246
+ # @return the url for download of SDK archive for the given platform and version
247
+ def sdk_url_for_platform(platform: nil, version: nil)
248
+ locations = sdk_locations
249
+ platform = Environment.architecture if platform.nil?
250
+ locations = locations.select{|l|l['platform'].eql?(platform)}
251
+ raise "No SDK for platform: #{platform}" if locations.empty?
252
+ version = locations.max_by { |entry| Gem::Version.new(entry['version']) }['version'] if version.nil?
253
+ info = locations.select{|entry| entry['version'].eql?(version)}
254
+ raise "No such version: #{version} for #{platform}" if info.empty?
255
+ return info.first['url']
256
+ end
257
+
258
+ def extract_archive_files(sdk_archive_path)
259
+ raise 'missing block' unless block_given?
260
+ case sdk_archive_path
261
+ # Windows and Mac use zip
262
+ when /\.zip$/
263
+ require 'zip'
264
+ # extract files from archive
265
+ Zip::File.open(sdk_archive_path) do |zip_file|
266
+ zip_file.each do |entry|
267
+ next if entry.name.end_with?('/')
268
+ yield(entry.name, entry.get_input_stream)
269
+ end
270
+ end
271
+ # Other Unixes use tar.gz
272
+ when /\.tar\.gz/
273
+ require 'zlib'
274
+ require 'rubygems/package'
275
+ Zlib::GzipReader.open(sdk_archive_path) do |gzip|
276
+ Gem::Package::TarReader.new(gzip) do |tar|
277
+ tar.each do |entry|
278
+ next if entry.directory?
279
+ yield(entry.full_name, entry)
280
+ end
281
+ end
282
+ end
283
+ else
284
+ raise "unknown archive extension: #{sdk_archive_path}"
285
+ end
235
286
  end
236
287
 
237
288
  # download aspera SDK or use local file
238
289
  # extracts ascp binary for current system architecture
290
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
239
291
  # @return ascp version (from execution)
240
- def install_sdk(sdk_url)
241
- # SDK is organized by architecture, check this first, in case architecture is not supported
242
- arch_filter = "#{Environment.architecture}/"
243
- require 'zip'
244
- sdk_zip_path = File.join(Dir.tmpdir, 'sdk.zip')
245
- if sdk_url.start_with?('file:')
292
+ def install_sdk(url: nil, folder: nil, backup: true, with_exe: true, &block)
293
+ url = sdk_url_for_platform if url.nil? || url.eql?('DEF')
294
+ folder = sdk_folder if folder.nil?
295
+ subfolder_lambda = block
296
+ if subfolder_lambda.nil?
297
+ subfolder_lambda = ->(name) do
298
+ if SDK_ARCHIVE_FOLDERS.any?{|i|name.include?(i)}
299
+ '/'
300
+ elsif name.end_with?(EXT_RUBY_PROTOBUF)
301
+ RB_SDK_SUBFOLDER
302
+ end
303
+ end
304
+ end
305
+ if url.start_with?('file:')
246
306
  # require specific file scheme: the path part is "relative", or absolute if there are 4 slash
247
- raise 'use format: file:///<path>' unless sdk_url.start_with?('file:///')
248
- sdk_zip_path = sdk_url.gsub(%r{^file:///}, '')
307
+ raise 'use format: file:///<path>' unless url.start_with?(FILE_SCHEME_PREFIX)
308
+ sdk_archive_path = url[FILE_SCHEME_PREFIX.length..-1]
309
+ delete_archive = false
249
310
  else
250
- Aspera::Rest.new(base_url: sdk_url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_zip_path)
311
+ sdk_archive_path = File.join(Dir.tmpdir, File.basename(url))
312
+ Aspera::Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', save_to_file: sdk_archive_path)
313
+ delete_archive = true
251
314
  end
252
315
  # rename old install
253
- if !Dir.empty?(sdk_folder)
316
+ if backup && !Dir.empty?(folder)
254
317
  Log.log.warn('Previous install exists, renaming folder.')
255
- File.rename(sdk_folder, "#{sdk_folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
318
+ File.rename(folder, "#{folder}.#{Time.now.strftime('%Y%m%d%H%M%S')}")
256
319
  # TODO: delete old archives ?
257
320
  end
258
- # extract files from archive
259
- Zip::File.open(sdk_zip_path) do |zip_file|
260
- zip_file.each do |entry|
261
- # skip folder entries
262
- next if entry.name.end_with?('/')
263
- dest_folder = nil
264
- # binaries
265
- dest_folder = sdk_folder if entry.name.include?(arch_filter)
266
- # ruby adapters
267
- dest_folder = sdk_ruby_folder if entry.name.end_with?(EXT_RUBY_PROTOBUF)
268
- next if dest_folder.nil?
269
- File.open(File.join(dest_folder, File.basename(entry.name)), 'wb') do |output_stream|
270
- IO.copy_stream(entry.get_input_stream, output_stream)
271
- end
321
+ extract_archive_files(sdk_archive_path) do |entry_name, entry_stream|
322
+ subfolder = subfolder_lambda.call(entry_name)
323
+ next if subfolder.nil?
324
+ dest_folder = File.join(folder, subfolder)
325
+ FileUtils.mkdir_p(dest_folder)
326
+ File.open(File.join(dest_folder, File.basename(entry_name)), 'wb') do |output_stream|
327
+ IO.copy_stream(entry_stream, output_stream)
272
328
  end
273
329
  end
274
- File.unlink(sdk_zip_path) rescue nil # Windows may give error
330
+ File.unlink(sdk_archive_path) rescue nil if delete_archive # Windows may give error
331
+ return unless with_exe
275
332
  # ensure license file are generated so that ascp invocation for version works
276
333
  path(:aspera_license)
277
334
  path(:aspera_conf)
278
335
  sdk_ascp_file = Products.ascp_filename
279
- sdk_ascp_path = File.join(sdk_folder, sdk_ascp_file)
336
+ sdk_ascp_path = File.join(folder, sdk_ascp_file)
280
337
  raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
281
338
  EXE_FILES.each do |exe_sym|
282
339
  exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
@@ -289,7 +346,7 @@ module Aspera
289
346
  transferd_version = get_exe_version(sdk_daemon_path, 'version')
290
347
  sdk_name = 'IBM Aspera Transfer SDK'
291
348
  sdk_version = transferd_version || sdk_ascp_version
292
- File.write(File.join(sdk_folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
349
+ File.write(File.join(folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
293
350
  return sdk_name, sdk_version
294
351
  end
295
352
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/assert'
4
+
3
5
  module Aspera
4
6
  module Ascp
5
7
  # processing of ascp management port events
@@ -237,8 +239,7 @@ module Aspera
237
239
  @last_event = @event_build
238
240
  @event_build = nil
239
241
  return @last_event
240
- else
241
- raise "mgt port: unexpected line: [#{line}]"
242
+ else Aspera.error_unexpected_value(line){'mgt port'}
242
243
  end
243
244
  return nil
244
245
  end
@@ -56,6 +56,18 @@ module Aspera
56
56
  log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
57
57
  run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
58
58
  sub_bin: File.join('Contents', 'Resources')
59
+ }, {
60
+ expected: CONNECT,
61
+ app_root: File.join(Dir.home, 'Applications', 'IBM Aspera Connect.app'),
62
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
63
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
64
+ sub_bin: File.join('Contents', 'Resources')
65
+ }, {
66
+ expected: CONNECT,
67
+ app_root: File.join('', 'Applications', 'IBM Aspera Connect.app'),
68
+ log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
69
+ run_root: File.join(Dir.home, 'Library', 'Application Support', 'Aspera', 'Aspera Connect'),
70
+ sub_bin: File.join('Contents', 'Resources')
59
71
  }, {
60
72
  expected: CLI_V1,
61
73
  app_root: File.join(Dir.home, 'Applications', 'Aspera CLI'),
data/lib/aspera/assert.rb CHANGED
@@ -8,13 +8,13 @@ module Aspera
8
8
  end
9
9
  class << self
10
10
  # the block is executed in the context of the Aspera module
11
- def assert(assertion, info = nil, level: 2, exception_class: AssertError)
11
+ def assert(assertion, info = nil, exception_class: AssertError)
12
12
  raise InternalError, 'bad assert: both info and block given' unless info.nil? || !block_given?
13
13
  return if assertion
14
14
  message = 'assertion failed'
15
15
  info = yield if block_given?
16
16
  message = "#{message}: #{info}" if info
17
- message = "#{message}: #{caller(level..level).first}"
17
+ message = "#{message}: #{caller.find{|call|!call.start_with?(__FILE__)}}"
18
18
  raise exception_class, message
19
19
  end
20
20
 
@@ -22,13 +22,18 @@ module Aspera
22
22
  # @param value [Object] the value to check
23
23
  # @param type [Class] the expected type
24
24
  def assert_type(value, type, exception_class: AssertError)
25
- assert(value.is_a?(type), level: 3, exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
25
+ assert(value.is_a?(type), exception_class: exception_class){"#{block_given? ? "#{yield}: " : nil}expecting #{type}, but have #{value.inspect}"}
26
26
  end
27
27
 
28
28
  # assert that value is one of the given values
29
+ # @param value value to check
30
+ # @param values accepted values
31
+ # @param exception_class exception in case of no match
29
32
  def assert_values(value, values, exception_class: AssertError)
30
- assert(values.include?(value), level: 3, exception_class: exception_class) do
31
- "#{block_given? ? "#{yield}: " : nil}expecting one of #{values.inspect}, but have #{value.inspect}"
33
+ assert(values.include?(value), exception_class: exception_class) do
34
+ val_list = values.inspect
35
+ val_list = "one of #{val_list}" if values.is_a?(Array)
36
+ "#{block_given? ? "#{yield}: " : nil}expecting #{val_list}, but have #{value.inspect}"
32
37
  end
33
38
  end
34
39