aspera-cli 4.0.0.pre1
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 +7 -0
- data/README.md +3592 -0
- data/bin/ascli +7 -0
- data/bin/asession +89 -0
- data/docs/Makefile +59 -0
- data/docs/README.erb.md +3012 -0
- data/docs/README.md +13 -0
- data/docs/diagrams.txt +49 -0
- data/docs/secrets.make +38 -0
- data/docs/test_env.conf +117 -0
- data/docs/transfer_spec.html +99 -0
- data/examples/aoc.rb +17 -0
- data/examples/proxy.pac +60 -0
- data/examples/transfer.rb +115 -0
- data/lib/aspera/api_detector.rb +60 -0
- data/lib/aspera/ascmd.rb +151 -0
- data/lib/aspera/ats_api.rb +43 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
- data/lib/aspera/cli/extended_value.rb +88 -0
- data/lib/aspera/cli/formater.rb +238 -0
- data/lib/aspera/cli/listener/line_dump.rb +17 -0
- data/lib/aspera/cli/listener/logger.rb +20 -0
- data/lib/aspera/cli/listener/progress.rb +52 -0
- data/lib/aspera/cli/listener/progress_multi.rb +91 -0
- data/lib/aspera/cli/main.rb +304 -0
- data/lib/aspera/cli/manager.rb +440 -0
- data/lib/aspera/cli/plugin.rb +90 -0
- data/lib/aspera/cli/plugins/alee.rb +24 -0
- data/lib/aspera/cli/plugins/ats.rb +231 -0
- data/lib/aspera/cli/plugins/bss.rb +71 -0
- data/lib/aspera/cli/plugins/config.rb +806 -0
- data/lib/aspera/cli/plugins/console.rb +62 -0
- data/lib/aspera/cli/plugins/cos.rb +106 -0
- data/lib/aspera/cli/plugins/faspex.rb +377 -0
- data/lib/aspera/cli/plugins/faspex5.rb +93 -0
- data/lib/aspera/cli/plugins/node.rb +438 -0
- data/lib/aspera/cli/plugins/oncloud.rb +937 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
- data/lib/aspera/cli/plugins/preview.rb +464 -0
- data/lib/aspera/cli/plugins/server.rb +216 -0
- data/lib/aspera/cli/plugins/shares.rb +63 -0
- data/lib/aspera/cli/plugins/shares2.rb +114 -0
- data/lib/aspera/cli/plugins/sync.rb +65 -0
- data/lib/aspera/cli/plugins/xnode.rb +115 -0
- data/lib/aspera/cli/transfer_agent.rb +251 -0
- data/lib/aspera/cli/version.rb +5 -0
- data/lib/aspera/colors.rb +39 -0
- data/lib/aspera/command_line_builder.rb +137 -0
- data/lib/aspera/fasp/aoc.rb +24 -0
- data/lib/aspera/fasp/connect.rb +99 -0
- data/lib/aspera/fasp/error.rb +21 -0
- data/lib/aspera/fasp/error_info.rb +60 -0
- data/lib/aspera/fasp/http_gw.rb +81 -0
- data/lib/aspera/fasp/installation.rb +240 -0
- data/lib/aspera/fasp/listener.rb +11 -0
- data/lib/aspera/fasp/local.rb +377 -0
- data/lib/aspera/fasp/manager.rb +69 -0
- data/lib/aspera/fasp/node.rb +88 -0
- data/lib/aspera/fasp/parameters.rb +235 -0
- data/lib/aspera/fasp/resume_policy.rb +76 -0
- data/lib/aspera/fasp/uri.rb +51 -0
- data/lib/aspera/faspex_gw.rb +196 -0
- data/lib/aspera/hash_ext.rb +28 -0
- data/lib/aspera/log.rb +80 -0
- data/lib/aspera/nagios.rb +71 -0
- data/lib/aspera/node.rb +14 -0
- data/lib/aspera/oauth.rb +319 -0
- data/lib/aspera/on_cloud.rb +421 -0
- data/lib/aspera/open_application.rb +72 -0
- data/lib/aspera/persistency_action_once.rb +42 -0
- data/lib/aspera/persistency_folder.rb +91 -0
- data/lib/aspera/preview/file_types.rb +300 -0
- data/lib/aspera/preview/generator.rb +258 -0
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +35 -0
- data/lib/aspera/preview/utils.rb +131 -0
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.erb.js +287 -0
- data/lib/aspera/proxy_auto_config.rb +34 -0
- data/lib/aspera/rest.rb +296 -0
- data/lib/aspera/rest_call_error.rb +13 -0
- data/lib/aspera/rest_error_analyzer.rb +98 -0
- data/lib/aspera/rest_errors_aspera.rb +58 -0
- data/lib/aspera/ssh.rb +53 -0
- data/lib/aspera/sync.rb +82 -0
- data/lib/aspera/temp_file_manager.rb +37 -0
- data/lib/aspera/uri_reader.rb +25 -0
- metadata +288 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'aspera/fasp/node'
|
2
|
+
require 'aspera/log'
|
3
|
+
require 'aspera/on_cloud.rb'
|
4
|
+
|
5
|
+
module Aspera
|
6
|
+
module Fasp
|
7
|
+
class Aoc < Node
|
8
|
+
def initialize(on_cloud_options)
|
9
|
+
@app=on_cloud_options[:app] || OnCloud::FILES_APP
|
10
|
+
@api_oncloud=OnCloud.new(on_cloud_options)
|
11
|
+
Log.log.warn("Under Development")
|
12
|
+
server_node_file = @api_oncloud.resolve_node_file(server_home_node_file,server_folder)
|
13
|
+
# force node as transfer agent
|
14
|
+
node_api=Fasp::Node.new(@api_oncloud.get_node_api(client_node_file[:node_info],OnCloud::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
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'aspera/fasp/manager'
|
2
|
+
require 'aspera/rest'
|
3
|
+
require 'aspera/open_application'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'tty-spinner'
|
6
|
+
|
7
|
+
module Aspera
|
8
|
+
module Fasp
|
9
|
+
class Connect < Manager
|
10
|
+
MAX_CONNECT_START_RETRY=3
|
11
|
+
SLEEP_SEC_BETWEEN_RETRY=2
|
12
|
+
private_constant :MAX_CONNECT_START_RETRY,:SLEEP_SEC_BETWEEN_RETRY
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
@connect_app_id=SecureRandom.uuid
|
16
|
+
# TODO: start here and create monitor
|
17
|
+
end
|
18
|
+
|
19
|
+
def start_transfer(transfer_spec,options=nil)
|
20
|
+
raise 'Using connect requires a graphical environment' if !OpenApplication.default_gui_mode.eql?(:graphical)
|
21
|
+
trynumber=0
|
22
|
+
begin
|
23
|
+
connect_url=Installation.instance.connect_uri
|
24
|
+
Log.log.debug("found: #{connect_url}")
|
25
|
+
@connect_api=Rest.new({base_url: "#{connect_url}/v5/connect",headers: {'Origin'=>Rest.user_agent}}) # could use v6 also now
|
26
|
+
cinfo=@connect_api.read('info/version')[:data]
|
27
|
+
rescue => e # Errno::ECONNREFUSED
|
28
|
+
raise StandardError,"Unable to start connect after #{trynumber} try" if trynumber >= MAX_CONNECT_START_RETRY
|
29
|
+
Log.log.warn("connect is not started. Retry ##{trynumber}, err=#{e}")
|
30
|
+
trynumber+=1
|
31
|
+
if !OpenApplication.uri_graphical('fasp://initialize')
|
32
|
+
OpenApplication.uri_graphical('https://downloads.asperasoft.com/connect2/')
|
33
|
+
raise StandardError,'Connect is not installed'
|
34
|
+
end
|
35
|
+
sleep SLEEP_SEC_BETWEEN_RETRY
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
if transfer_spec['direction'] == 'send'
|
39
|
+
Log.log.warn("Connect requires upload selection using GUI, ignoring #{transfer_spec['paths']}".red)
|
40
|
+
transfer_spec.delete('paths')
|
41
|
+
resdata=@connect_api.create('windows/select-open-file-dialog/',{'title'=>'Select Files','suggestedName'=>'','allowMultipleSelection'=>true,'allowedFileTypes'=>'','aspera_connect_settings'=>{'app_id'=>@connect_app_id}})[:data]
|
42
|
+
transfer_spec['paths']=resdata['dataTransfer']['files'].map { |i| {'source'=>i['name']}}
|
43
|
+
end
|
44
|
+
@request_id=SecureRandom.uuid
|
45
|
+
# if there is a token, we ask connect client to use well known ssh private keys
|
46
|
+
# instead of asking password
|
47
|
+
transfer_spec['authentication']='token' if transfer_spec.has_key?('token')
|
48
|
+
connect_transfer_args={
|
49
|
+
'transfer_specs'=>[{
|
50
|
+
'transfer_spec'=>transfer_spec,
|
51
|
+
'aspera_connect_settings'=>{
|
52
|
+
'allow_dialogs'=>true,
|
53
|
+
'app_id'=>@connect_app_id,
|
54
|
+
'request_id'=>@request_id
|
55
|
+
}}]}
|
56
|
+
# asynchronous anyway
|
57
|
+
@connect_api.create('transfers/start',connect_transfer_args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_for_transfers_completion
|
61
|
+
connect_activity_args={'aspera_connect_settings'=>{'app_id'=>@connect_app_id}}
|
62
|
+
started=false
|
63
|
+
spinner=nil
|
64
|
+
loop do
|
65
|
+
result=@connect_api.create('transfers/activity',connect_activity_args)[:data]
|
66
|
+
if result['transfers']
|
67
|
+
trdata=result['transfers'].select{|i| i['aspera_connect_settings'] and i['aspera_connect_settings']['request_id'].eql?(@request_id)}.first
|
68
|
+
raise 'problem with connect, please kill it' unless trdata
|
69
|
+
# TODO: get session id
|
70
|
+
case trdata['status']
|
71
|
+
when 'completed'
|
72
|
+
notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@connect_app_id,'Type'=>'DONE'})
|
73
|
+
break
|
74
|
+
when 'initiating'
|
75
|
+
if spinner.nil?
|
76
|
+
spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
|
77
|
+
spinner.start
|
78
|
+
end
|
79
|
+
spinner.update(title: trdata['status'])
|
80
|
+
spinner.spin
|
81
|
+
when 'running'
|
82
|
+
#puts "running: sessions:#{trdata['sessions'].length}, #{trdata['sessions'].map{|i| i['bytes_transferred']}.join(',')}"
|
83
|
+
if !started and trdata['bytes_expected'] != 0
|
84
|
+
notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@connect_app_id,'Type'=>'NOTIFICATION','PreTransferBytes'=>trdata['bytes_expected']})
|
85
|
+
started=true
|
86
|
+
else
|
87
|
+
notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@connect_app_id,'Type'=>'STATS','Bytescont'=>trdata['bytes_written']})
|
88
|
+
end
|
89
|
+
else
|
90
|
+
raise Fasp::Error.new("#{trdata['status']}: #{trdata['error_desc']}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
sleep 1
|
94
|
+
end
|
95
|
+
return [] #TODO
|
96
|
+
end # wait
|
97
|
+
end # Connect
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'aspera/fasp/error_info'
|
2
|
+
|
3
|
+
module Aspera
|
4
|
+
module Fasp
|
5
|
+
# error raised if transfer fails
|
6
|
+
class Error < StandardError
|
7
|
+
attr_reader :err_code
|
8
|
+
def initialize(message,err_code=nil)
|
9
|
+
super(message)
|
10
|
+
@err_code = err_code
|
11
|
+
end
|
12
|
+
|
13
|
+
def info
|
14
|
+
r=Fasp::ERROR_INFO[@err_code] || {r: false , c: 'UNKNOWN', m: 'unknown', a: 'unknown'}
|
15
|
+
return r.merge({i: @err_code})
|
16
|
+
end
|
17
|
+
|
18
|
+
def retryable?; info[:r];end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Aspera
|
2
|
+
module Fasp
|
3
|
+
# from https://www.google.com/search?q=FASP+error+codes
|
4
|
+
# Note that the fact that an error is retryable is not internally defined by protocol, it's client responsibility
|
5
|
+
ERROR_INFO = {
|
6
|
+
# id retryable mnemo message additional info
|
7
|
+
1 => { r: false , c: 'FASP_PROTO', m: 'Generic fasp(tm) protocol error', a: 'fasp(tm) error'},
|
8
|
+
2 => { r: false , c: 'ASCP', m: 'Generic SCP error', a: 'ASCP error'},
|
9
|
+
3 => { r: false , c: 'AMBIGUOUS_TARGET', m: 'Target incorrectly specified', a: 'Ambiguous target'},
|
10
|
+
4 => { r: false , c: 'NO_SUCH_FILE', m: 'No such file or directory', a: 'No such file or directory'},
|
11
|
+
5 => { r: false , c: 'NO_PERMS', m: 'Insufficient permission to read or write', a: 'Insufficient permissions'},
|
12
|
+
6 => { r: false , c: 'NOT_DIR', m: 'Target is not a directory', a: 'Target must be a directory'},
|
13
|
+
7 => { r: false , c: 'IS_DIR', m: 'File is a directory - expected regular file', a: 'Expected regular file'},
|
14
|
+
8 => { r: false , c: 'USAGE', m: 'Incorrect usage of scp command', a: 'Incorrect usage of Aspera scp command'},
|
15
|
+
9 => { r: false , c: 'LIC_DUP', m: 'Duplicate license', a: 'Duplicate license'},
|
16
|
+
10 => { r: false , c: 'LIC_RATE_EXCEEDED', m: 'Rate exceeds the cap imposed by license', a: 'Rate exceeds cap imposed by license'},
|
17
|
+
11 => { r: false , c: 'INTERNAL_ERROR', m: 'Internal error (unexpected error)', a: 'Internal error'},
|
18
|
+
12 => { r: true , c: 'TRANSFER_ERROR', m: 'Error establishing control connection', a: 'Error establishing SSH connection (check SSH port and firewall)'},
|
19
|
+
13 => { r: true , c: 'TRANSFER_TIMEOUT', m: 'Timeout establishing control connection', a: 'Timeout establishing SSH connection (check SSH port and firewall)'},
|
20
|
+
14 => { r: true , c: 'CONNECTION_ERROR', m: 'Error establishing data connection', a: 'Error establishing UDP connection (check UDP port and firewall)'},
|
21
|
+
15 => { r: true , c: 'CONNECTION_TIMEOUT', m: 'Timeout establishing data connection', a: 'Timeout establishing UDP connection (check UDP port and firewall)'},
|
22
|
+
16 => { r: true , c: 'CONNECTION_LOST', m: 'Connection lost', a: 'Connection lost'},
|
23
|
+
17 => { r: true , c: 'RCVR_SEND_ERROR', m: 'Receiver fails to send feedback', a: 'Network failure (receiver can\'t send feedback)'},
|
24
|
+
18 => { r: true , c: 'RCVR_RECV_ERROR', m: 'Receiver fails to receive data packets', a: 'Network failure (receiver can\'t receive UDP data)'},
|
25
|
+
19 => { r: false , c: 'AUTH', m: 'Authentication failure', a: 'Authentication failure'},
|
26
|
+
20 => { r: false , c: 'NOTHING', m: 'Nothing to transfer', a: 'Nothing to transfer'},
|
27
|
+
21 => { r: false , c: 'NOT_REGULAR', m: 'Not a regular file (special file)', a: 'Not a regular file'},
|
28
|
+
22 => { r: false , c: 'FILE_TABLE_OVR', m: 'File table overflow', a: 'File table overflow'},
|
29
|
+
23 => { r: true , c: 'TOO_MANY_FILES', m: 'Too many files open', a: 'Too many files open'},
|
30
|
+
24 => { r: false , c: 'FILE_TOO_BIG', m: 'File too big for file system', a: 'File too big for filesystem'},
|
31
|
+
25 => { r: false , c: 'NO_SPACE_LEFT', m: 'No space left on disk', a: 'No space left on disk'},
|
32
|
+
26 => { r: false , c: 'READ_ONLY_FS', m: 'Read only file system', a: 'Read only filesystem'},
|
33
|
+
27 => { r: false , c: 'SOME_FILE_ERRS', m: 'Some individual files failed', a: 'One or more files failed'},
|
34
|
+
28 => { r: false , c: 'USER_CANCEL', m: 'Cancelled by user', a: 'Cancelled by user'},
|
35
|
+
29 => { r: false , c: 'LIC_NOLIC', m: 'License not found or unable to access', a: 'Unable to access license info'},
|
36
|
+
30 => { r: false , c: 'LIC_EXPIRED', m: 'License expired', a: 'License expired'},
|
37
|
+
31 => { r: false , c: 'SOCK_SETUP', m: 'Unable to setup socket (create, bind, etc ...)', a: 'Unable to set up socket'},
|
38
|
+
32 => { r: true , c: 'OUT_OF_MEMORY', m: 'Out of memory, unable to allocate', a: 'Out of memory'},
|
39
|
+
33 => { r: true , c: 'THREAD_SPAWN', m: 'Can\'t spawn thread', a: 'Unable to spawn thread'},
|
40
|
+
34 => { r: false , c: 'UNAUTHORIZED', m: 'Unauthorized by external auth server', a: 'Unauthorized'},
|
41
|
+
35 => { r: true , c: 'DISK_READ', m: 'Error reading source file from disk', a: 'Disk read error'},
|
42
|
+
36 => { r: true , c: 'DISK_WRITE', m: 'Error writing to disk', a: 'Disk write error'},
|
43
|
+
37 => { r: true , c: 'AUTHORIZATION', m: 'Used interchangeably with ERR_UNAUTHORIZED', a: 'Authorization failure'},
|
44
|
+
38 => { r: false , c: 'LIC_ILLEGAL', m: 'Operation not permitted by license', a: 'Operation not permitted by license'},
|
45
|
+
39 => { r: true , c: 'PEER_ABORTED_SESSION', m: 'Remote peer terminated session', a: 'Peer aborted session'},
|
46
|
+
40 => { r: true , c: 'DATA_TRANSFER_TIMEOUT', m: 'Transfer stalled, timed out', a: 'Data transfer stalled, timed out'},
|
47
|
+
41 => { r: false , c: 'BAD_PATH', m: 'Path violates docroot containment', a: 'File location is outside \'docroot\' hierarchy'},
|
48
|
+
42 => { r: false , c: 'ALREADY_EXISTS', m: 'File or directory already exists', a: 'File or directory already exists'},
|
49
|
+
43 => { r: false , c: 'STAT_FAILS', m: 'Cannot stat file', a: 'Cannot collect details about file or directory'},
|
50
|
+
44 => { r: true , c: 'PMTU_BRTT_ERROR', m: 'UDP session initiation fatal error', a: 'UDP session initiation fatal error'},
|
51
|
+
45 => { r: true , c: 'BWMEAS_ERROR', m: 'Bandwidth measurement fatal error', a: 'Bandwidth measurement fatal error'},
|
52
|
+
46 => { r: false , c: 'VLINK_ERROR', m: 'Virtual link error', a: 'Virtual link error'},
|
53
|
+
47 => { r: false , c: 'CONNECTION_ERROR_HTTP', m: 'Error establishing HTTP connection', a: 'Error establishing HTTP connection (check HTTP port and firewall)'},
|
54
|
+
48 => { r: false , c: 'FILE_ENCRYPTION_ERROR', m: 'File encryption error, e.g. corrupt file', a: 'File encryption/decryption error, e.g. corrupt file'},
|
55
|
+
49 => { r: false , c: 'FILE_DECRYPTION_PASS', m: 'File encryption/decryption error, e.g. corrupt file', a: 'File decryption error, bad passphrase'},
|
56
|
+
50 => { r: false , c: 'BAD_CONFIGURATION', m: 'Aspera.conf contains invalid data and was rejected', a: 'Invalid configuration'},
|
57
|
+
51 => { r: false , c: 'UNDEFINED', m: 'Should never happen, report to Aspera', a: 'Undefined error'},
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/bin/echo this is a ruby class:
|
2
|
+
require 'aspera/fasp/manager'
|
3
|
+
require 'aspera/log'
|
4
|
+
require 'aspera/rest'
|
5
|
+
|
6
|
+
# ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
|
7
|
+
module Aspera
|
8
|
+
module Fasp
|
9
|
+
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
10
|
+
class HttpGW < Manager
|
11
|
+
# start FASP transfer based on transfer spec (hash table)
|
12
|
+
# note that it is asynchronous
|
13
|
+
# HTTP download only supports file list
|
14
|
+
def start_transfer(transfer_spec,options={})
|
15
|
+
raise "GW URL must be set" unless !@gw_api.nil?
|
16
|
+
raise "option: must be hash (or nil)" unless options.is_a?(Hash)
|
17
|
+
raise "paths: must be Array" unless transfer_spec['paths'].is_a?(Array)
|
18
|
+
case transfer_spec['direction']
|
19
|
+
when 'send'
|
20
|
+
# this is a websocket
|
21
|
+
raise "error, not implemented"
|
22
|
+
when 'receive'
|
23
|
+
transfer_spec['zip_required']||=false
|
24
|
+
transfer_spec['authentication']||='token'
|
25
|
+
transfer_spec['source_root']||='/'
|
26
|
+
# is normally provided by application, like package name
|
27
|
+
if !transfer_spec.has_key?('download_name')
|
28
|
+
# by default it is the name of first file
|
29
|
+
dname=File.basename(transfer_spec['paths'].first['source'])
|
30
|
+
# we remove extension
|
31
|
+
dname=dname.gsub(/\.@gw_api.*$/,'')
|
32
|
+
# ands add indication of number of files if there is more than one
|
33
|
+
if transfer_spec['paths'].length > 1
|
34
|
+
dname=dname+" #{transfer_spec['paths'].length} Files"
|
35
|
+
end
|
36
|
+
transfer_spec['download_name']=dname
|
37
|
+
end
|
38
|
+
creation=@gw_api.create('download',{'transfer_spec'=>transfer_spec})[:data]
|
39
|
+
transfer_uuid=creation['url'].split('/').last
|
40
|
+
if transfer_spec['zip_required'] or transfer_spec['paths'].length > 1
|
41
|
+
# it is a zip file if zip is required or there is more than 1 file
|
42
|
+
file_dest=transfer_spec['download_name']+'.zip'
|
43
|
+
else
|
44
|
+
# it is a plain file if we don't require zip and there is only one file
|
45
|
+
file_dest=File.basename(transfer_spec['paths'].first['source'])
|
46
|
+
end
|
47
|
+
file_dest=File.join(transfer_spec['destination_root'],file_dest)
|
48
|
+
@gw_api.call({:operation=>'GET',:subpath=>"download/#{transfer_uuid}",:save_to_file=>file_dest})
|
49
|
+
else
|
50
|
+
raise "error"
|
51
|
+
end
|
52
|
+
end # start_transfer
|
53
|
+
|
54
|
+
# wait for completion of all jobs started
|
55
|
+
# @return list of :success or error message
|
56
|
+
def wait_for_transfers_completion
|
57
|
+
return [:success]
|
58
|
+
end
|
59
|
+
|
60
|
+
# terminates monitor thread
|
61
|
+
def shutdown
|
62
|
+
end
|
63
|
+
|
64
|
+
def url=(api_url)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def initialize(params)
|
70
|
+
raise "params must be Hash" unless params.is_a?(Hash)
|
71
|
+
params=params.symbolize_keys
|
72
|
+
raise "must have only one param: url" unless params.keys.eql?([:url])
|
73
|
+
super()
|
74
|
+
@gw_api=Rest.new({:base_url => params[:url]})
|
75
|
+
api_info = @gw_api.read('info')[:data]
|
76
|
+
Log.log.info("#{api_info}")
|
77
|
+
end
|
78
|
+
|
79
|
+
end # LocalHttp
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'aspera/log'
|
3
|
+
require 'aspera/open_application' # current_os_type
|
4
|
+
|
5
|
+
require 'xmlsimple'
|
6
|
+
require 'zlib'
|
7
|
+
require 'base64'
|
8
|
+
|
9
|
+
module Aspera
|
10
|
+
module Fasp
|
11
|
+
# Singleton that tells where to find ascp and other local resources (keys..) , using the "path(symb)" method.
|
12
|
+
# It is used by object : Fasp::Local to find necessary resources
|
13
|
+
# By default it takes the first Aspera product found specified in product_locations
|
14
|
+
# but the user can specify ascp location by calling:
|
15
|
+
# Installation.instance.use_ascp_from_product(product_name)
|
16
|
+
# or
|
17
|
+
# Installation.instance.ascp_path=ascp_path
|
18
|
+
class Installation
|
19
|
+
include Singleton
|
20
|
+
# currently used ascp executable
|
21
|
+
attr_accessor :ascp_path
|
22
|
+
# where key files are generated and used
|
23
|
+
attr_accessor :config_folder
|
24
|
+
# find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
|
25
|
+
# or select one from installed_products()
|
26
|
+
def use_ascp_from_product(product_name)
|
27
|
+
if product_name.eql?(FIRST_FOUND)
|
28
|
+
pl=installed_products.first
|
29
|
+
raise "no FASP installation found\nPlease check manual on how to install FASP." if pl.nil?
|
30
|
+
else
|
31
|
+
pl=installed_products.select{|i|i[:name].eql?(product_name)}.first
|
32
|
+
raise "no such product installed: #{product_name}" if pl.nil?
|
33
|
+
end
|
34
|
+
@ascp_path=pl[:ascp_path]
|
35
|
+
Log.log.debug("ascp_path=#{@ascp_path}")
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return the list of installed products in format of product_locations
|
39
|
+
def installed_products
|
40
|
+
if @found_products.nil?
|
41
|
+
@found_products=product_locations.select do |pl|
|
42
|
+
next false unless Dir.exist?(pl[:app_root])
|
43
|
+
Log.log.debug("found #{pl[:app_root]}")
|
44
|
+
sub_bin = pl[:sub_bin] || BIN_SUBFOLDER
|
45
|
+
exec_ext = OpenApplication.current_os_type.eql?(:windows) ? '.exe' : ''
|
46
|
+
pl[:ascp_path]=File.join(pl[:app_root],sub_bin,'ascp')+exec_ext
|
47
|
+
next false unless File.exist?(pl[:ascp_path])
|
48
|
+
product_info_file="#{pl[:app_root]}/#{PRODUCT_INFO}"
|
49
|
+
if File.exist?(product_info_file)
|
50
|
+
res_s=XmlSimple.xml_in(File.read(product_info_file),{"ForceArray"=>false})
|
51
|
+
pl[:name]=res_s['name']
|
52
|
+
pl[:version]=res_s['version']
|
53
|
+
else
|
54
|
+
pl[:name]=pl[:expected]
|
55
|
+
end
|
56
|
+
true # select this version
|
57
|
+
end
|
58
|
+
end
|
59
|
+
return @found_products
|
60
|
+
end
|
61
|
+
|
62
|
+
FILES=[:ascp,:ascp4,:ssh_bypass_key_dsa,:ssh_bypass_key_rsa,:fallback_cert,:fallback_key]
|
63
|
+
|
64
|
+
# get path of one resource file of currently activated product
|
65
|
+
# keys and certs are generated locally... (they are well known values, arch. independant)
|
66
|
+
def path(k)
|
67
|
+
case k
|
68
|
+
when :ascp,:ascp4
|
69
|
+
use_ascp_from_product(FIRST_FOUND) if @ascp_path.nil?
|
70
|
+
file=@ascp_path
|
71
|
+
# note that there might be a .exe at the end
|
72
|
+
file=file.gsub('ascp','ascp4') if k.eql?(:ascp4)
|
73
|
+
when :ssh_bypass_key_dsa
|
74
|
+
file=File.join(@config_folder,'aspera_bypass_dsa.pem')
|
75
|
+
File.write(file,Zlib::Inflate.inflate(Base64.decode64(SSH_BYPASS_DSA))) unless File.exist?(file)
|
76
|
+
File.chmod(0400,file)
|
77
|
+
when :ssh_bypass_key_rsa
|
78
|
+
file=File.join(@config_folder,'aspera_bypass_rsa.pem')
|
79
|
+
File.write(file,Zlib::Inflate.inflate(Base64.decode64(SSH_BYPASS_RSA))) unless File.exist?(file)
|
80
|
+
File.chmod(0400,file)
|
81
|
+
when :fallback_cert,:fallback_key
|
82
|
+
file_key=File.join(@config_folder,'aspera_fallback_key.pem')
|
83
|
+
file_cert=File.join(@config_folder,'aspera_fallback_cert.pem')
|
84
|
+
if !File.exist?(file_key) or !File.exist?(file_cert)
|
85
|
+
require 'openssl'
|
86
|
+
# create new self signed certificate forhttp fallback
|
87
|
+
private_key = OpenSSL::PKey::RSA.new(1024)
|
88
|
+
cert = OpenSSL::X509::Certificate.new
|
89
|
+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=US/ST=California/L=Emeryville/O=Aspera Inc./OU=Corporate/CN=Aspera Inc./emailAddress=info@asperasoft.com")
|
90
|
+
cert.not_before = Time.now
|
91
|
+
cert.not_after = Time.now + 365 * 24 * 60 * 60
|
92
|
+
cert.public_key = private_key.public_key
|
93
|
+
cert.serial = 0x0
|
94
|
+
cert.version = 2
|
95
|
+
cert.sign(private_key, OpenSSL::Digest::SHA1.new)
|
96
|
+
File.write(file_key,private_key.to_pem)
|
97
|
+
File.write(file_cert,cert.to_pem)
|
98
|
+
File.chmod(0400,file_key)
|
99
|
+
File.chmod(0400,file_cert)
|
100
|
+
end
|
101
|
+
file = k.eql?(:fallback_cert) ? file_cert : file_key
|
102
|
+
else
|
103
|
+
raise "INTERNAL ERROR: #{k}"
|
104
|
+
end
|
105
|
+
raise "no such file: #{file}" unless File.exist?(file)
|
106
|
+
return file
|
107
|
+
end
|
108
|
+
|
109
|
+
# @returns the file path of local connect where API's URI can be read
|
110
|
+
def connect_uri
|
111
|
+
connect=get_product_folders('Aspera Connect')
|
112
|
+
folder=File.join(connect[:run_root],VARRUN_SUBFOLDER)
|
113
|
+
['','s'].each do |ext|
|
114
|
+
uri_file=File.join(folder,"http#{ext}.uri")
|
115
|
+
Log.log.debug("checking connect port file: #{uri_file}")
|
116
|
+
if File.exist?(uri_file)
|
117
|
+
return File.open(uri_file){|f|f.gets}.strip
|
118
|
+
end
|
119
|
+
end
|
120
|
+
raise "no connect uri file found in #{folder}"
|
121
|
+
end
|
122
|
+
|
123
|
+
# @ return path to configuration file of aspera CLI
|
124
|
+
def cli_conf_file
|
125
|
+
connect=get_product_folders('Aspera CLI')
|
126
|
+
return File.join(connect[:app_root],BIN_SUBFOLDER,'.aspera_cli_conf')
|
127
|
+
end
|
128
|
+
|
129
|
+
# add Aspera private keys for web access, token based authorization
|
130
|
+
def bypass_keys
|
131
|
+
return [ "%08x-%04x-%04x-%04x-%04x%08x" % "t1(\xBF;\xF3E\xB5\xAB\x14F\x02\xC6\x7F)P".unpack("NnnnnN"),
|
132
|
+
Installation.instance.path(:ssh_bypass_key_dsa),
|
133
|
+
Installation.instance.path(:ssh_bypass_key_rsa) ]
|
134
|
+
end
|
135
|
+
|
136
|
+
# DEPRECATED ZONE
|
137
|
+
|
138
|
+
def activated;Log.log.warn("deprecated, use ascp_path accessor");nil;end
|
139
|
+
|
140
|
+
def activated=(product_name);Log.log.warn("deprecated, use method use_ascp_from_product");use_ascp_from_product(product_name);end
|
141
|
+
|
142
|
+
def paths;Log.log.warn("deprecated, no replacement");raise "deprecated";end
|
143
|
+
|
144
|
+
def paths=(res_paths)
|
145
|
+
raise "must be a hash" unless res_paths.is_a?(Hash)
|
146
|
+
raise "must have :ascp key" unless res_paths.has_key?(:ascp)
|
147
|
+
Log.log.warn("deprecated, use method: ascp_path=")
|
148
|
+
@ascp_path=res_paths[:ascp]
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
BIN_SUBFOLDER='bin'
|
154
|
+
ETC_SUBFOLDER='etc'
|
155
|
+
VARRUN_SUBFOLDER=File.join('var','run')
|
156
|
+
# product information manifest: XML (part of aspera product)
|
157
|
+
PRODUCT_INFO='product-info.mf'
|
158
|
+
# policy for product selection
|
159
|
+
FIRST_FOUND='FIRST'
|
160
|
+
|
161
|
+
private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO
|
162
|
+
|
163
|
+
# get some specific folder from specific applications: Connect or CLI
|
164
|
+
def get_product_folders(name)
|
165
|
+
found=installed_products.select{|i|i[:expected].eql?(name) or i[:name].eql?(name)}
|
166
|
+
raise "Product: #{name} not found, please install." if found.empty?
|
167
|
+
return found.first
|
168
|
+
end
|
169
|
+
|
170
|
+
def initialize
|
171
|
+
@ascp_path=nil
|
172
|
+
@config_folder='.'
|
173
|
+
@found_products=nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# returns product folders depending on OS
|
177
|
+
# fields
|
178
|
+
# :expected M app name is taken from the manifest if present, else defaults to this value
|
179
|
+
# :app_root M main forlder for the application
|
180
|
+
# :log_root O location of log files (Linux uses syslog)
|
181
|
+
# :run_root O only for Connect Client, location of http port file
|
182
|
+
# :sub_bin O subfolder with executables, default : bin
|
183
|
+
def product_locations
|
184
|
+
case OpenApplication.current_os_type
|
185
|
+
when :windows; return [{
|
186
|
+
:expected =>'Aspera Connect',
|
187
|
+
:app_root =>File.join(ENV['LOCALAPPDATA'],'Programs','Aspera','Aspera Connect'),
|
188
|
+
:log_root =>File.join(ENV['LOCALAPPDATA'],'Aspera','Aspera Connect','var','log'),
|
189
|
+
:run_root =>File.join(ENV['LOCALAPPDATA'],'Aspera','Aspera Connect')
|
190
|
+
},{
|
191
|
+
:expected =>'Aspera CLI',
|
192
|
+
:app_root =>File.join('C:','Program Files','Aspera','cli'),
|
193
|
+
:log_root =>File.join('C:','Program Files','Aspera','cli','var','log'),
|
194
|
+
},{
|
195
|
+
:expected =>'Enterprise Server',
|
196
|
+
:app_root =>File.join('C:','Program Files','Aspera','Enterprise Server'),
|
197
|
+
:log_root =>File.join('C:','Program Files','Aspera','Enterprise Server','var','log'),
|
198
|
+
}]
|
199
|
+
when :mac; return [{
|
200
|
+
:expected =>'Aspera Connect',
|
201
|
+
:app_root =>File.join(Dir.home,'Applications','Aspera Connect.app'),
|
202
|
+
:log_root =>File.join(Dir.home,'Library','Logs','Aspera_Connect'),
|
203
|
+
:run_root =>File.join(Dir.home,'Library','Application Support','Aspera','Aspera Connect'),
|
204
|
+
:sub_bin =>File.join('Contents','Resources'),
|
205
|
+
},{
|
206
|
+
:expected =>'Aspera CLI',
|
207
|
+
:app_root =>File.join(Dir.home,'Applications','Aspera CLI'),
|
208
|
+
:log_root =>File.join(Dir.home,'Library','Logs','Aspera')
|
209
|
+
},{
|
210
|
+
:expected =>'Enterprise Server',
|
211
|
+
:app_root =>File.join('','Library','Aspera'),
|
212
|
+
:log_root =>File.join(Dir.home,'Library','Logs','Aspera'),
|
213
|
+
},{
|
214
|
+
:expected =>'Aspera Drive',
|
215
|
+
:app_root =>File.join('','Applications','Aspera Drive.app'),
|
216
|
+
:log_root =>File.join(Dir.home,'Library','Logs','Aspera_Drive'),
|
217
|
+
:sub_bin =>File.join('Contents','Resources'),
|
218
|
+
}]
|
219
|
+
else; return [{ # other: Linux and unix family
|
220
|
+
:expected =>'Aspera Connect',
|
221
|
+
:app_root =>File.join(Dir.home,'.aspera','connect'),
|
222
|
+
:run_root =>File.join(Dir.home,'.aspera','connect')
|
223
|
+
},{
|
224
|
+
:expected =>'Aspera CLI',
|
225
|
+
:app_root =>File.join(Dir.home,'.aspera','cli'),
|
226
|
+
},{
|
227
|
+
:expected =>'Enterprise Server',
|
228
|
+
:app_root =>File.join('','opt','aspera'),
|
229
|
+
}]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# not pass protected
|
234
|
+
SSH_BYPASS_DSA='eJxtksuSojAAAO98xdypKYFggGOA8HQ0ICBywwiooARRIH79zu55+9qnrurv719M7PrbL3uPvkjsZyjBXyE+/hXfwo/vm+/ZNxEKzSay2zDybHhXub80t7HikDy4Ckta5DgeA4+vVbYyh9znxzbboRYzIWymBAxJsr3z9nCY1X7aEVm0rzJdKQ470q6jCbu0B7rXO6TdJFm5p7iieTnlN0JcSbiC13p6mfqy4QC0EaiMyVht5ou00Lh+l9J5ndbO3DPTR/kUoCcw4bNkoy5GvymbeRR4NYwLwH1nlVaWB7UIZVoFjKHeRaQnL0xU/vFcJX/Rxarz8qS+jQ+GM/moFQlekpAmD1Cn0yO6B4l2lcLMip+gUTxlV/wcHFnh0i0d9Mh8F8rYA8urKu0gZ3d0Pm01Z6GiQHlmPDD8pPGASsJfygmLz+ZHZgRuovS4NDZYwvMkF67Y2r6Na5iC/nGj4emOIG0XIYFuOfXIao4Y9aeS2dOaKXXvidRdh5I2+o5tPKXY1t5h8OiG2xHlH4fqqQbnPGzeUDjkb6aUVLJ6MX4UTPPG0nBFLF4DyHrfYLtY0vPkTDqucjvdbPeJSuaufp72TmI42UX4tIea7SaUUr1uI3QphmlFMMwuTqTPEih0lw35op3AdjJjEdf+AqAe9paDed1JkyckpdbAuzv7P/nznESRXhej8O8xvLX//94fHTzUbwo='
|
235
|
+
# pass protected (but not fips compliant)
|
236
|
+
SSH_BYPASS_RSA='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'
|
237
|
+
private_constant :SSH_BYPASS_DSA,:SSH_BYPASS_RSA
|
238
|
+
end # Installation
|
239
|
+
end
|
240
|
+
end
|