aspera-cli 4.3.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.
@@ -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