aspera-cli 4.4.0 → 4.5.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.
- checksums.yaml +4 -4
- data/README.md +1042 -787
- data/bin/ascli +1 -1
- data/bin/asession +3 -5
- data/docs/Makefile +4 -7
- data/docs/README.erb.md +988 -740
- data/examples/faspex4.rb +4 -6
- data/examples/transfer.rb +2 -2
- data/lib/aspera/aoc.rb +139 -118
- data/lib/aspera/cli/listener/progress_multi.rb +5 -5
- data/lib/aspera/cli/main.rb +64 -34
- data/lib/aspera/cli/manager.rb +19 -20
- data/lib/aspera/cli/plugin.rb +9 -1
- data/lib/aspera/cli/plugins/aoc.rb +156 -143
- data/lib/aspera/cli/plugins/ats.rb +11 -10
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +236 -112
- data/lib/aspera/cli/plugins/faspex.rb +29 -7
- data/lib/aspera/cli/plugins/faspex5.rb +21 -8
- data/lib/aspera/cli/plugins/node.rb +21 -9
- data/lib/aspera/cli/plugins/orchestrator.rb +5 -3
- data/lib/aspera/cli/plugins/preview.rb +2 -2
- data/lib/aspera/cli/plugins/server.rb +3 -3
- data/lib/aspera/cli/plugins/shares.rb +17 -0
- data/lib/aspera/cli/transfer_agent.rb +47 -85
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +4 -4
- data/lib/aspera/fasp/{manager.rb → agent_base.rb} +7 -6
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +46 -39
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +14 -17
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +4 -4
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +25 -8
- data/lib/aspera/fasp/agent_trsdk.rb +106 -0
- data/lib/aspera/fasp/default.rb +17 -0
- data/lib/aspera/fasp/installation.rb +64 -48
- data/lib/aspera/fasp/parameters.rb +7 -3
- data/lib/aspera/faspex_gw.rb +6 -6
- data/lib/aspera/keychain/encrypted_hash.rb +120 -0
- data/lib/aspera/keychain/macos_security.rb +94 -0
- data/lib/aspera/log.rb +45 -32
- data/lib/aspera/node.rb +3 -6
- data/lib/aspera/rest.rb +65 -49
- metadata +68 -27
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/secrets.rb +0 -20
data/examples/faspex4.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# find Faspex API here: https://developer.ibm.com/apis/catalog/?search=faspex
|
3
3
|
# this example makes use of class Aspera::Rest for REST calls, alternatively class RestClient of gem rest-client could be used
|
4
|
-
# this example makes use of class Aspera::Fasp::
|
4
|
+
# this example makes use of class Aspera::Fasp::AgentDirect for transfers, alternatively the official "Transfer SDK" could be used
|
5
5
|
# Aspera SDK can be downloaded with: `ascli conf ascp install` , it installs in $HOME/.aspera/ascli/sdk
|
6
6
|
require 'aspera/rest'
|
7
7
|
require 'aspera/log'
|
8
|
-
require 'aspera/fasp/
|
8
|
+
require 'aspera/fasp/agent_direct'
|
9
9
|
|
10
10
|
tmpdir=ENV['tmp']||Dir.tmpdir || '.'
|
11
11
|
|
@@ -59,10 +59,8 @@ pkg_created=api_v3.create('send',package_create_params)[:data]
|
|
59
59
|
transfer_spec=pkg_created['xfer_sessions'].first
|
60
60
|
# set paths of files to send
|
61
61
|
transfer_spec['paths']=[{'source'=>file_to_send}]
|
62
|
-
# get
|
63
|
-
transfer_client=Aspera::Fasp::
|
64
|
-
# disable ascp output on stdout (optional)
|
65
|
-
transfer_client.quiet=true
|
62
|
+
# get local agent (ascp), disable ascp output on stdout to not mix with JSON events
|
63
|
+
transfer_client=Aspera::Fasp::AgentDirect.new({quiet: true})
|
66
64
|
# start transfer (asynchronous)
|
67
65
|
job_id=transfer_client.start_transfer(transfer_spec)
|
68
66
|
# wait for all transfer completion (for the example)
|
data/examples/transfer.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# Example: transfer a file using one of the provided transfer agents
|
3
3
|
# location of ascp can be specified with env var "ascp"
|
4
4
|
# temp folder can be specified with env var "tmp"
|
5
|
-
require 'aspera/fasp/
|
5
|
+
require 'aspera/fasp/agent_direct'
|
6
6
|
require 'aspera/fasp/listener'
|
7
7
|
require 'aspera/fasp/installation'
|
8
8
|
require 'aspera/log'
|
@@ -39,7 +39,7 @@ Aspera::Fasp::Installation.instance.ascp_path=ENV['ascp'] if ENV.has_key?('ascp'
|
|
39
39
|
#
|
40
40
|
|
41
41
|
# get FASP Manager singleton based on above ascp location
|
42
|
-
fasp_manager=Aspera::Fasp::
|
42
|
+
fasp_manager=Aspera::Fasp::AgentDirect.new
|
43
43
|
|
44
44
|
# Note that it would also be possible to start transfers using other agents
|
45
45
|
#require 'aspera/fasp/connect'
|
data/lib/aspera/aoc.rb
CHANGED
@@ -2,7 +2,7 @@ require 'aspera/log'
|
|
2
2
|
require 'aspera/rest'
|
3
3
|
require 'aspera/hash_ext'
|
4
4
|
require 'aspera/data_repository'
|
5
|
-
require 'aspera/
|
5
|
+
require 'aspera/fasp/default'
|
6
6
|
require 'base64'
|
7
7
|
|
8
8
|
module Aspera
|
@@ -18,19 +18,20 @@ module Aspera
|
|
18
18
|
# to avoid infinite loop in pub link redirection
|
19
19
|
MAX_REDIRECT=10
|
20
20
|
CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
|
21
|
+
# index offset in data repository of client app
|
21
22
|
DATA_REPO_INDEX_START = 4
|
23
|
+
# cookie prefix so that console can decode identity
|
24
|
+
COOKIE_PREFIX='aspera.aoc'
|
22
25
|
|
23
26
|
# path in URL of public links
|
24
|
-
|
27
|
+
PUBLIC_LINK_PATHS=['/packages/public/receive','/packages/public/send','/files/public']
|
25
28
|
JWT_AUDIENCE='https://api.asperafiles.com/api/v1/oauth2/token'
|
26
29
|
OAUTH_API_SUBPATH='api/v1/oauth2'
|
27
|
-
|
28
|
-
|
29
|
-
'ssh_port' => Node::SSH_PORT_DEFAULT,
|
30
|
-
'fasp_port' => Node::UDP_PORT_DEFAULT
|
31
|
-
}
|
30
|
+
# minimum fields for user info if retrieval fails
|
31
|
+
USER_INFO_FIELDS_MIN=['name','email','id','default_workspace_id','organization_id']
|
32
32
|
|
33
|
-
private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:
|
33
|
+
private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:PUBLIC_LINK_PATHS,:JWT_AUDIENCE,
|
34
|
+
:OAUTH_API_SUBPATH,:COOKIE_PREFIX,:USER_INFO_FIELDS_MIN
|
34
35
|
|
35
36
|
public
|
36
37
|
# various API scopes supported
|
@@ -45,84 +46,119 @@ module Aspera
|
|
45
46
|
FILES_APP='files'
|
46
47
|
PACKAGES_APP='packages'
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
49
|
+
# class static methods
|
50
|
+
class << self
|
51
51
|
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
52
|
-
|
53
|
-
|
52
|
+
def get_client_info(client_name=CLIENT_APPS.first)
|
53
|
+
client_index=CLIENT_APPS.index(client_name)
|
54
|
+
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
55
|
+
return client_name,Base64.urlsafe_encode64(DataRepository.instance.get_bin(DATA_REPO_INDEX_START+client_index))
|
56
|
+
end
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
58
|
+
# @param url of AoC instance
|
59
|
+
# @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
|
60
|
+
def parse_url(aoc_org_url)
|
61
|
+
uri=URI.parse(aoc_org_url.gsub(/\/+$/,''))
|
62
|
+
instance_fqdn=uri.host
|
63
|
+
Log.log.debug("instance_fqdn=#{instance_fqdn}")
|
64
|
+
raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
|
65
|
+
organization,instance_domain=instance_fqdn.split('.',2)
|
66
|
+
Log.log.debug("instance_domain=#{instance_domain}")
|
67
|
+
Log.log.debug("organization=#{organization}")
|
68
|
+
raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
|
69
|
+
return organization,instance_domain
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
# base API url depends on domain, which could be "qa.xxx"
|
73
|
+
def api_base_url(api_domain=PROD_DOMAIN)
|
74
|
+
return "https://api.#{api_domain}"
|
75
|
+
end
|
73
76
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
def metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
|
78
|
+
return Rest.new({
|
79
|
+
:base_url => "#{api_base_url(api_domain)}/metering/v1",
|
80
|
+
:headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
|
81
|
+
})
|
82
|
+
end
|
80
83
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
84
|
+
# node API scopes
|
85
|
+
def node_scope(access_key,scope)
|
86
|
+
return 'node.'+access_key+':'+scope
|
87
|
+
end
|
85
88
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
+
def set_use_default_ports(val)
|
90
|
+
@@use_standard_ports=val
|
91
|
+
end
|
89
92
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
93
|
+
# check option "link"
|
94
|
+
# if present try to get token value (resolve redirection if short links used)
|
95
|
+
# then set options url/token/auth
|
96
|
+
def resolve_pub_link(rest_opts,public_link_url)
|
97
|
+
return if public_link_url.nil?
|
98
|
+
# set to token if available after redirection
|
99
|
+
url_param_token_pair=nil
|
100
|
+
redirect_count=0
|
101
|
+
loop do
|
102
|
+
uri=URI.parse(public_link_url)
|
103
|
+
if PUBLIC_LINK_PATHS.include?(uri.path)
|
104
|
+
url_param_token_pair=URI::decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
|
105
|
+
if url_param_token_pair.nil?
|
106
|
+
raise ArgumentError,"link option must be URL with 'token' parameter"
|
107
|
+
end
|
108
|
+
# ok we get it !
|
109
|
+
rest_opts[:org_url]='https://'+uri.host
|
110
|
+
rest_opts[:auth][:grant]=:url_token
|
111
|
+
rest_opts[:auth][:url_token]=url_param_token_pair.last
|
112
|
+
return
|
104
113
|
end
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
114
|
+
Log.log.debug("no expected format: #{public_link_url}")
|
115
|
+
raise "exceeded max redirection: #{MAX_REDIRECT}" if redirect_count > MAX_REDIRECT
|
116
|
+
r = Net::HTTP.get_response(uri)
|
117
|
+
if r.code.start_with?("3")
|
118
|
+
public_link_url = r['location']
|
119
|
+
raise "no location in redirection" if public_link_url.nil?
|
120
|
+
Log.log.debug("redirect to: #{public_link_url}")
|
121
|
+
else
|
122
|
+
# not a redirection
|
123
|
+
raise ArgumentError,'link option must be redirect or have token parameter'
|
124
|
+
end
|
125
|
+
end # loop
|
126
|
+
|
127
|
+
raise RuntimeError,'too many redirections'
|
128
|
+
end
|
129
|
+
|
130
|
+
# additional transfer spec (tags) for package information
|
131
|
+
def package_tags(package_info,operation)
|
132
|
+
return {'tags'=>{'aspera'=>{'files'=>{
|
133
|
+
'package_id' => package_info['id'],
|
134
|
+
'package_name' => package_info['name'],
|
135
|
+
'package_operation' => operation
|
136
|
+
}}}}
|
137
|
+
end
|
138
|
+
|
139
|
+
# add details to show in analytics
|
140
|
+
def analytics_ts(app,direction,ws_id,ws_name)
|
141
|
+
# translate transfer to operation
|
142
|
+
operation=case direction
|
143
|
+
when 'send'; 'upload'
|
144
|
+
when 'receive'; 'download'
|
145
|
+
else raise "ERROR: unexpected value: #{direction}"
|
121
146
|
end
|
122
|
-
end # loop
|
123
147
|
|
124
|
-
|
125
|
-
|
148
|
+
return {
|
149
|
+
'tags' => {
|
150
|
+
'aspera' => {
|
151
|
+
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
152
|
+
'files' => {
|
153
|
+
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
154
|
+
'workspace_name' => ws_name, # activity tracking
|
155
|
+
'workspace_id' => ws_id,
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
end
|
161
|
+
end # static methods
|
126
162
|
|
127
163
|
# @param :link,:url,:auth,:client_id,:client_secret,:scope,:redirect_uri,:private_key,:username,:subpath,:password (for pub link)
|
128
164
|
def initialize(opt)
|
@@ -130,6 +166,7 @@ module Aspera
|
|
130
166
|
# key: access key
|
131
167
|
# value: associated secret
|
132
168
|
@key_chain=nil
|
169
|
+
@user_info=nil
|
133
170
|
|
134
171
|
# init rest params
|
135
172
|
aoc_rest_p={:auth=>{:type =>:oauth2}}
|
@@ -197,50 +234,28 @@ module Aspera
|
|
197
234
|
|
198
235
|
def key_chain=(keychain)
|
199
236
|
raise "keychain already set" unless @key_chain.nil?
|
237
|
+
raise "keychain must have get_secret" unless keychain.respond_to?(:get_secret)
|
200
238
|
@key_chain=keychain
|
201
239
|
nil
|
202
240
|
end
|
203
241
|
|
204
|
-
#
|
205
|
-
def
|
206
|
-
|
207
|
-
'
|
208
|
-
'
|
209
|
-
|
210
|
-
}
|
211
|
-
end
|
212
|
-
|
213
|
-
# add details to show in analytics
|
214
|
-
def self.analytics_ts(app,direction,ws_id,ws_name)
|
215
|
-
# translate transfer to operation
|
216
|
-
operation=case direction
|
217
|
-
when 'send'; 'upload'
|
218
|
-
when 'receive'; 'download'
|
219
|
-
else raise "ERROR: unexpected value: #{direction}"
|
242
|
+
# cached user information
|
243
|
+
def user_info
|
244
|
+
if @user_info.nil?
|
245
|
+
# get our user's default information
|
246
|
+
@user_info=self.read('self')[:data] rescue nil
|
247
|
+
@user_info=USER_INFO_FIELDS_MIN.inject({}){|m,f|m[f]=nil;m} if @user_info.nil?
|
248
|
+
USER_INFO_FIELDS_MIN.each{|f|@user_info[f]='unknown' if @user_info[f].nil?}
|
220
249
|
end
|
221
|
-
|
222
|
-
return {
|
223
|
-
'tags' => {
|
224
|
-
'aspera' => {
|
225
|
-
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
226
|
-
'files' => {
|
227
|
-
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
228
|
-
'workspace_name' => ws_name, # activity tracking
|
229
|
-
'workspace_id' => ws_id,
|
230
|
-
}
|
231
|
-
}
|
232
|
-
}
|
233
|
-
}
|
250
|
+
return @user_info
|
234
251
|
end
|
235
252
|
|
236
253
|
# build ts addon for IBM Aspera Console (cookie)
|
237
|
-
def
|
238
|
-
|
239
|
-
elements.
|
240
|
-
|
241
|
-
return {
|
242
|
-
'cookie'=>elements.join(':')
|
243
|
-
}
|
254
|
+
def console_ts(app)
|
255
|
+
# we are sure that fields are not nil
|
256
|
+
elements=[app,user_info['name'],user_info['email']].map{|e|Base64.strict_encode64(e)}
|
257
|
+
elements.unshift(COOKIE_PREFIX)
|
258
|
+
return {'cookie'=>elements.join(':')}
|
244
259
|
end
|
245
260
|
|
246
261
|
# build "transfer info", 2 elements array with:
|
@@ -248,12 +263,12 @@ module Aspera
|
|
248
263
|
# - source and token regeneration method
|
249
264
|
def tr_spec(app,direction,node_file,ts_add)
|
250
265
|
# prepare the rest end point is used to generate the bearer token
|
251
|
-
|
266
|
+
token_generation_lambda=lambda {|do_refresh|self.oauth_token(scope: self.class.node_scope(node_file[:node_info]['access_key'],SCOPE_NODE_USER), refresh: do_refresh)}
|
252
267
|
# prepare transfer specification
|
253
268
|
# note xfer_id and xfer_retry are set by the transfer agent itself
|
254
269
|
transfer_spec={
|
255
270
|
'direction' => direction,
|
256
|
-
'token' =>
|
271
|
+
'token' => token_generation_lambda.call(false), # first time, use cache
|
257
272
|
'tags' => {
|
258
273
|
'aspera' => {
|
259
274
|
'app' => app,
|
@@ -270,8 +285,14 @@ module Aspera
|
|
270
285
|
}
|
271
286
|
# add remote host info
|
272
287
|
if @@use_standard_ports
|
273
|
-
|
288
|
+
# get default TCP/UDP ports and transfer user
|
289
|
+
transfer_spec.merge!(Fasp::Default::AK_TSPEC_BASE)
|
290
|
+
# by default: same address as node API
|
274
291
|
transfer_spec['remote_host']=node_file[:node_info]['host']
|
292
|
+
# 30 it's necessarily https scheme: webui does not allow anything else
|
293
|
+
if node_file[:node_info]['transfer_url'].is_a?(String) and !node_file[:node_info]['transfer_url'].empty?
|
294
|
+
transfer_spec['remote_host']=URI.parse(node_file[:node_info]['transfer_url']).host
|
295
|
+
end
|
275
296
|
else
|
276
297
|
# retrieve values from API
|
277
298
|
std_t_spec=get_node_api(node_file[:node_info],scope: SCOPE_NODE_USER).create('files/download_setup',{:transfer_requests => [ { :transfer_request => {:paths => [ {"source"=>'/'} ] } } ] } )[:data]['transfer_specs'].first['transfer_spec']
|
@@ -282,7 +303,7 @@ module Aspera
|
|
282
303
|
# additional information for transfer agent
|
283
304
|
source_and_token_generator={
|
284
305
|
:src => :node_gen4,
|
285
|
-
:regenerate_token =>
|
306
|
+
:regenerate_token => token_generation_lambda
|
286
307
|
}
|
287
308
|
return transfer_spec,source_and_token_generator
|
288
309
|
end
|
@@ -292,10 +313,10 @@ module Aspera
|
|
292
313
|
# no scope: requires secret
|
293
314
|
# if secret provided beforehand: use it
|
294
315
|
def get_node_api(node_info,options={})
|
295
|
-
raise "INTERNAL ERROR: method parameters: options must
|
316
|
+
raise "INTERNAL ERROR: method parameters: options must be Hash" unless options.is_a?(Hash)
|
296
317
|
options.keys.each {|k| raise "INTERNAL ERROR: not valid option: #{k}" unless [:scope,:use_secret].include?(k)}
|
297
318
|
# get optional secret unless :use_secret is false (default is true)
|
298
|
-
ak_secret=@key_chain.get_secret(node_info['access_key'],false) if !options.has_key?(:use_secret) or options[:use_secret]
|
319
|
+
ak_secret=@key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if !@key_chain.nil? and ( !options.has_key?(:use_secret) or options[:use_secret] )
|
299
320
|
if ak_secret.nil? and !options.has_key?(:scope)
|
300
321
|
raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}"
|
301
322
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'aspera/fasp/listener'
|
2
|
-
require 'aspera/fasp/
|
2
|
+
require 'aspera/fasp/agent_base'
|
3
3
|
require 'ruby-progressbar'
|
4
4
|
|
5
5
|
module Aspera
|
@@ -43,13 +43,13 @@ module Aspera
|
|
43
43
|
:title => '',
|
44
44
|
:total => nil)
|
45
45
|
end
|
46
|
-
if !data.has_key?(Fasp::
|
47
|
-
Log.log.error("Internal error: no #{Fasp::
|
46
|
+
if !data.has_key?(Fasp::AgentBase::LISTENER_SESSION_ID_S)
|
47
|
+
Log.log.error("Internal error: no #{Fasp::AgentBase::LISTENER_SESSION_ID_S} in event: #{data}")
|
48
48
|
return
|
49
49
|
end
|
50
50
|
newtitle=@sessions.length < 2 ? '' : "multi=#{@sessions.length}"
|
51
51
|
@progress_bar.title=newtitle unless @progress_bar.title.eql?(newtitle)
|
52
|
-
session=@sessions[data[Fasp::
|
52
|
+
session=@sessions[data[Fasp::AgentBase::LISTENER_SESSION_ID_S]]||={
|
53
53
|
cumulative: 0,
|
54
54
|
job_size: 0,
|
55
55
|
current: 0
|
@@ -78,7 +78,7 @@ module Aspera
|
|
78
78
|
# stop event when one file is completed
|
79
79
|
session[:cumulative]=session[:cumulative]+data['size'].to_i
|
80
80
|
when 'DONE' # end of session
|
81
|
-
@sessions.delete(data[Fasp::
|
81
|
+
@sessions.delete(data[Fasp::AgentBase::LISTENER_SESSION_ID_S])
|
82
82
|
update_progress
|
83
83
|
update_total
|
84
84
|
else
|