aspera-cli 4.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|