aspera-cli 4.0.0.pre2 → 4.2.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 +761 -210
- data/bin/ascli +2 -0
- data/bin/dascli +13 -0
- data/docs/Makefile +2 -1
- data/docs/README.erb.md +628 -160
- data/docs/test_env.conf +22 -10
- data/docs/transfer_spec.html +1 -1
- data/lib/aspera/aoc.rb +87 -108
- data/lib/aspera/cli/formater.rb +2 -0
- data/lib/aspera/cli/main.rb +48 -45
- data/lib/aspera/cli/manager.rb +19 -6
- data/lib/aspera/cli/plugin.rb +9 -4
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +208 -183
- data/lib/aspera/cli/plugins/ats.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +205 -125
- data/lib/aspera/cli/plugins/console.rb +2 -2
- data/lib/aspera/cli/plugins/faspex.rb +15 -8
- data/lib/aspera/cli/plugins/faspex5.rb +76 -37
- data/lib/aspera/cli/plugins/node.rb +3 -3
- data/lib/aspera/cli/plugins/preview.rb +35 -25
- data/lib/aspera/cli/plugins/server.rb +23 -8
- data/lib/aspera/cli/transfer_agent.rb +7 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -1
- data/lib/aspera/cos_node.rb +33 -28
- data/lib/aspera/environment.rb +15 -4
- data/lib/aspera/fasp/connect.rb +28 -21
- data/lib/aspera/fasp/http_gw.rb +140 -28
- data/lib/aspera/fasp/installation.rb +119 -57
- data/lib/aspera/fasp/local.rb +174 -178
- data/lib/aspera/fasp/manager.rb +12 -0
- data/lib/aspera/fasp/node.rb +4 -4
- data/lib/aspera/fasp/parameters.rb +6 -18
- data/lib/aspera/fasp/resume_policy.rb +13 -12
- data/lib/aspera/log.rb +10 -2
- data/lib/aspera/node.rb +61 -1
- data/lib/aspera/oauth.rb +36 -13
- data/lib/aspera/persistency_folder.rb +9 -4
- data/lib/aspera/preview/file_types.rb +53 -21
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/rest.rb +29 -18
- data/lib/aspera/secrets.rb +20 -0
- data/lib/aspera/temp_file_manager.rb +19 -0
- metadata +40 -22
|
@@ -5,6 +5,7 @@ require 'aspera/data_repository'
|
|
|
5
5
|
require 'xmlsimple'
|
|
6
6
|
require 'zlib'
|
|
7
7
|
require 'base64'
|
|
8
|
+
require 'fileutils'
|
|
8
9
|
|
|
9
10
|
module Aspera
|
|
10
11
|
module Fasp
|
|
@@ -17,14 +18,28 @@ module Aspera
|
|
|
17
18
|
# Installation.instance.ascp_path=""
|
|
18
19
|
class Installation
|
|
19
20
|
include Singleton
|
|
20
|
-
# currently used ascp executable
|
|
21
|
-
attr_accessor :ascp_path
|
|
22
|
-
# location of SDK files
|
|
23
|
-
attr_accessor :folder
|
|
24
21
|
PRODUCT_CONNECT='Aspera Connect'
|
|
25
22
|
PRODUCT_CLI_V1='Aspera CLI'
|
|
26
23
|
PRODUCT_DRIVE='Aspera Drive'
|
|
27
24
|
PRODUCT_ENTSRV='Enterprise Server'
|
|
25
|
+
MAX_REDIRECT_SDK=2
|
|
26
|
+
private_constant :MAX_REDIRECT_SDK
|
|
27
|
+
# set ascp executable path
|
|
28
|
+
def ascp_path=(v)
|
|
29
|
+
@path_to_ascp=v
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# filename for ascp with optional extension (Windows)
|
|
33
|
+
def ascp_filename
|
|
34
|
+
return 'ascp'+Environment.exe_extension
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# location of SDK files
|
|
38
|
+
def folder=(v)
|
|
39
|
+
@sdk_folder=v
|
|
40
|
+
folder_path
|
|
41
|
+
end
|
|
42
|
+
|
|
28
43
|
# find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
|
|
29
44
|
# or select one from installed_products()
|
|
30
45
|
def use_ascp_from_product(product_name)
|
|
@@ -35,33 +50,37 @@ module Aspera
|
|
|
35
50
|
pl=installed_products.select{|i|i[:name].eql?(product_name)}.first
|
|
36
51
|
raise "no such product installed: #{product_name}" if pl.nil?
|
|
37
52
|
end
|
|
38
|
-
|
|
39
|
-
Log.log.debug("ascp_path=#{@
|
|
53
|
+
self.ascp_path=pl[:ascp_path]
|
|
54
|
+
Log.log.debug("ascp_path=#{@path_to_ascp}")
|
|
40
55
|
end
|
|
41
56
|
|
|
42
57
|
# @return the list of installed products in format of product_locations
|
|
43
58
|
def installed_products
|
|
44
59
|
if @found_products.nil?
|
|
45
|
-
|
|
46
|
-
# add
|
|
47
|
-
|
|
60
|
+
scan_locations=product_locations.clone
|
|
61
|
+
# add SDK as first search path
|
|
62
|
+
scan_locations.unshift({
|
|
48
63
|
:expected =>'SDK',
|
|
49
|
-
:app_root
|
|
64
|
+
:app_root =>folder_path,
|
|
50
65
|
:sub_bin =>''
|
|
51
66
|
})
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
# search installed products: with ascp
|
|
68
|
+
@found_products=scan_locations.select! do |item|
|
|
69
|
+
# skip if not main folder
|
|
70
|
+
next false unless Dir.exist?(item[:app_root])
|
|
71
|
+
Log.log.debug("Found #{item[:app_root]}")
|
|
72
|
+
sub_bin = item[:sub_bin] || BIN_SUBFOLDER
|
|
73
|
+
item[:ascp_path]=File.join(item[:app_root],sub_bin,ascp_filename)
|
|
74
|
+
# skip if no ascp
|
|
75
|
+
next false unless File.exist?(item[:ascp_path])
|
|
76
|
+
# read info from product info file if present
|
|
77
|
+
product_info_file="#{item[:app_root]}/#{PRODUCT_INFO}"
|
|
59
78
|
if File.exist?(product_info_file)
|
|
60
|
-
res_s=XmlSimple.xml_in(File.read(product_info_file),{
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
res_s=XmlSimple.xml_in(File.read(product_info_file),{'ForceArray'=>false})
|
|
80
|
+
item[:name]=res_s['name']
|
|
81
|
+
item[:version]=res_s['version']
|
|
63
82
|
else
|
|
64
|
-
|
|
83
|
+
item[:name]=item[:expected]
|
|
65
84
|
end
|
|
66
85
|
true # select this version
|
|
67
86
|
end
|
|
@@ -69,6 +88,7 @@ module Aspera
|
|
|
69
88
|
return @found_products
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
# all ascp files (in SDK)
|
|
72
92
|
FILES=[:ascp,:ascp4,:ssh_bypass_key_dsa,:ssh_bypass_key_rsa,:aspera_license,:aspera_conf,:fallback_cert,:fallback_key]
|
|
73
93
|
|
|
74
94
|
# get path of one resource file of currently activated product
|
|
@@ -76,44 +96,38 @@ module Aspera
|
|
|
76
96
|
def path(k)
|
|
77
97
|
case k
|
|
78
98
|
when :ascp,:ascp4
|
|
79
|
-
use_ascp_from_product(FIRST_FOUND) if @
|
|
80
|
-
file=@
|
|
99
|
+
use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
|
|
100
|
+
file=@path_to_ascp
|
|
81
101
|
# note that there might be a .exe at the end
|
|
82
102
|
file=file.gsub('ascp','ascp4') if k.eql?(:ascp4)
|
|
83
103
|
when :ssh_bypass_key_dsa
|
|
84
|
-
file=File.join(
|
|
104
|
+
file=File.join(folder_path,'aspera_bypass_dsa.pem')
|
|
85
105
|
File.write(file,get_key('dsa',1)) unless File.exist?(file)
|
|
86
106
|
File.chmod(0400,file)
|
|
87
107
|
when :ssh_bypass_key_rsa
|
|
88
|
-
file=File.join(
|
|
108
|
+
file=File.join(folder_path,'aspera_bypass_rsa.pem')
|
|
89
109
|
File.write(file,get_key('rsa',2)) unless File.exist?(file)
|
|
90
110
|
File.chmod(0400,file)
|
|
91
111
|
when :aspera_license
|
|
92
|
-
file=File.join(
|
|
112
|
+
file=File.join(folder_path,'aspera-license')
|
|
93
113
|
File.write(file,Base64.strict_encode64("#{Zlib::Inflate.inflate(DataRepository.instance.get_bin(6))}==SIGNATURE==\n#{Base64.strict_encode64(DataRepository.instance.get_bin(7))}")) unless File.exist?(file)
|
|
94
114
|
File.chmod(0400,file)
|
|
95
115
|
when :aspera_conf
|
|
96
|
-
file=File.join(
|
|
116
|
+
file=File.join(folder_path,'aspera.conf')
|
|
97
117
|
File.write(file,%Q{<?xml version='1.0' encoding='UTF-8'?>
|
|
98
118
|
<CONF version="2">
|
|
99
119
|
<default>
|
|
100
120
|
<file_system>
|
|
101
|
-
<storage_rc>
|
|
102
|
-
<adaptive>
|
|
103
|
-
true
|
|
104
|
-
</adaptive>
|
|
105
|
-
</storage_rc>
|
|
106
121
|
<resume_suffix>.aspera-ckpt</resume_suffix>
|
|
107
122
|
<partial_file_suffix>.partial</partial_file_suffix>
|
|
108
|
-
<replace_illegal_chars>_</replace_illegal_chars>
|
|
109
123
|
</file_system>
|
|
110
124
|
</default>
|
|
111
125
|
</CONF>
|
|
112
126
|
}) unless File.exist?(file)
|
|
113
127
|
File.chmod(0400,file)
|
|
114
128
|
when :fallback_cert,:fallback_key
|
|
115
|
-
file_key=File.join(
|
|
116
|
-
file_cert=File.join(
|
|
129
|
+
file_key=File.join(folder_path,'aspera_fallback_key.pem')
|
|
130
|
+
file_cert=File.join(folder_path,'aspera_fallback_cert.pem')
|
|
117
131
|
if !File.exist?(file_key) or !File.exist?(file_cert)
|
|
118
132
|
require 'openssl'
|
|
119
133
|
# create new self signed certificate for http fallback
|
|
@@ -139,7 +153,7 @@ module Aspera
|
|
|
139
153
|
return file
|
|
140
154
|
end
|
|
141
155
|
|
|
142
|
-
# @
|
|
156
|
+
# @return the file path of local connect where API's URI can be read
|
|
143
157
|
def connect_uri
|
|
144
158
|
connect=get_product_folders(PRODUCT_CONNECT)
|
|
145
159
|
folder=File.join(connect[:run_root],VARRUN_SUBFOLDER)
|
|
@@ -168,23 +182,67 @@ module Aspera
|
|
|
168
182
|
return [:ssh_bypass_key_dsa,:ssh_bypass_key_rsa].map{|i|Installation.instance.path(i)}
|
|
169
183
|
end
|
|
170
184
|
|
|
171
|
-
|
|
185
|
+
# Check that specified path is ascp and get version
|
|
186
|
+
def get_ascp_version(ascp_path)
|
|
187
|
+
raise "File basename of #{ascp_path} must be #{ascp_filename}" unless File.basename(ascp_path).eql?(ascp_filename)
|
|
188
|
+
ascp_version='n/a'
|
|
189
|
+
raise "error in sdk: no ascp included" if ascp_path.nil?
|
|
190
|
+
cmd_out=%x{"#{ascp_path}" -A}
|
|
191
|
+
raise "An error occured when testing #{ascp_filename}: #{cmd_out}" unless $? == 0
|
|
192
|
+
# get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
|
|
193
|
+
m=cmd_out.match(/ascp version (.*)/)
|
|
194
|
+
ascp_version=m[1] unless m.nil?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# download aspera SDK or use local file
|
|
198
|
+
# extracts ascp binary for current system architecture
|
|
199
|
+
# @return ascp version (from execution)
|
|
200
|
+
def install_sdk(sdk_url)
|
|
172
201
|
require 'zip'
|
|
173
202
|
sdk_zip_path=File.join(Dir.tmpdir,'sdk.zip')
|
|
174
|
-
|
|
203
|
+
if sdk_url.start_with?('file:')
|
|
204
|
+
# require specific file scheme: the path part is "relative", or absolute if there are 4 slash
|
|
205
|
+
raise 'use format: file:///<path>' unless sdk_url.start_with?('file:///')
|
|
206
|
+
sdk_zip_path=sdk_url.gsub(%r{^file:///},'')
|
|
207
|
+
else
|
|
208
|
+
redirect_remain=MAX_REDIRECT_SDK
|
|
209
|
+
begin
|
|
210
|
+
Aspera::Rest.new(base_url: sdk_url).call(operation: 'GET',save_to_file: sdk_zip_path)
|
|
211
|
+
rescue Aspera::RestCallError => e
|
|
212
|
+
if e.response.is_a?(Net::HTTPRedirection)
|
|
213
|
+
if redirect_remain > 0
|
|
214
|
+
redirect_remain-=1
|
|
215
|
+
sdk_url=e.response['location']
|
|
216
|
+
retry
|
|
217
|
+
else
|
|
218
|
+
raise "Too many redirect"
|
|
219
|
+
end
|
|
220
|
+
else
|
|
221
|
+
raise e
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
# SDK is organized by architecture
|
|
175
226
|
filter="/#{Environment.architecture}/"
|
|
176
227
|
ascp_path=nil
|
|
228
|
+
sdk_path=folder_path
|
|
229
|
+
# rename old install
|
|
230
|
+
if File.exist?(File.join(sdk_path,ascp_filename))
|
|
231
|
+
Log.log.warn("Previous install exists, renaming.")
|
|
232
|
+
File.rename(sdk_path,"#{sdk_path}.#{Time.now.strftime("%Y%m%d%H%M%S")}")
|
|
233
|
+
end
|
|
177
234
|
# first ensure license file is here so that ascp invokation for version works
|
|
178
235
|
self.path(:aspera_license)
|
|
179
236
|
self.path(:aspera_conf)
|
|
180
237
|
Zip::File.open(sdk_zip_path) do |zip_file|
|
|
181
238
|
zip_file.each do |entry|
|
|
239
|
+
# get only specified arch, but not folder, only files
|
|
182
240
|
if entry.name.include?(filter) and !entry.name.end_with?('/')
|
|
183
|
-
archive_file=File.join(
|
|
241
|
+
archive_file=File.join(sdk_path,File.basename(entry.name))
|
|
184
242
|
File.open(archive_file, 'wb') do |output_stream|
|
|
185
243
|
IO.copy_stream(entry.get_input_stream, output_stream)
|
|
186
244
|
end
|
|
187
|
-
if entry.name.
|
|
245
|
+
if File.basename(entry.name).eql?(ascp_filename)
|
|
188
246
|
FileUtils.chmod(0755,archive_file)
|
|
189
247
|
ascp_path=archive_file
|
|
190
248
|
end
|
|
@@ -192,13 +250,8 @@ module Aspera
|
|
|
192
250
|
end
|
|
193
251
|
end
|
|
194
252
|
File.unlink(sdk_zip_path) rescue nil # Windows may give error
|
|
195
|
-
ascp_version=
|
|
196
|
-
|
|
197
|
-
# get version from ascp, only after full extract, as windows requires SSL/TLS DLLs
|
|
198
|
-
m=%x{#{ascp_path} -A}.match(/ascp version (.*)/)
|
|
199
|
-
ascp_version=m[1] unless m.nil?
|
|
200
|
-
File.write(File.join(@folder,PRODUCT_INFO),"<product><name>IBM Aspera SDK</name><version>#{ascp_version}</version></product>")
|
|
201
|
-
end
|
|
253
|
+
ascp_version=get_ascp_version(ascp_path)
|
|
254
|
+
File.write(File.join(folder_path,PRODUCT_INFO),"<product><name>IBM Aspera SDK</name><version>#{ascp_version}</version></product>")
|
|
202
255
|
return ascp_version
|
|
203
256
|
end
|
|
204
257
|
|
|
@@ -211,25 +264,31 @@ module Aspera
|
|
|
211
264
|
PRODUCT_INFO='product-info.mf'
|
|
212
265
|
# policy for product selection
|
|
213
266
|
FIRST_FOUND='FIRST'
|
|
214
|
-
SDK_URL='https://eudemo.asperademo.com/aspera/faspex/sdk.zip'
|
|
215
267
|
|
|
216
|
-
private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO
|
|
268
|
+
private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO
|
|
217
269
|
|
|
218
|
-
|
|
270
|
+
def initialize
|
|
271
|
+
@path_to_ascp=nil
|
|
272
|
+
@sdk_folder=nil
|
|
273
|
+
@found_products=nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @return folder paths for specified applications
|
|
277
|
+
# @param name Connect or CLI
|
|
219
278
|
def get_product_folders(name)
|
|
220
279
|
found=installed_products.select{|i|i[:expected].eql?(name) or i[:name].eql?(name)}
|
|
221
280
|
raise "Product: #{name} not found, please install." if found.empty?
|
|
222
281
|
return found.first
|
|
223
282
|
end
|
|
224
283
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@
|
|
228
|
-
@
|
|
284
|
+
# @return the path to folder where SDK is installed
|
|
285
|
+
def folder_path
|
|
286
|
+
raise "Undefined path to SDK" if @sdk_folder.nil?
|
|
287
|
+
FileUtils.mkdir_p(@sdk_folder) unless Dir.exist?(@sdk_folder)
|
|
288
|
+
@sdk_folder
|
|
229
289
|
end
|
|
230
290
|
|
|
231
|
-
#
|
|
232
|
-
# fields
|
|
291
|
+
# @return product folders depending on OS fields
|
|
233
292
|
# :expected M app name is taken from the manifest if present, else defaults to this value
|
|
234
293
|
# :app_root M main folder for the application
|
|
235
294
|
# :log_root O location of log files (Linux uses syslog)
|
|
@@ -271,7 +330,7 @@ module Aspera
|
|
|
271
330
|
:log_root =>File.join(Dir.home,'Library','Logs','Aspera_Drive'),
|
|
272
331
|
:sub_bin =>File.join('Contents','Resources'),
|
|
273
332
|
}]
|
|
274
|
-
else; return [{ # other: Linux and
|
|
333
|
+
else; return [{ # other: Linux and Unix family
|
|
275
334
|
:expected =>PRODUCT_CONNECT,
|
|
276
335
|
:app_root =>File.join(Dir.home,'.aspera','connect'),
|
|
277
336
|
:run_root =>File.join(Dir.home,'.aspera','connect')
|
|
@@ -285,6 +344,9 @@ module Aspera
|
|
|
285
344
|
end
|
|
286
345
|
end
|
|
287
346
|
|
|
347
|
+
# @return a standard bypass key
|
|
348
|
+
# @param type rsa or dsa
|
|
349
|
+
# @param id in repository 1 for dsa, 2 for rsa
|
|
288
350
|
def get_key(type,id)
|
|
289
351
|
hf=['begin','end'].map{|t|"-----#{t} #{type} private key-----".upcase}
|
|
290
352
|
bin=Base64.strict_encode64(DataRepository.instance.get_bin(id))
|
data/lib/aspera/fasp/local.rb
CHANGED
|
@@ -17,17 +17,33 @@ require 'securerandom'
|
|
|
17
17
|
|
|
18
18
|
module Aspera
|
|
19
19
|
module Fasp
|
|
20
|
-
# default transfer username for access key based transfers
|
|
20
|
+
# (public) default transfer username for access key based transfers
|
|
21
21
|
ACCESS_KEY_TRANSFER_USER='xfer'
|
|
22
22
|
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
|
23
23
|
class Local < Manager
|
|
24
|
-
#
|
|
24
|
+
# options for initialize
|
|
25
|
+
DEFAULT_OPTIONS = {
|
|
26
|
+
:spawn_timeout_sec => 3,
|
|
27
|
+
:spawn_delay_sec => 2,
|
|
28
|
+
:wss => false,
|
|
29
|
+
:resume => {}
|
|
30
|
+
}
|
|
31
|
+
DEFAULT_UDP_PORT=33001
|
|
32
|
+
private_constant :DEFAULT_OPTIONS
|
|
33
|
+
# set to false to keep ascp progress bar display ("true" adds ascp's option -q)
|
|
25
34
|
attr_accessor :quiet
|
|
26
|
-
|
|
27
|
-
#
|
|
35
|
+
|
|
36
|
+
# start ascp transfer (non blocking), single or multi-session
|
|
37
|
+
# job information added to @jobs
|
|
38
|
+
# @param transfer_spec [Hash] aspera transfer specification
|
|
39
|
+
# @param options [Hash] :resumer, :regenerate_token
|
|
28
40
|
def start_transfer(transfer_spec,options={})
|
|
29
|
-
raise
|
|
30
|
-
|
|
41
|
+
raise 'option: must be hash (or nil)' unless options.is_a?(Hash)
|
|
42
|
+
job_options = options.clone
|
|
43
|
+
job_options[:resumer] ||= @resume_policy
|
|
44
|
+
job_options[:job_id] ||= SecureRandom.uuid
|
|
45
|
+
# clone transfer spec because we modify it (first level keys)
|
|
46
|
+
transfer_spec=transfer_spec.clone
|
|
31
47
|
# if there is aspera tags
|
|
32
48
|
if transfer_spec['tags'].is_a?(Hash) and transfer_spec['tags']['aspera'].is_a?(Hash)
|
|
33
49
|
# TODO: what is this for ? only on local ascp ?
|
|
@@ -39,6 +55,7 @@ module Aspera
|
|
|
39
55
|
transfer_spec['tags']['aspera']['xfer_retry']||=3600
|
|
40
56
|
end
|
|
41
57
|
Log.dump('ts',transfer_spec)
|
|
58
|
+
|
|
42
59
|
# add bypass keys when authentication is token and no auth is provided
|
|
43
60
|
if transfer_spec.has_key?('token') and
|
|
44
61
|
!transfer_spec.has_key?('remote_password') and
|
|
@@ -47,8 +64,28 @@ module Aspera
|
|
|
47
64
|
transfer_spec['EX_ssh_key_paths'] = Installation.instance.bypass_keys
|
|
48
65
|
end
|
|
49
66
|
|
|
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
|
|
71
|
+
if transfer_spec.has_key?('multi_session')
|
|
72
|
+
multi_session_number=transfer_spec['multi_session'].to_i
|
|
73
|
+
if multi_session_number < 0
|
|
74
|
+
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']
|
|
82
|
+
transfer_spec.delete('fasp_port')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
50
87
|
# compute known args
|
|
51
|
-
env_args=Parameters.ts_to_env_args(transfer_spec,wss: @
|
|
88
|
+
env_args=Parameters.ts_to_env_args(transfer_spec,wss: @options[:wss])
|
|
52
89
|
|
|
53
90
|
# add fallback cert and key as arguments if needed
|
|
54
91
|
if ['1','force'].include?(transfer_spec['http_fallback'])
|
|
@@ -60,118 +97,98 @@ module Aspera
|
|
|
60
97
|
|
|
61
98
|
# transfer job can be multi session
|
|
62
99
|
xfer_job={
|
|
63
|
-
:id => job_id,
|
|
64
|
-
:sessions => []
|
|
100
|
+
:id => job_options[:job_id],
|
|
101
|
+
:sessions => [] # all sessions as below
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
# generic session information
|
|
68
105
|
session={
|
|
69
|
-
:
|
|
70
|
-
:
|
|
71
|
-
:
|
|
72
|
-
:
|
|
106
|
+
:thread => nil, # Thread object monitoring management port, not nil when pushed to :sessions
|
|
107
|
+
:error => nil, # exception if failed
|
|
108
|
+
:io => nil, # management port server socket
|
|
109
|
+
:id => nil, # SessionId from INIT message in mgt port
|
|
110
|
+
:env_args => env_args, # env vars and args to ascp (from transfer spec)
|
|
111
|
+
:options => job_options # [Hash]
|
|
73
112
|
}
|
|
74
113
|
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
if multi_session_number <= 1
|
|
115
|
+
Log.log.debug('Starting single session thread')
|
|
77
116
|
# single session for transfer : simple
|
|
78
117
|
session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
|
|
79
118
|
xfer_job[:sessions].push(session)
|
|
80
119
|
else
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
raise "multi_session(#{transfer_spec['multi_session']}) shall be integer > 1" unless multi_session >= 1
|
|
85
|
-
# managed here, so delete from transfer spec
|
|
86
|
-
transfer_spec.delete('multi_session')
|
|
87
|
-
# TODO: check if changing fasp(UDP) port is really necessary, not clear from doc
|
|
88
|
-
if transfer_spec.has_key?('fasp_port')
|
|
89
|
-
multi_session_udp_port_base=transfer_spec['fasp_port']
|
|
90
|
-
transfer_spec.delete('fasp_port')
|
|
91
|
-
end
|
|
92
|
-
1.upto(multi_session) do |i|
|
|
120
|
+
Log.log.debug('Starting multi session threads')
|
|
121
|
+
1.upto(multi_session_number) do |i|
|
|
122
|
+
sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
|
|
93
123
|
# do deep copy (each thread has its own copy because it is modified here below and in thread)
|
|
94
124
|
this_session=session.clone()
|
|
95
125
|
this_session[:env_args]=this_session[:env_args].clone()
|
|
96
126
|
this_session[:env_args][:args]=this_session[:env_args][:args].clone()
|
|
97
|
-
this_session[:env_args][:args].unshift("-C#{i}:#{
|
|
127
|
+
this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_number}")
|
|
98
128
|
# necessary only if server is not linux, i.e. server does not support port re-use
|
|
99
|
-
this_session[:env_args][:args].unshift(
|
|
129
|
+
this_session[:env_args][:args].unshift('-O',"#{multi_session_udp_port_base+i-1}")
|
|
100
130
|
this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
|
|
101
131
|
xfer_job[:sessions].push(this_session)
|
|
102
132
|
end
|
|
103
133
|
end
|
|
104
|
-
Log.log.debug(
|
|
134
|
+
Log.log.debug('started session thread(s)')
|
|
105
135
|
|
|
106
136
|
# add job to list of jobs
|
|
107
|
-
@jobs[job_id]=xfer_job
|
|
108
|
-
|
|
137
|
+
@jobs[job_options[:job_id]]=xfer_job
|
|
109
138
|
Log.log.debug("jobs: #{@jobs.keys.count}")
|
|
110
|
-
|
|
139
|
+
|
|
140
|
+
return job_options[:job_id]
|
|
111
141
|
end # start_transfer
|
|
112
142
|
|
|
113
143
|
# wait for completion of all jobs started
|
|
114
144
|
# @return list of :success or error message
|
|
115
145
|
def wait_for_transfers_completion
|
|
116
|
-
Log.log.debug(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if running.eql?(0)
|
|
131
|
-
# since all are finished and we return the result, clear statuses
|
|
132
|
-
@jobs.clear
|
|
133
|
-
return result
|
|
134
|
-
end
|
|
135
|
-
Log.log.debug("wait for completed: running: #{running}")
|
|
136
|
-
# wait for session termination
|
|
137
|
-
@cond_var.wait(@mutex)
|
|
138
|
-
end # loop
|
|
139
|
-
end # mutex
|
|
140
|
-
# never reach here
|
|
141
|
-
raise "internal error"
|
|
146
|
+
Log.log.debug('wait_for_transfers_completion')
|
|
147
|
+
# set to non-nil to exit loop
|
|
148
|
+
result=[]
|
|
149
|
+
@jobs.each do |id,job|
|
|
150
|
+
job[:sessions].each do |session|
|
|
151
|
+
Log.log.debug("join #{session[:thread]}")
|
|
152
|
+
session[:thread].join
|
|
153
|
+
result.push(session[:error] ? session[:error] : :success)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
Log.log.debug('all transfers joined')
|
|
157
|
+
# since all are finished and we return the result, clear statuses
|
|
158
|
+
@jobs.clear
|
|
159
|
+
return result
|
|
142
160
|
end
|
|
143
161
|
|
|
144
|
-
#
|
|
162
|
+
# used by asession (to be removed ?)
|
|
145
163
|
def shutdown
|
|
146
|
-
Log.log.debug(
|
|
147
|
-
Log.log.debug("send signal to monitor")
|
|
148
|
-
# tell monitor to stop
|
|
149
|
-
@mutex.synchronize do
|
|
150
|
-
@monitor_stop=true
|
|
151
|
-
@cond_var.broadcast
|
|
152
|
-
end
|
|
153
|
-
# wait for thread termination
|
|
154
|
-
@monitor_thread.join
|
|
155
|
-
@monitor_thread=nil
|
|
156
|
-
Log.log.debug("joined monitor")
|
|
164
|
+
Log.log.debug('fasp local shutdown')
|
|
157
165
|
end
|
|
158
166
|
|
|
159
|
-
# This is the low level method to start
|
|
167
|
+
# This is the low level method to start the "ascp" process
|
|
160
168
|
# currently, relies on command line arguments
|
|
161
169
|
# start ascp with management port.
|
|
162
170
|
# raises FaspError on error
|
|
163
171
|
# if there is a thread info: set and broadcast session id
|
|
164
172
|
# @param env_args a hash containing :args :env :ascp_version
|
|
165
|
-
#
|
|
166
|
-
|
|
173
|
+
# @param session this session information
|
|
174
|
+
# could be private method
|
|
175
|
+
def start_transfer_with_args_env(env_args,session)
|
|
176
|
+
raise 'env_args must be Hash' unless env_args.is_a?(Hash)
|
|
177
|
+
raise 'session must be Hash' unless session.is_a?(Hash)
|
|
178
|
+
# by default we assume an exception will be raised (for ensure block)
|
|
179
|
+
exception_raised=true
|
|
167
180
|
begin
|
|
168
181
|
Log.log.debug("env_args=#{env_args.inspect}")
|
|
169
|
-
|
|
182
|
+
# get location of ascp executable
|
|
183
|
+
ascp_path=@mutex.synchronize do
|
|
184
|
+
Fasp::Installation.instance.path(env_args[:ascp_version])
|
|
185
|
+
end
|
|
186
|
+
# (optional) check it exists
|
|
170
187
|
raise Fasp::Error.new("no such file: #{ascp_path}") unless File.exist?(ascp_path)
|
|
171
|
-
|
|
188
|
+
# open random local TCP port for listening for ascp management
|
|
189
|
+
mgt_sock = TCPServer.new('127.0.0.1',0)
|
|
190
|
+
# clone arguments as we eed to modify with mgt port
|
|
172
191
|
ascp_arguments=env_args[:args].clone
|
|
173
|
-
# open random local TCP port listening
|
|
174
|
-
mgt_sock = TCPServer.new('127.0.0.1',0 )
|
|
175
192
|
# add management port
|
|
176
193
|
ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
|
|
177
194
|
# start ascp in sub process
|
|
@@ -180,8 +197,9 @@ module Aspera
|
|
|
180
197
|
ascp_pid = Process.spawn(env_args[:env],[ascp_path,ascp_path],*ascp_arguments)
|
|
181
198
|
# in parent, wait for connection to socket max 3 seconds
|
|
182
199
|
Log.log.debug("before accept for pid (#{ascp_pid})")
|
|
200
|
+
# init management socket
|
|
183
201
|
ascp_mgt_io=nil
|
|
184
|
-
Timeout.timeout(
|
|
202
|
+
Timeout.timeout(@options[:spawn_timeout_sec]) do
|
|
185
203
|
ascp_mgt_io = mgt_sock.accept
|
|
186
204
|
# management messages include file names which may be utf8
|
|
187
205
|
# by default socket is US-ASCII
|
|
@@ -189,21 +207,13 @@ module Aspera
|
|
|
189
207
|
ascp_mgt_io.set_encoding(Encoding::UTF_8)
|
|
190
208
|
end
|
|
191
209
|
Log.log.debug("after accept (#{ascp_mgt_io})")
|
|
192
|
-
|
|
193
|
-
unless session.nil?
|
|
194
|
-
@mutex.synchronize do
|
|
195
|
-
session[:io]=ascp_mgt_io
|
|
196
|
-
@cond_var.broadcast
|
|
197
|
-
end
|
|
198
|
-
end
|
|
210
|
+
session[:io]=ascp_mgt_io
|
|
199
211
|
# exact text for event, with \n
|
|
200
212
|
current_event_text=''
|
|
201
213
|
# parsed event (hash)
|
|
202
214
|
current_event_data=nil
|
|
203
|
-
|
|
204
215
|
# this is the last full status
|
|
205
216
|
last_status_event=nil
|
|
206
|
-
|
|
207
217
|
# read management port
|
|
208
218
|
loop do
|
|
209
219
|
# TODO: timeout here ?
|
|
@@ -222,45 +232,44 @@ module Aspera
|
|
|
222
232
|
# event field
|
|
223
233
|
current_event_data[$1] = $2
|
|
224
234
|
when ''
|
|
225
|
-
# end event
|
|
226
|
-
raise
|
|
235
|
+
# empty line is separator to end event information
|
|
236
|
+
raise 'unexpected empty line' if current_event_data.nil?
|
|
227
237
|
current_event_data[Manager::LISTENER_SESSION_ID_B]=ascp_pid
|
|
228
238
|
notify_listeners(current_event_text,current_event_data)
|
|
229
|
-
# TODO: check if this is always the last event
|
|
230
239
|
case current_event_data['Type']
|
|
240
|
+
when 'INIT'
|
|
241
|
+
session[:id]=current_event_data['SessionId']
|
|
242
|
+
Log.log.debug("session id: #{session[:id]}")
|
|
231
243
|
when 'DONE','ERROR'
|
|
244
|
+
# TODO: check if this is always the last event
|
|
232
245
|
last_status_event = current_event_data
|
|
233
|
-
when 'INIT'
|
|
234
|
-
unless session.nil?
|
|
235
|
-
@mutex.synchronize do
|
|
236
|
-
session[:state]=:started
|
|
237
|
-
session[:id]=current_event_data['SessionId']
|
|
238
|
-
Log.log.debug("session id: #{session[:id]}")
|
|
239
|
-
@cond_var.broadcast
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
246
|
end # event type
|
|
243
247
|
else
|
|
244
248
|
raise "unexpected line:[#{line}]"
|
|
245
249
|
end # case
|
|
246
|
-
end # loop
|
|
250
|
+
end # loop (process mgt port lines)
|
|
247
251
|
# check that last status was received before process exit
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
if last_status_event.is_a?(Hash)
|
|
253
|
+
case last_status_event['Type']
|
|
254
|
+
when 'DONE'
|
|
255
|
+
# all went well
|
|
256
|
+
exception_raised=false
|
|
257
|
+
when 'ERROR'
|
|
258
|
+
Log.log.error("code: #{last_status_event['Code']}")
|
|
259
|
+
if last_status_event['Description'] =~ /bearer token/i
|
|
260
|
+
Log.log.error('need to regenerate token'.red)
|
|
261
|
+
if session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
|
|
262
|
+
# regenerate token here, expired, or error on it
|
|
263
|
+
env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
|
|
264
|
+
end
|
|
259
265
|
end
|
|
266
|
+
raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
|
|
267
|
+
else # case
|
|
268
|
+
raise "unexpected last event type: #{last_status_event['Type']}"
|
|
260
269
|
end
|
|
261
|
-
raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
|
|
262
270
|
else
|
|
263
|
-
|
|
271
|
+
exception_raised=false
|
|
272
|
+
Log.log.debug('no status read from ascp mgt port')
|
|
264
273
|
end
|
|
265
274
|
rescue SystemCallError => e
|
|
266
275
|
# Process.spawn
|
|
@@ -270,107 +279,94 @@ module Aspera
|
|
|
270
279
|
rescue Interrupt => e
|
|
271
280
|
raise Fasp::Error.new('transfer interrupted by user')
|
|
272
281
|
ensure
|
|
273
|
-
#
|
|
282
|
+
# if ascp was successfully started
|
|
274
283
|
unless ascp_pid.nil?
|
|
275
|
-
|
|
276
|
-
Process.kill('INT',ascp_pid)
|
|
277
|
-
rescue
|
|
278
|
-
end
|
|
279
|
-
# avoid zombie
|
|
284
|
+
# "wait" for process to avoid zombie
|
|
280
285
|
Process.wait(ascp_pid)
|
|
286
|
+
status=$?
|
|
281
287
|
ascp_pid=nil
|
|
282
288
|
session.delete(:io)
|
|
289
|
+
if !status.success?
|
|
290
|
+
message="ascp failed with code #{status.exitstatus}"
|
|
291
|
+
if exception_raised
|
|
292
|
+
# just debug, as main exception is already here
|
|
293
|
+
Log.log.debug(message)
|
|
294
|
+
else
|
|
295
|
+
raise Fasp::Error.new(message)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
283
298
|
end
|
|
284
299
|
end # begin-ensure
|
|
285
300
|
end # start_transfer_with_args_env
|
|
286
301
|
|
|
287
|
-
# send command
|
|
302
|
+
# send command of management port to ascp session
|
|
303
|
+
# @param job_id identified transfer process
|
|
304
|
+
# @param session_index index of session (for multi session)
|
|
305
|
+
# @param data command on mgt port, examples:
|
|
288
306
|
# {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
289
307
|
# {'type'=>'DONE'}
|
|
290
308
|
def send_command(job_id,session_index,data)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
end
|
|
309
|
+
job=@jobs[job_id]
|
|
310
|
+
raise 'no such job' if job.nil?
|
|
311
|
+
session=job[:sessions][session_index]
|
|
312
|
+
raise 'no such session' if session.nil?
|
|
313
|
+
Log.log.debug("command: #{data}")
|
|
314
|
+
# build command
|
|
315
|
+
command=data.
|
|
316
|
+
keys.
|
|
317
|
+
map{|k|"#{k.capitalize}: #{data[k]}"}.
|
|
318
|
+
unshift('FASPMGR 2').
|
|
319
|
+
push('','').
|
|
320
|
+
join("\n")
|
|
321
|
+
session[:io].puts(command)
|
|
305
322
|
end
|
|
306
323
|
|
|
307
324
|
private
|
|
308
325
|
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
# @param options : keys(symbol): wss, resume
|
|
327
|
+
def initialize(options=nil)
|
|
311
328
|
super()
|
|
312
329
|
# by default no interactive progress bar
|
|
313
330
|
@quiet=true
|
|
314
|
-
#
|
|
331
|
+
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, condvar on change
|
|
315
332
|
@jobs={}
|
|
316
|
-
# mutex protects
|
|
333
|
+
# mutex protects global data accessed by threads
|
|
317
334
|
@mutex=Mutex.new
|
|
318
|
-
#
|
|
319
|
-
@
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
# manage options
|
|
336
|
+
@options=DEFAULT_OPTIONS.clone
|
|
337
|
+
if !options.nil?
|
|
338
|
+
raise "expecting Hash (or nil), but have #{options.class}" unless options.is_a?(Hash)
|
|
339
|
+
options.each do |k,v|
|
|
340
|
+
if DEFAULT_OPTIONS.has_key?(k)
|
|
341
|
+
@options[k]=v
|
|
342
|
+
else
|
|
343
|
+
raise "unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map{|i|i.to_s}.join(",")}"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
Log.log.debug("local options= #{options}")
|
|
348
|
+
@resume_policy=ResumePolicy.new(@options[:resume].symbolize_keys)
|
|
325
349
|
end
|
|
326
350
|
|
|
327
351
|
# transfer thread entry
|
|
328
|
-
#
|
|
329
|
-
# TODO: extract resume algorithm in a specific object
|
|
352
|
+
# @param session information
|
|
330
353
|
def transfer_thread_entry(session)
|
|
331
354
|
begin
|
|
332
355
|
# set name for logging
|
|
333
|
-
Thread.current[:name]=
|
|
334
|
-
# update state once in thread
|
|
335
|
-
session[:state]=:started
|
|
356
|
+
Thread.current[:name]='transfer'
|
|
336
357
|
Log.log.debug("ENTER (#{Thread.current[:name]})")
|
|
337
358
|
# start transfer with selected resumer policy
|
|
338
|
-
session[:resumer].process do
|
|
359
|
+
session[:options][:resumer].process do
|
|
339
360
|
start_transfer_with_args_env(session[:env_args],session)
|
|
340
361
|
end
|
|
341
362
|
Log.log.debug('transfer ok'.bg_green)
|
|
342
|
-
session[:state]=:success
|
|
343
363
|
rescue => e
|
|
344
|
-
session[:state]=:failed
|
|
345
364
|
session[:error]=e
|
|
346
|
-
Log.log.error("#{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red) if Log.instance.level.eql?(:debug)
|
|
347
|
-
ensure
|
|
348
|
-
@mutex.synchronize do
|
|
349
|
-
# ensure id is set to unblock start procedure
|
|
350
|
-
session[:id]||=nil
|
|
351
|
-
@cond_var.broadcast
|
|
352
|
-
end
|
|
365
|
+
Log.log.error("Transfer thread error: #{e.class}:\n#{e.message}:\n#{e.backtrace.join("\n")}".red) if Log.instance.level.eql?(:debug)
|
|
353
366
|
end
|
|
354
367
|
Log.log.debug("EXIT (#{Thread.current[:name]})")
|
|
355
368
|
end
|
|
356
369
|
|
|
357
|
-
# main thread method for monitor
|
|
358
|
-
# currently: just joins started threads
|
|
359
|
-
def monitor_thread_entry
|
|
360
|
-
Thread.current[:name]="monitor"
|
|
361
|
-
@mutex.synchronize do
|
|
362
|
-
until @monitor_stop do
|
|
363
|
-
# wait for session termination
|
|
364
|
-
@cond_var.wait(@mutex)
|
|
365
|
-
@jobs.values do |job|
|
|
366
|
-
job[:sessions].each do |session|
|
|
367
|
-
session[:thread].join if [:success,:failed].include?(session[:state])
|
|
368
|
-
end # sessions
|
|
369
|
-
end # jobs
|
|
370
|
-
end # monitor run
|
|
371
|
-
end # sync
|
|
372
|
-
Log.log.debug("EXIT (#{Thread.current[:name]})")
|
|
373
|
-
end # monitor_thread_entry
|
|
374
370
|
end # Local
|
|
375
371
|
end
|
|
376
372
|
end
|