aspera-cli 4.3.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 +17,6 @@ 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
22
  # options for initialize (same as values in option transfer_info)
@@ -70,10 +70,15 @@ module Aspera
70
70
  Base64.strict_encode64(JSON.generate(v))
71
71
  end
72
72
 
73
+ def self.ts_has_file_list(ts)
74
+ ts.has_key?('EX_ascp_args') and ts['EX_ascp_args'].is_a?(Array) and ['--file-list','--file-pair-list'].any?{|i|ts['EX_ascp_args'].include?(i)}
75
+ end
76
+
73
77
  def initialize(job_spec,options)
74
78
  @job_spec=job_spec
75
79
  @options=options
76
80
  @builder=Aspera::CommandLineBuilder.new(@job_spec,self.class.description)
81
+ Log.log.debug("agent options: #{@options}")
77
82
  end
78
83
 
79
84
  public
@@ -97,7 +102,7 @@ module Aspera
97
102
  @job_spec.delete('source_root') if @job_spec.has_key?('source_root') and @job_spec['source_root'].empty?
98
103
 
99
104
  # use web socket session initiation ?
100
- if @builder.process_param('wss_enabled',:get_value) and @options[:wss]
105
+ if @builder.process_param('wss_enabled',:get_value) and ( @options[:wss] or !@job_spec.has_key?('fasp_port') )
101
106
  # by default use web socket session if available, unless removed by user
102
107
  @builder.add_command_line_options(['--ws-connect'])
103
108
  # TODO: option to give order ssh,ws (legacy http is implied bu ssh)
@@ -122,9 +127,17 @@ module Aspera
122
127
  # destination will be base64 encoded, put before path arguments
123
128
  @builder.add_command_line_options(['--dest64'])
124
129
  end
125
- @builder.params_definition['paths'][:mandatory]=!@job_spec.has_key?('keepalive')
130
+ # paths is mandatory, unless ...
131
+ file_list_provided=self.class.ts_has_file_list(@job_spec)
132
+ @builder.params_definition['paths'][:mandatory]=!@job_spec.has_key?('keepalive') and !file_list_provided
126
133
  paths_array=@builder.process_param('paths',:get_value)
127
- unless paths_array.nil?
134
+ if file_list_provided and ! paths_array.nil?
135
+ Log.log.warn("file list provided both in transfer spec and ascp file list. Keeping file list only.")
136
+ paths_array=nil
137
+ end
138
+ if ! paths_array.nil?
139
+ # it's an array
140
+ raise "paths is empty in transfer spec" if paths_array.empty?
128
141
  # use file list if there is storage defined for it.
129
142
  if @@file_list_folder.nil?
130
143
  # not safe for special characters ? (maybe not, depends on OS)
@@ -1,5 +1,6 @@
1
1
  require 'aspera/log'
2
2
  require 'aspera/aoc'
3
+ require 'aspera/node'
3
4
  require 'aspera/cli/main'
4
5
  require 'webrick'
5
6
  require 'webrick/https'
@@ -73,16 +74,16 @@ module Aspera
73
74
  "xfer_retry" => 3600 } }
74
75
  # this transfer spec is for transfer to AoC
75
76
  faspex_transfer_spec={
76
- 'direction' => 'send',
77
- 'remote_user' => 'xfer',
77
+ 'direction' => 'send',
78
78
  'remote_host' => node_info['host'],
79
- 'ssh_port' => 33001,
80
- 'fasp_port' => 33001,
81
- 'tags' => ts_tags,
82
- 'token' => node_auth_bearer_token,
83
- 'paths' => [{'destination' => '/'}],
84
- 'cookie' => 'unused',
85
- 'create_dir' => true,
79
+ 'remote_user' => Node::ACCESS_KEY_TRANSFER_USER,
80
+ 'ssh_port' => Node::SSH_PORT_DEFAULT,
81
+ 'fasp_port' => Node::UDP_PORT_DEFAULT
82
+ 'tags' => ts_tags,
83
+ 'token' => node_auth_bearer_token,
84
+ 'paths' => [{'destination' => '/'}],
85
+ 'cookie' => 'unused',
86
+ 'create_dir' => true,
86
87
  'rate_policy' => 'fair',
87
88
  'rate_policy_allowed' => 'fixed',
88
89
  'min_rate_cap_kbps' => nil,
data/lib/aspera/node.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'aspera/rest'
2
+ require 'aspera/oauth'
2
3
  require 'aspera/log'
3
4
  require 'zlib'
4
5
  require 'base64'
@@ -9,10 +10,19 @@ module Aspera
9
10
  # permissions
10
11
  ACCESS_LEVELS=['delete','list','mkdir','preview','read','rename','write']
11
12
  MATCH_EXEC_PREFIX='exec:'
13
+ # (public) default transfer username for access key based transfers
14
+ ACCESS_KEY_TRANSFER_USER='xfer'
15
+ SSH_PORT_DEFAULT=33001
16
+ UDP_PORT_DEFAULT=33001
12
17
 
13
18
  # register node special token decoder
14
19
  Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
15
20
 
21
+ def self.set_ak_basic_token(ts,ak,secret)
22
+ raise "ERROR: expected xfer" unless ts['remote_user'].eql?(ACCESS_KEY_TRANSFER_USER)
23
+ ts['token']="Basic #{Base64.strict_encode64("#{ak}:#{secret}")}"
24
+ end
25
+
16
26
  # for access keys: provide expression to match entry in folder
17
27
  # if no prefix: regex
18
28
  # if prefix: ruby code
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aspera-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Laurent Martin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-19 00:00:00.000000000 Z
11
+ date: 2021-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xml-simple
@@ -224,6 +224,7 @@ files:
224
224
  - docs/README.erb.md
225
225
  - docs/README.md
226
226
  - docs/diagrams.txt
227
+ - docs/doc_tools.rb
227
228
  - docs/test_env.conf
228
229
  - examples/aoc.rb
229
230
  - examples/faspex4.rb
@@ -273,7 +274,6 @@ files:
273
274
  - lib/aspera/data/7
274
275
  - lib/aspera/data_repository.rb
275
276
  - lib/aspera/environment.rb
276
- - lib/aspera/fasp/aoc.rb
277
277
  - lib/aspera/fasp/connect.rb
278
278
  - lib/aspera/fasp/error.rb
279
279
  - lib/aspera/fasp/error_info.rb
@@ -1,24 +0,0 @@
1
- require 'aspera/fasp/node'
2
- require 'aspera/log'
3
- require 'aspera/aoc.rb'
4
-
5
- module Aspera
6
- module Fasp
7
- class Aoc < Node
8
- def initialize(aoc_options)
9
- @app=aoc_options[:app] || AoC::FILES_APP
10
- @api_aoc=AoC.new(aoc_options)
11
- Log.log.warn("Under Development")
12
- server_node_file = @api_aoc.resolve_node_file(server_home_node_file,server_folder)
13
- # force node as transfer agent
14
- node_api=Fasp::Node.new(@api_aoc.get_node_api(client_node_file[:node_info],scope: AoC::SCOPE_NODE_USER))
15
- super(node_api)
16
- # additional node to node TS info
17
- @add_ts={
18
- 'remote_access_key' => server_node_file[:node_info]['access_key'],
19
- 'destination_root_id' => server_node_file[:file_id]
20
- }
21
- end
22
- end
23
- end
24
- end