aspera-cli 4.2.0 → 4.4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +749 -353
  3. data/docs/Makefile +4 -4
  4. data/docs/README.erb.md +743 -283
  5. data/docs/doc_tools.rb +58 -0
  6. data/docs/test_env.conf +9 -1
  7. data/examples/aoc.rb +14 -3
  8. data/examples/faspex4.rb +89 -0
  9. data/lib/aspera/aoc.rb +24 -22
  10. data/lib/aspera/cli/main.rb +48 -20
  11. data/lib/aspera/cli/plugin.rb +13 -6
  12. data/lib/aspera/cli/plugins/aoc.rb +117 -78
  13. data/lib/aspera/cli/plugins/config.rb +127 -80
  14. data/lib/aspera/cli/plugins/faspex.rb +112 -63
  15. data/lib/aspera/cli/plugins/faspex5.rb +29 -25
  16. data/lib/aspera/cli/plugins/node.rb +54 -25
  17. data/lib/aspera/cli/plugins/preview.rb +94 -68
  18. data/lib/aspera/cli/plugins/server.rb +16 -5
  19. data/lib/aspera/cli/transfer_agent.rb +92 -72
  20. data/lib/aspera/cli/version.rb +1 -1
  21. data/lib/aspera/command_line_builder.rb +48 -31
  22. data/lib/aspera/cos_node.rb +4 -3
  23. data/lib/aspera/fasp/http_gw.rb +47 -26
  24. data/lib/aspera/fasp/local.rb +31 -24
  25. data/lib/aspera/fasp/manager.rb +3 -0
  26. data/lib/aspera/fasp/node.rb +23 -1
  27. data/lib/aspera/fasp/parameters.rb +72 -89
  28. data/lib/aspera/fasp/parameters.yaml +531 -0
  29. data/lib/aspera/fasp/uri.rb +1 -1
  30. data/lib/aspera/faspex_gw.rb +10 -9
  31. data/lib/aspera/id_generator.rb +22 -0
  32. data/lib/aspera/node.rb +11 -3
  33. data/lib/aspera/oauth.rb +131 -135
  34. data/lib/aspera/persistency_action_once.rb +11 -7
  35. data/lib/aspera/persistency_folder.rb +6 -26
  36. data/lib/aspera/rest.rb +1 -1
  37. data/lib/aspera/sync.rb +40 -35
  38. data/lib/aspera/timer_limiter.rb +22 -0
  39. data/lib/aspera/web_auth.rb +105 -0
  40. metadata +22 -4
  41. data/docs/transfer_spec.html +0 -99
  42. data/lib/aspera/fasp/aoc.rb +0 -24
@@ -4,21 +4,45 @@ module Aspera
4
4
  # process_param is called repeatedly with all known parameters
5
5
  # add_env_args is called to get resulting param list and env var (also checks that all params were used)
6
6
  class CommandLineBuilder
7
+ # transform yes/no to trye/false
8
+ def self.yes_to_true(value)
9
+ case value
10
+ when 'yes'; return true
11
+ when 'no'; return false
12
+ end
13
+ raise "unsupported value: #{value}"
14
+ end
7
15
 
8
- private
9
- # default value for command line based on option name
10
- def switch_name(param_name,options)
11
- return options[:option_switch] if options.has_key?(:option_switch)
12
- return '--'+param_name.to_s.gsub('_','-')
16
+ # Called by provider of definition before constructor of this class so that params_definition has all mandatory fields
17
+ def self.normalize_description(d)
18
+ d.each do |param_name,options|
19
+ raise "Expecting Hash, but have #{options.class} in #{param_name}" unless options.is_a?(Hash)
20
+ #options[:accepted_types]=:bool if options[:cltype].eql?(:envvar) and !options.has_key?(:accepted_types)
21
+ # by default : not mandatory
22
+ options[:mandatory]||=false
23
+ options[:desc]||=''
24
+ # by default : string, unless it's without arg
25
+ if ! options.has_key?(:accepted_types)
26
+ options[:accepted_types]=options[:cltype].eql?(:opt_without_arg) ? :bool : :string
27
+ end
28
+ # single type is placed in array
29
+ options[:accepted_types]=[options[:accepted_types]] unless options[:accepted_types].is_a?(Array)
30
+ if !options.has_key?(:option_switch) and options.has_key?(:cltype) and [:opt_without_arg,:opt_with_arg].include?(options[:cltype])
31
+ options[:option_switch]='--'+param_name.to_s.gsub('_','-')
32
+ end
33
+ end
13
34
  end
14
35
 
36
+ private
37
+
38
+ # clvarname : command line variable name
15
39
  def env_name(param_name,options)
16
- return options[:variable]
40
+ return options[:clvarname]
17
41
  end
18
42
 
19
43
  public
20
44
 
21
- BOOLEAN_CLASSES=[TrueClass,FalseClass]
45
+ attr_reader :params_definition
22
46
 
23
47
  # @param param_hash
24
48
  def initialize(param_hash,params_definition)
@@ -44,15 +68,6 @@ module Aspera
44
68
  return nil
45
69
  end
46
70
 
47
- # transform yes/no to trye/false
48
- def self.yes_to_true(value)
49
- case value
50
- when 'yes'; return true
51
- when 'no'; return false
52
- end
53
- raise "unsupported value: #{value}"
54
- end
55
-
56
71
  # add options directly to ascp command line
57
72
  def add_command_line_options(options)
58
73
  return if options.nil?
@@ -71,24 +86,25 @@ module Aspera
71
86
  # @param options : options for type
72
87
  def process_param(param_name,action=nil)
73
88
  options=@params_definition[param_name]
74
- action=options[:type] if action.nil?
89
+ action=options[:cltype] if action.nil?
75
90
  # should not happen
76
91
  raise "Internal error: ask processing of param #{param_name}" if options.nil?
77
- # by default : not mandatory
78
- options[:mandatory]||=false
79
- if options.has_key?(:accepted_types)
80
- # single type is placed in array
81
- options[:accepted_types]=[options[:accepted_types]] unless options[:accepted_types].is_a?(Array)
82
- else
83
- # by default : string, unless it's without arg
84
- options[:accepted_types]=action.eql?(:opt_without_arg) ? BOOLEAN_CLASSES : [String]
85
- end
86
92
  # check mandatory parameter (nil is valid value)
87
93
  raise Fasp::Error.new("mandatory parameter: #{param_name}") if options[:mandatory] and !@param_hash.has_key?(param_name)
88
94
  parameter_value=@param_hash[param_name]
89
- parameter_value=options[:default] if parameter_value.nil? and options.has_key?(:default)
95
+ #parameter_value=options[:default] if parameter_value.nil? and options.has_key?(:default)
96
+ expected_classes=options[:accepted_types].map do |s|
97
+ case s
98
+ when :string; String
99
+ when :array; Array
100
+ when :hash; Hash
101
+ when :int; Integer
102
+ when :bool; [TrueClass,FalseClass]
103
+ else raise "INTERNAL: unexpected value: #{s}"
104
+ end
105
+ end.flatten
90
106
  # check provided type
91
- raise Fasp::Error.new("#{param_name} is : #{parameter_value.class} (#{parameter_value}), shall be #{options[:accepted_types]}, ") unless parameter_value.nil? or options[:accepted_types].inject(false){|m,v|m or parameter_value.is_a?(v)}
107
+ raise Fasp::Error.new("#{param_name} is : #{parameter_value.class} (#{parameter_value}), shall be #{options[:accepted_types]}, ") unless parameter_value.nil? or expected_classes.include?(parameter_value.class)
92
108
  @used_param_names.push(param_name) unless action.eql?(:defer)
93
109
 
94
110
  # process only non-nil values
@@ -102,7 +118,8 @@ module Aspera
102
118
  end
103
119
  raise "unsupported value: #{parameter_value}" unless options[:accepted_values].nil? or options[:accepted_values].include?(parameter_value)
104
120
  if options[:encode]
105
- newvalue=options[:encode].call(parameter_value)
121
+ # :encode has name of class with encoding method
122
+ newvalue=Kernel.const_get(options[:encode]).send("encode_#{param_name}",parameter_value)
106
123
  raise Fasp::Error.new("unsupported #{param_name}: #{parameter_value}") if newvalue.nil?
107
124
  parameter_value=newvalue
108
125
  end
@@ -123,12 +140,12 @@ module Aspera
123
140
  else raise Fasp::Error.new("unsupported #{param_name}: #{parameter_value}")
124
141
  end
125
142
  add_param=!add_param if options[:add_on_false]
126
- add_command_line_options([switch_name(param_name,options)]) if add_param
143
+ add_command_line_options([options[:option_switch]]) if add_param
127
144
  when :opt_with_arg # transform into command line option with value
128
145
  #parameter_value=parameter_value.to_s if parameter_value.is_a?(Integer)
129
146
  parameter_value=[parameter_value] unless parameter_value.is_a?(Array)
130
147
  # if transfer_spec value is an array, applies option many times
131
- parameter_value.each{|v|add_command_line_options([switch_name(param_name,options),v])}
148
+ parameter_value.each{|v|add_command_line_options([options[:option_switch],v])}
132
149
  else
133
150
  raise "Error"
134
151
  end
@@ -6,6 +6,7 @@ module Aspera
6
6
  class CosNode < Rest
7
7
  attr_reader :add_ts
8
8
  IBM_CLOUD_TOKEN_URL='https://iam.cloud.ibm.com/identity'
9
+ TOKEN_FIELD='delegated_refresh_token'
9
10
  def initialize(bucket_name,storage_endpoint,instance_id,api_key,auth_url=IBM_CLOUD_TOKEN_URL)
10
11
  @auth_url=auth_url
11
12
  @api_key=api_key
@@ -32,7 +33,7 @@ module Aspera
32
33
  # prepare transfer spec addition
33
34
  @add_ts={'tags'=>{'aspera'=>{'node'=>{'storage_credentials'=>{
34
35
  'type' => 'token',
35
- 'token' => {'delegated_refresh_token'=>nil}
36
+ 'token' => {TOKEN_FIELD=>nil}
36
37
  }}}}}
37
38
  generate_token
38
39
  end
@@ -45,10 +46,10 @@ module Aspera
45
46
  :base_url => @auth_url,
46
47
  :grant => :delegated_refresh,
47
48
  :api_key => @api_key,
48
- :token_field=> 'delegated_refresh_token'
49
+ :token_field=> TOKEN_FIELD
49
50
  })
50
51
  # get delagated token to be placed in rest call header and in transfer tags
51
- @add_ts['tags']['aspera']['node']['storage_credentials']['token']['delegated_refresh_token']=delegated_oauth.get_authorization().gsub(/^Bearer /,'')
52
+ @add_ts['tags']['aspera']['node']['storage_credentials']['token'][TOKEN_FIELD]=delegated_oauth.get_authorization().gsub(/^Bearer /,'')
52
53
  @params[:headers]={'X-Aspera-Storage-Credentials'=>JSON.generate(@add_ts['tags']['aspera']['node']['storage_credentials'])}
53
54
  end
54
55
  end
@@ -4,15 +4,14 @@ require 'aspera/log'
4
4
  require 'aspera/rest'
5
5
  require 'websocket-client-simple'
6
6
  require 'securerandom'
7
- require 'openssl'
8
7
  require 'base64'
9
8
  require 'json'
10
- require 'uri'
11
9
 
12
10
  # ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
11
+ # https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
13
12
  module Aspera
14
13
  module Fasp
15
- # executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
14
+ # start a transfer using Aspera HTTP Gateway, using web socket session
16
15
  class HttpGW < Manager
17
16
  # message returned by HTTP GW in case of success
18
17
  OK_MESSAGE='end upload'
@@ -25,20 +24,36 @@ module Aspera
25
24
  end
26
25
 
27
26
  def upload(transfer_spec)
28
- # precalculate size
27
+ # total size of all files
29
28
  total_size=0
30
- # currently, files are sent flat
31
- source_path=[]
29
+ # we need to keep track of actual file path because transfer spec is modified to be sent in web socket
30
+ source_paths=[]
31
+ # get source root or nil
32
+ source_root = (transfer_spec.has_key?('source_root') and !transfer_spec['source_root'].empty?) ? transfer_spec['source_root'] : nil
33
+ # source root is ignored by GW, used only here
34
+ transfer_spec.delete('source_root')
35
+ # compute total size of files to upload (for progress)
36
+ # modify transfer spec to be suitable for GW
32
37
  transfer_spec['paths'].each do |item|
33
- filepath=item['source']
34
- item['source']=item['destination']=File.basename(filepath)
35
- total_size+=item['file_size']=File.size(filepath)
36
- source_path.push(filepath)
38
+ # save actual file location to be able read contents later
39
+ full_src_filepath=item['source']
40
+ # add source root if needed
41
+ full_src_filepath=File.join(source_root,full_src_filepath) unless source_root.nil?
42
+ # GW expects a simple file name in 'source' but if user wants to change the name, we take it
43
+ item['source']=File.basename(item['destination'].nil? ? item['source'] : item['destination'])
44
+ item['file_size']=File.size(full_src_filepath)
45
+ total_size+=item['file_size']
46
+ # save so that we can actually read the file later
47
+ source_paths.push(full_src_filepath)
37
48
  end
49
+
38
50
  session_id=SecureRandom.uuid
39
51
  ws=::WebSocket::Client::Simple::Client.new
52
+ # error message if any in callback
40
53
  error=nil
54
+ # number of files totally sent
41
55
  received=0
56
+ # setup callbacks on websocket
42
57
  ws.on :message do |msg|
43
58
  Log.log.info("ws: message: #{msg.data}")
44
59
  message=msg.data
@@ -64,44 +79,49 @@ module Aspera
64
79
  end
65
80
  # open web socket to end point
66
81
  ws.connect("#{@gw_api.params[:base_url]}/upload")
82
+ # async wait ready
67
83
  while !ws.open? and error.nil? do
68
84
  Log.log.info("ws: wait")
69
85
  sleep(0.2)
70
86
  end
87
+ # notify progress bar
71
88
  notify_begin(session_id,total_size)
89
+ # first step send transfer spec
90
+ Log.dump(:ws_spec,transfer_spec)
72
91
  ws_send(ws,:transfer_spec,transfer_spec)
73
92
  # current file index
74
- filenum=0
93
+ file_index=0
75
94
  # aggregate size sent
76
95
  sent_bytes=0
77
96
  # last progress event
78
- lastevent=Time.now-1
97
+ lastevent=nil
79
98
  transfer_spec['paths'].each do |item|
80
- # TODO: on destination write same path?
81
- destination_path=item['source']
82
99
  # TODO: get mime type?
83
100
  file_mime_type=''
84
- total=item['file_size']
101
+ file_size=item['file_size']
102
+ file_name=File.basename(item[item['destination'].nil? ? 'source' : 'destination'])
85
103
  # compute total number of slices
86
- numslices=1+(total-1)/@upload_chunksize
87
- # current slice index
88
- slicenum=0
89
- File.open(source_path[filenum]) do |file|
104
+ numslices=1+(file_size-1)/@upload_chunksize
105
+ File.open(source_paths[file_index]) do |file|
106
+ # current slice index
107
+ slicenum=0
90
108
  while !file.eof? do
91
109
  data=file.read(@upload_chunksize)
92
110
  slice_data={
93
- name: destination_path,
111
+ name: file_name,
94
112
  type: file_mime_type,
95
- size: total,
113
+ size: file_size,
96
114
  data: Base64.strict_encode64(data),
97
115
  slice: slicenum,
98
116
  total_slices: numslices,
99
- fileIndex: filenum
117
+ fileIndex: file_index
100
118
  }
119
+ # log without data
120
+ Log.dump(:slide_data,slice_data.keys.inject({}){|m,i|m[i]=i.eql?(:data)?'base64 data':slice_data[i];m}) if slicenum.eql?(0)
101
121
  ws_send(ws,:slice_upload, slice_data)
102
122
  sent_bytes+=data.length
103
123
  currenttime=Time.now
104
- if (currenttime-lastevent)>UPLOAD_REFRESH_SEC
124
+ if lastevent.nil? or (currenttime-lastevent)>UPLOAD_REFRESH_SEC
105
125
  notify_progress(session_id,sent_bytes)
106
126
  lastevent=currenttime
107
127
  end
@@ -109,7 +129,7 @@ module Aspera
109
129
  raise error unless error.nil?
110
130
  end
111
131
  end
112
- filenum+=1
132
+ file_index+=1
113
133
  end
114
134
  ws.close
115
135
  notify_end(session_id)
@@ -150,7 +170,8 @@ module Aspera
150
170
  raise "GW URL must be set" unless !@gw_api.nil?
151
171
  raise "option: must be hash (or nil)" unless options.is_a?(Hash)
152
172
  raise "paths: must be Array" unless transfer_spec['paths'].is_a?(Array)
153
- raise "on token based transfer is supported in GW" unless transfer_spec['token'].is_a?(String)
173
+ raise "only token based transfer is supported in GW" unless transfer_spec['token'].is_a?(String)
174
+ Log.dump(:user_spec,transfer_spec)
154
175
  transfer_spec['authentication']||='token'
155
176
  case transfer_spec['direction']
156
177
  when 'send'
@@ -188,6 +209,6 @@ module Aspera
188
209
  @upload_chunksize=128000 # TODO: configurable ?
189
210
  end
190
211
 
191
- end # LocalHttp
212
+ end # HttpGW
192
213
  end
193
214
  end
@@ -17,15 +17,14 @@ require 'securerandom'
17
17
 
18
18
  module Aspera
19
19
  module Fasp
20
- # (public) default transfer username for access key based transfers
21
- ACCESS_KEY_TRANSFER_USER='xfer'
22
20
  # executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
23
21
  class Local < Manager
24
- # options for initialize
22
+ # options for initialize (same as values in option transfer_info)
25
23
  DEFAULT_OPTIONS = {
26
24
  :spawn_timeout_sec => 3,
27
25
  :spawn_delay_sec => 2,
28
26
  :wss => false,
27
+ :multi_incr_udp => true,
29
28
  :resume => {}
30
29
  }
31
30
  DEFAULT_UDP_PORT=33001
@@ -64,21 +63,27 @@ module Aspera
64
63
  transfer_spec['EX_ssh_key_paths'] = Installation.instance.bypass_keys
65
64
  end
66
65
 
67
- # TODO: check if changing fasp(UDP) port is really necessary, not clear from doc
68
- # compute this before using transfer spec, even if the var is not used in single session
69
- multi_session_udp_port_base=DEFAULT_UDP_PORT
70
- multi_session_number=0
66
+ # Compute this before using transfer spec because it potentially modifies the transfer spec
67
+ # (even if the var is not used in single session)
68
+ multi_session_info=nil
71
69
  if transfer_spec.has_key?('multi_session')
72
- multi_session_number=transfer_spec['multi_session'].to_i
73
- if multi_session_number < 0
70
+ multi_session_info={
71
+ count: transfer_spec['multi_session'].to_i,
72
+ }
73
+ # Managed by multi-session, so delete from transfer spec
74
+ transfer_spec.delete('multi_session')
75
+ if multi_session_info[:count] < 0
74
76
  Log.log.error("multi_session(#{transfer_spec['multi_session']}) shall be integer >= 0")
75
- multi_session_number = 0
76
- end
77
- if multi_session_number > 0
78
- # managed here, so delete from transfer spec
79
- transfer_spec.delete('multi_session')
80
- if transfer_spec.has_key?('fasp_port')
81
- multi_session_udp_port_base=transfer_spec['fasp_port']
77
+ multi_session_info = nil
78
+ elsif multi_session_info[:count].eql?(0)
79
+ Log.log.debug("multi_session count is zero: no multisession")
80
+ multi_session_info = nil
81
+ else # multi_session_info[:count] > 0
82
+ # if option not true: keep default udp port for all sessions
83
+ if @options[:multi_incr_udp]
84
+ # override if specified, else use default value
85
+ multi_session_info[:udp_base]=transfer_spec.has_key?('fasp_port') ? transfer_spec['fasp_port'] : DEFAULT_UDP_PORT
86
+ # delete from original transfer spec, as we will increment values
82
87
  transfer_spec.delete('fasp_port')
83
88
  end
84
89
  end
@@ -111,22 +116,23 @@ module Aspera
111
116
  :options => job_options # [Hash]
112
117
  }
113
118
 
114
- if multi_session_number <= 1
119
+ if multi_session_info.nil?
115
120
  Log.log.debug('Starting single session thread')
116
121
  # single session for transfer : simple
117
122
  session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
118
123
  xfer_job[:sessions].push(session)
119
124
  else
120
125
  Log.log.debug('Starting multi session threads')
121
- 1.upto(multi_session_number) do |i|
126
+ 1.upto(multi_session_info[:count]) do |i|
127
+ # do not delay the first session
122
128
  sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
123
129
  # do deep copy (each thread has its own copy because it is modified here below and in thread)
124
130
  this_session=session.clone()
125
131
  this_session[:env_args]=this_session[:env_args].clone()
126
132
  this_session[:env_args][:args]=this_session[:env_args][:args].clone()
127
- this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_number}")
128
- # necessary only if server is not linux, i.e. server does not support port re-use
129
- this_session[:env_args][:args].unshift('-O',"#{multi_session_udp_port_base+i-1}")
133
+ this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
134
+ # option: increment (default as per ascp manual) or not (cluster on other side ?)
135
+ this_session[:env_args][:args].unshift('-O',"#{multi_session_info[:udp_base]+i-1}") if @options[:multi_incr_udp]
130
136
  this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
131
137
  xfer_job[:sessions].push(this_session)
132
138
  end
@@ -260,6 +266,7 @@ module Aspera
260
266
  Log.log.error('need to regenerate token'.red)
261
267
  if session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
262
268
  # regenerate token here, expired, or error on it
269
+ # Note: in multi-session, each session will have a different one.
263
270
  env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
264
271
  end
265
272
  end
@@ -323,7 +330,7 @@ module Aspera
323
330
 
324
331
  private
325
332
 
326
- # @param options : keys(symbol): wss, resume
333
+ # @param options : keys(symbol): see DEFAULT_OPTIONS
327
334
  def initialize(options=nil)
328
335
  super()
329
336
  # by default no interactive progress bar
@@ -332,7 +339,7 @@ module Aspera
332
339
  @jobs={}
333
340
  # mutex protects global data accessed by threads
334
341
  @mutex=Mutex.new
335
- # manage options
342
+ # set default options and override if specified
336
343
  @options=DEFAULT_OPTIONS.clone
337
344
  if !options.nil?
338
345
  raise "expecting Hash (or nil), but have #{options.class}" unless options.is_a?(Hash)
@@ -340,7 +347,7 @@ module Aspera
340
347
  if DEFAULT_OPTIONS.has_key?(k)
341
348
  @options[k]=v
342
349
  else
343
- raise "unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map{|i|i.to_s}.join(",")}"
350
+ raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map{|i|i.to_s}.join(",")}"
344
351
  end
345
352
  end
346
353
  end
@@ -72,6 +72,9 @@ module Aspera
72
72
  # start_transfer(transfer_spec,options) : start and wait for completion
73
73
  # wait_for_transfers_completion : wait for termination of all transfers, @return list of : :success or error message
74
74
  # optional: shutdown
75
+
76
+ # This checks the validity of the value returned by wait_for_transfers_completion
77
+ # it must be a list of :success or exception
75
78
  def self.validate_status_list(statuses)
76
79
  raise "internal error: bad statuses type: #{statuses.class}" unless statuses.is_a?(Array)
77
80
  raise "internal error: bad statuses content: #{statuses}" unless statuses.select{|i|!i.eql?(:success) and !i.is_a?(StandardError)}.empty?
@@ -7,9 +7,12 @@ module Aspera
7
7
  # this singleton class is used by the CLI to provide a common interface to start a transfer
8
8
  # before using it, the use must set the `node_api` member.
9
9
  class Node < Manager
10
- def initialize(node_api)
10
+ # option include: root_id if the node is an access key
11
+ attr_writer :options
12
+ def initialize(node_api,options={})
11
13
  super()
12
14
  @node_api=node_api
15
+ @options=options
13
16
  # TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
14
17
  @transfer_id=nil
15
18
  end
@@ -32,6 +35,25 @@ module Aspera
32
35
 
33
36
  # generic method
34
37
  def start_transfer(transfer_spec,options=nil)
38
+ # add root id if access key
39
+ if @options.has_key?(:root_id)
40
+ case transfer_spec['direction']
41
+ when 'send';transfer_spec['source_root_id']=@options[:root_id]
42
+ when 'receive';transfer_spec['destination_root_id']=@options[:root_id]
43
+ else raise "unexpected direction in ts: #{transfer_spec['direction']}"
44
+ end
45
+ end
46
+ # manage special additional parameter
47
+ if transfer_spec.has_key?('EX_ssh_key_paths') and transfer_spec['EX_ssh_key_paths'].is_a?(Array) and !transfer_spec['EX_ssh_key_paths'].empty?
48
+ # not standard, so place standard field
49
+ if transfer_spec.has_key?('ssh_private_key')
50
+ Log.log.warn('Both ssh_private_key and EX_ssh_key_paths are present, using ssh_private_key')
51
+ else
52
+ Log.log.warn('EX_ssh_key_paths has multiple keys, using first one only') unless transfer_spec['EX_ssh_key_paths'].length.eql?(1)
53
+ transfer_spec['ssh_private_key']=File.read(transfer_spec['EX_ssh_key_paths'].first)
54
+ transfer_spec.delete('EX_ssh_key_paths')
55
+ end
56
+ end
35
57
  if transfer_spec['tags'].is_a?(Hash) and transfer_spec['tags']['aspera'].is_a?(Hash)
36
58
  transfer_spec['tags']['aspera']['xfer_retry']||=150
37
59
  end