aspera-cli 4.14.0 → 4.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +300 -185
- data/CONTRIBUTING.md +74 -23
- data/README.md +2346 -1619
- data/bin/ascli +16 -25
- data/bin/asession +15 -15
- data/examples/dascli +2 -2
- data/examples/proxy.pac +1 -1
- data/lib/aspera/aoc.rb +216 -150
- data/lib/aspera/ascmd.rb +25 -18
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
- data/lib/aspera/cli/error.rb +17 -0
- data/lib/aspera/cli/extended_value.rb +51 -16
- data/lib/aspera/cli/formatter.rb +276 -174
- data/lib/aspera/cli/hints.rb +81 -0
- data/lib/aspera/cli/main.rb +114 -147
- data/lib/aspera/cli/manager.rb +181 -136
- data/lib/aspera/cli/plugin.rb +82 -64
- data/lib/aspera/cli/plugins/alee.rb +0 -1
- data/lib/aspera/cli/plugins/aoc.rb +327 -331
- data/lib/aspera/cli/plugins/ats.rb +12 -8
- data/lib/aspera/cli/plugins/bss.rb +2 -2
- data/lib/aspera/cli/plugins/config.rb +575 -439
- data/lib/aspera/cli/plugins/console.rb +40 -0
- data/lib/aspera/cli/plugins/cos.rb +4 -5
- data/lib/aspera/cli/plugins/faspex.rb +111 -92
- data/lib/aspera/cli/plugins/faspex5.rb +245 -182
- data/lib/aspera/cli/plugins/node.rb +239 -160
- data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
- data/lib/aspera/cli/plugins/preview.rb +54 -38
- data/lib/aspera/cli/plugins/server.rb +63 -20
- data/lib/aspera/cli/plugins/shares.rb +64 -38
- data/lib/aspera/cli/sync_actions.rb +68 -0
- data/lib/aspera/cli/transfer_agent.rb +64 -67
- data/lib/aspera/cli/transfer_progress.rb +73 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -1
- data/lib/aspera/command_line_builder.rb +27 -22
- data/lib/aspera/cos_node.rb +6 -4
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +21 -8
- data/lib/aspera/fasp/agent_alpha.rb +116 -0
- data/lib/aspera/fasp/agent_base.rb +40 -76
- data/lib/aspera/fasp/agent_connect.rb +21 -22
- data/lib/aspera/fasp/agent_direct.rb +169 -179
- data/lib/aspera/fasp/agent_httpgw.rb +200 -195
- data/lib/aspera/fasp/agent_node.rb +43 -35
- data/lib/aspera/fasp/agent_trsdk.rb +124 -41
- data/lib/aspera/fasp/error_info.rb +2 -2
- data/lib/aspera/fasp/faux_file.rb +52 -0
- data/lib/aspera/fasp/installation.rb +89 -191
- data/lib/aspera/fasp/management.rb +249 -0
- data/lib/aspera/fasp/parameters.rb +86 -47
- data/lib/aspera/fasp/parameters.yaml +75 -8
- data/lib/aspera/fasp/products.rb +162 -0
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +6 -6
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +8 -7
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +51 -0
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/line_logger.rb +23 -0
- data/lib/aspera/log.rb +61 -19
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +105 -21
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +57 -36
- data/lib/aspera/open_application.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +5 -4
- data/lib/aspera/preview/file_types.rb +56 -268
- data/lib/aspera/preview/generator.rb +28 -39
- data/lib/aspera/preview/options.rb +2 -0
- data/lib/aspera/preview/terminal.rb +36 -16
- data/lib/aspera/preview/utils.rb +23 -29
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +127 -80
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +16 -14
- data/lib/aspera/rest_errors_aspera.rb +39 -34
- data/lib/aspera/secret_hider.rb +18 -17
- data/lib/aspera/ssh.rb +10 -5
- data/lib/aspera/temp_file_manager.rb +11 -4
- data/lib/aspera/web_auth.rb +10 -7
- data/lib/aspera/web_server_simple.rb +11 -5
- data.tar.gz.sig +0 -0
- metadata +108 -39
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/listener/line_dump.rb +0 -19
- data/lib/aspera/cli/listener/logger.rb +0 -22
- data/lib/aspera/cli/listener/progress.rb +0 -50
- data/lib/aspera/cli/listener/progress_multi.rb +0 -84
- data/lib/aspera/cli/plugins/sync.rb +0 -44
- data/lib/aspera/fasp/listener.rb +0 -13
- data/lib/aspera/sync.rb +0 -213
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'singleton'
|
4
4
|
require 'aspera/log'
|
5
|
+
require 'aspera/assert'
|
5
6
|
|
6
7
|
module Aspera
|
7
8
|
module Fasp
|
@@ -19,10 +20,10 @@ module Aspera
|
|
19
20
|
def initialize(params=nil)
|
20
21
|
@parameters = DEFAULTS.dup
|
21
22
|
if !params.nil?
|
22
|
-
|
23
|
+
assert_type(params, Hash)
|
23
24
|
params.each do |k, v|
|
24
|
-
|
25
|
-
|
25
|
+
assert_values(k, DEFAULTS.keys){'resume parameter'}
|
26
|
+
assert_type(v, Integer){k}
|
26
27
|
@parameters[k] = v
|
27
28
|
end
|
28
29
|
end
|
@@ -32,7 +33,7 @@ module Aspera
|
|
32
33
|
# calls block a number of times (resumes) until success or limit reached
|
33
34
|
# this is re-entrant, one resumer can handle multiple transfers in //
|
34
35
|
def execute_with_resume
|
35
|
-
|
36
|
+
assert(block_given?)
|
36
37
|
# maximum of retry
|
37
38
|
remaining_resumes = @parameters[:iter_max]
|
38
39
|
sleep_seconds = @parameters[:sleep_initial]
|
@@ -43,9 +44,10 @@ module Aspera
|
|
43
44
|
begin
|
44
45
|
# call provided block
|
45
46
|
yield
|
47
|
+
# exit retry loop if success
|
46
48
|
break
|
47
49
|
rescue Fasp::Error => e
|
48
|
-
Log.log.warn{"An error occurred: #{e.message}"}
|
50
|
+
Log.log.warn{"An error occurred during transfer: #{e.message}"}
|
49
51
|
# failure in ascp
|
50
52
|
if e.retryable?
|
51
53
|
# exit if we exceed the max number of retry
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# cspell:words logdir bidi watchd cooloff asyncadmin
|
4
|
+
|
5
|
+
require 'aspera/command_line_builder'
|
6
|
+
require 'aspera/fasp/installation'
|
7
|
+
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
9
|
+
require 'json'
|
10
|
+
require 'base64'
|
11
|
+
require 'open3'
|
12
|
+
require 'English'
|
13
|
+
|
14
|
+
module Aspera
|
15
|
+
module Fasp
|
16
|
+
# builds command line arg for async
|
17
|
+
module Sync
|
18
|
+
# sync direction, default is push
|
19
|
+
DIRECTIONS = %i[push pull bidi].freeze
|
20
|
+
# custom JSON for async instance command line options
|
21
|
+
PARAMS_VX_INSTANCE =
|
22
|
+
{
|
23
|
+
'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
24
|
+
'watchd' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
25
|
+
'apply_local_docroot' => { cli: { type: :opt_without_arg}},
|
26
|
+
'quiet' => { cli: { type: :opt_without_arg}},
|
27
|
+
'ws_connect' => { cli: { type: :opt_without_arg}}
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
# map sync session parameters to transfer spec: sync -> ts, true if same
|
31
|
+
PARAMS_VX_SESSION =
|
32
|
+
{
|
33
|
+
'name' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
34
|
+
'local_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
35
|
+
'remote_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
36
|
+
'local_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
37
|
+
'remote_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
38
|
+
'host' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_host},
|
39
|
+
'user' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_user},
|
40
|
+
'private_key_paths' => { cli: { type: :opt_with_arg, switch: '--private-key-path'}, accepted_types: :array},
|
41
|
+
'direction' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
42
|
+
'checksum' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
43
|
+
'tags' => { cli: { type: :opt_with_arg, switch: '--tags64', convert: 'Aspera::Fasp::Parameters.convert_json64'},
|
44
|
+
accepted_types: :hash, ts: true},
|
45
|
+
'tcp_port' => { cli: { type: :opt_with_arg}, accepted_types: :int, ts: :ssh_port},
|
46
|
+
'rate_policy' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
47
|
+
'target_rate' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
48
|
+
'cooloff' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
49
|
+
'pending_max' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
50
|
+
'scan_intensity' => { cli: { type: :opt_with_arg}, accepted_types: :string},
|
51
|
+
'cipher' => { cli: { type: :opt_with_arg, convert: 'Aspera::Fasp::Parameters.convert_remove_hyphen'}, accepted_types: :string, ts: true},
|
52
|
+
'transfer_threads' => { cli: { type: :opt_with_arg}, accepted_types: :int},
|
53
|
+
'preserve_time' => { cli: { type: :opt_without_arg}, ts: :preserve_times},
|
54
|
+
'preserve_access_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
55
|
+
'preserve_modification_time' => { cli: { type: :opt_without_arg}, ts: nil},
|
56
|
+
'preserve_uid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_uid},
|
57
|
+
'preserve_gid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_gid},
|
58
|
+
'create_dir' => { cli: { type: :opt_without_arg}, ts: true},
|
59
|
+
'reset' => { cli: { type: :opt_without_arg}},
|
60
|
+
# NOTE: only one env var, but multiple sessions... could be a problem
|
61
|
+
'remote_password' => { cli: { type: :envvar, variable: 'ASPERA_SCP_PASS'}, ts: true},
|
62
|
+
'cookie' => { cli: { type: :envvar, variable: 'ASPERA_SCP_COOKIE'}, ts: true},
|
63
|
+
'token' => { cli: { type: :envvar, variable: 'ASPERA_SCP_TOKEN'}, ts: true},
|
64
|
+
'license' => { cli: { type: :envvar, variable: 'ASPERA_SCP_LICENSE'}}
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_INSTANCE)
|
68
|
+
Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_SESSION)
|
69
|
+
|
70
|
+
PARAMS_VX_KEYS = %w[instance sessions].freeze
|
71
|
+
|
72
|
+
# Translation of transfer spec parameters to async v2 API (asyncs)
|
73
|
+
TS_TO_PARAMS_V2 = {
|
74
|
+
'remote_host' => 'remote.host',
|
75
|
+
'remote_user' => 'remote.user',
|
76
|
+
'remote_password' => 'remote.pass',
|
77
|
+
'sshfp' => 'remote.fingerprint',
|
78
|
+
'ssh_port' => 'remote.port',
|
79
|
+
'wss_port' => 'remote.ws_port',
|
80
|
+
'proxy' => 'remote.proxy',
|
81
|
+
'token' => 'remote.token',
|
82
|
+
'tags' => 'tags'
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
ASYNC_EXECUTABLE = 'async'
|
86
|
+
ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
|
87
|
+
|
88
|
+
private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :TS_TO_PARAMS_V2, :ASYNC_EXECUTABLE, :ASYNC_ADMIN_EXECUTABLE
|
89
|
+
|
90
|
+
class << self
|
91
|
+
# Set remote_dir in sync parameters based on transfer spec
|
92
|
+
# @param params [Hash] sync parameters, old or new format
|
93
|
+
# @param remote_dir_key [String] key to update in above hash
|
94
|
+
# @param transfer_spec [Hash] transfer spec
|
95
|
+
def update_remote_dir(sync_params, remote_dir_key, transfer_spec)
|
96
|
+
if transfer_spec.dig(*%w[tags aspera node file_id])
|
97
|
+
# in AoC, use gen4
|
98
|
+
sync_params[remote_dir_key] = '/'
|
99
|
+
elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
|
100
|
+
# TODO : something more generic, independent of Shares
|
101
|
+
# in Shares, the actual folder on remote end is not always the same as the name of the share
|
102
|
+
actual_remote = transfer_spec['paths']&.first&.[]('source')
|
103
|
+
sync_params[remote_dir_key] = actual_remote if actual_remote
|
104
|
+
end
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def remote_certificates(remote)
|
109
|
+
certificates_to_use = []
|
110
|
+
# use web socket secure for session ?
|
111
|
+
if remote['connect_mode']&.eql?('ws')
|
112
|
+
remote.delete('port')
|
113
|
+
remote.delete('fingerprint')
|
114
|
+
# ignore cert for wss ?
|
115
|
+
if false # @options[:check_ignore]&.call(remote['host'], remote['ws_port'])
|
116
|
+
wss_cert_file = TempFileManager.instance.new_file_path_global('wss_cert')
|
117
|
+
wss_url = "https://#{remote['host']}:#{remote['ws_port']}"
|
118
|
+
File.write(wss_cert_file, Rest.remote_certificates(wss_url))
|
119
|
+
certificates_to_use.push(wss_cert_file)
|
120
|
+
end
|
121
|
+
# set location for CA bundle to be the one of Ruby, see env var SSL_CERT_FILE / SSL_CERT_DIR
|
122
|
+
# certificates_to_use.concat(@options[:trusted_certs]) if @options[:trusted_certs]
|
123
|
+
else
|
124
|
+
# remove unused parameter (avoid warning)
|
125
|
+
remote.delete('ws_port')
|
126
|
+
# add SSH bypass keys when authentication is token and no auth is provided
|
127
|
+
if remote.key?('token') && !remote.key?('pass')
|
128
|
+
certificates_to_use.concat(Installation.instance.aspera_token_ssh_key_paths)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
return certificates_to_use
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param sync_params [Hash] sync parameters, old or new format
|
135
|
+
# @param block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
|
136
|
+
def start(sync_params, &block)
|
137
|
+
assert_type(sync_params, Hash)
|
138
|
+
env_args = {
|
139
|
+
args: [],
|
140
|
+
env: {}
|
141
|
+
}
|
142
|
+
if sync_params.key?('local')
|
143
|
+
remote = sync_params['remote']
|
144
|
+
# async native JSON format (v2)
|
145
|
+
assert_type(remote, Hash)
|
146
|
+
# get transfer spec if possible, and feed back to new structure
|
147
|
+
if block
|
148
|
+
transfer_spec = yield((sync_params['direction'] || 'push').to_sym, sync_params['local']['path'], remote['path'])
|
149
|
+
# async native JSON format
|
150
|
+
assert_type(sync_params['local'], Hash)
|
151
|
+
# translate transfer spec to async parameters
|
152
|
+
TS_TO_PARAMS_V2.each do |ts_param, sy_path|
|
153
|
+
next unless transfer_spec.key?(ts_param)
|
154
|
+
sy_dig = sy_path.split('.')
|
155
|
+
param = sy_dig.pop
|
156
|
+
hash = sy_dig.empty? ? sync_params : sync_params[sy_dig.first]
|
157
|
+
hash = sync_params[sy_dig.first] = {} if hash.nil?
|
158
|
+
hash[param] = transfer_spec[ts_param]
|
159
|
+
end
|
160
|
+
update_remote_dir(remote, 'path', transfer_spec)
|
161
|
+
end
|
162
|
+
remote['connect_mode'] ||= remote.key?('ws_port') ? 'ws' : 'ssh'
|
163
|
+
add_certificates = remote_certificates(remote)
|
164
|
+
if !add_certificates.empty?
|
165
|
+
remote['private_key_paths'] ||= []
|
166
|
+
remote['private_key_paths'].concat(add_certificates)
|
167
|
+
end
|
168
|
+
assert_type(sync_params, Hash)
|
169
|
+
env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
|
170
|
+
elsif sync_params.key?('sessions')
|
171
|
+
# ascli JSON format (v1)
|
172
|
+
if block
|
173
|
+
sync_params['sessions'].each do |session|
|
174
|
+
transfer_spec = yield((session['direction'] || 'push').to_sym, session['local_dir'], session['remote_dir'])
|
175
|
+
PARAMS_VX_SESSION.each do |async_param, behavior|
|
176
|
+
if behavior.key?(:ts)
|
177
|
+
tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
|
178
|
+
session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
session['private_key_paths'] = Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
|
182
|
+
update_remote_dir(session, 'remote_dir', transfer_spec)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
|
186
|
+
sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
|
187
|
+
assert_type(sync_params['sessions'], Array)
|
188
|
+
assert_type(sync_params['sessions'].first, Hash)
|
189
|
+
if sync_params.key?('instance')
|
190
|
+
assert_type(sync_params['instance'], Hash)
|
191
|
+
instance_builder = Aspera::CommandLineBuilder.new(sync_params['instance'], PARAMS_VX_INSTANCE)
|
192
|
+
instance_builder.process_params
|
193
|
+
instance_builder.add_env_args(env_args)
|
194
|
+
end
|
195
|
+
|
196
|
+
sync_params['sessions'].each do |session_params|
|
197
|
+
assert_type(session_params, Hash)
|
198
|
+
assert(session_params.key?('name')){'session must contain at least name'}
|
199
|
+
session_builder = Aspera::CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
|
200
|
+
session_builder.process_params
|
201
|
+
session_builder.add_env_args(env_args)
|
202
|
+
end
|
203
|
+
else
|
204
|
+
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
205
|
+
end
|
206
|
+
Log.log.debug{Log.dump(:sync_params, sync_params)}
|
207
|
+
Log.log.debug{"execute: #{env_args[:env].map{|k, v| "#{k}=\"#{v}\""}.join(' ')} \"#{ASYNC_EXECUTABLE}\" \"#{env_args[:args].join('" "')}\""}
|
208
|
+
res = system(env_args[:env], [ASYNC_EXECUTABLE, ASYNC_EXECUTABLE], *env_args[:args])
|
209
|
+
Log.log.debug{"result=#{res}"}
|
210
|
+
case res
|
211
|
+
when true then return nil
|
212
|
+
when false then raise "failed: #{$CHILD_STATUS}"
|
213
|
+
when nil then raise "not started: #{$CHILD_STATUS}"
|
214
|
+
else error_unexpected_value(res)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def parse_status(stdout)
|
219
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
220
|
+
result = {}
|
221
|
+
ids = nil
|
222
|
+
stdout.split("\n").each do |line|
|
223
|
+
info = line.split(':', 2).map(&:lstrip)
|
224
|
+
if info[1].eql?('')
|
225
|
+
info[1] = ids = []
|
226
|
+
elsif info[1].nil?
|
227
|
+
ids.push(info[0])
|
228
|
+
next
|
229
|
+
end
|
230
|
+
result[info[0]] = info[1]
|
231
|
+
end
|
232
|
+
return result
|
233
|
+
end
|
234
|
+
|
235
|
+
def admin_status(sync_params, session_name)
|
236
|
+
command_line = [ASYNC_ADMIN_EXECUTABLE, '--quiet']
|
237
|
+
if sync_params.key?('local')
|
238
|
+
assert(!sync_params['name'].nil?){'Missing session name'}
|
239
|
+
assert(session_name.nil? || session_name.eql?(sync_params['name'])){'Session not found'}
|
240
|
+
command_line.push("--name=#{sync_params['name']}")
|
241
|
+
if sync_params.key?('local_db_dir')
|
242
|
+
command_line.push("--local-db-dir=#{sync_params['local_db_dir']}")
|
243
|
+
elsif sync_params.dig('local', 'path')
|
244
|
+
command_line.push("--local-dir=#{sync_params.dig('local', 'path')}")
|
245
|
+
else
|
246
|
+
raise 'Missing either local_db_dir or local.path'
|
247
|
+
end
|
248
|
+
elsif sync_params.key?('sessions')
|
249
|
+
session = session_name.nil? ? sync_params['sessions'].first : sync_params['sessions'].find{|s|s['name'].eql?(session_name)}
|
250
|
+
raise "Session #{session_name} not found in #{sync_params['sessions'].map{|s|s['name']}.join(',')}" if session.nil?
|
251
|
+
raise 'Missing session name' if session['name'].nil?
|
252
|
+
command_line.push("--name=#{session['name']}")
|
253
|
+
if session.key?('local_db_dir')
|
254
|
+
command_line.push("--local-db-dir=#{session['local_db_dir']}")
|
255
|
+
elsif session.key?('local_dir')
|
256
|
+
command_line.push("--local-dir=#{session['local_dir']}")
|
257
|
+
else
|
258
|
+
raise 'Missing either local_db_dir or local_dir'
|
259
|
+
end
|
260
|
+
else
|
261
|
+
raise 'At least one of `local` or `sessions` must be present in async parameters'
|
262
|
+
end
|
263
|
+
Log.log.debug{"execute: #{command_line.join(' ')}"}
|
264
|
+
stdout, stderr, status = Open3.capture3(*command_line)
|
265
|
+
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
266
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
267
|
+
raise "Sync failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
268
|
+
return parse_status(stdout)
|
269
|
+
end
|
270
|
+
end # end self
|
271
|
+
end # end Sync
|
272
|
+
end # end Fasp
|
273
|
+
end # end Aspera
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'aspera/fasp/parameters'
|
4
|
+
require 'aspera/assert'
|
4
5
|
|
5
6
|
module Aspera
|
6
7
|
module Fasp
|
@@ -27,22 +28,23 @@ module Aspera
|
|
27
28
|
end
|
28
29
|
class << self
|
29
30
|
def action_to_direction(tspec, command)
|
30
|
-
|
31
|
+
assert_type(tspec, Hash){'transfer spec'}
|
31
32
|
tspec['direction'] = case command.to_sym
|
32
33
|
when :upload then DIRECTION_SEND
|
33
34
|
when :download then DIRECTION_RECEIVE
|
34
|
-
else
|
35
|
+
else error_unexpected_value(command.to_sym)
|
35
36
|
end
|
36
37
|
return tspec
|
37
38
|
end
|
38
39
|
|
39
40
|
def action(tspec)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
assert_type(tspec, Hash){'transfer spec'}
|
42
|
+
assert_values(tspec['direction'], [DIRECTION_SEND, DIRECTION_RECEIVE]){'direction'}
|
43
|
+
case tspec['direction']
|
44
|
+
when DIRECTION_SEND then :upload
|
45
|
+
when DIRECTION_RECEIVE then :download
|
46
|
+
else error_unexpected_value(tspec['direction'])
|
47
|
+
end
|
46
48
|
end
|
47
49
|
end
|
48
50
|
end
|
data/lib/aspera/fasp/uri.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate
|
3
|
+
# cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate faspe
|
4
4
|
|
5
5
|
require 'aspera/log'
|
6
|
+
require 'aspera/rest'
|
6
7
|
require 'aspera/command_line_builder'
|
7
8
|
|
8
9
|
module Aspera
|
9
10
|
module Fasp
|
10
|
-
# translates a "faspe:" URI (used in Faspex 4) into transfer spec
|
11
|
+
# translates a "faspe:" URI (used in Faspex 4) into transfer spec (Hash)
|
11
12
|
class Uri
|
13
|
+
SCHEME = 'faspe'
|
12
14
|
def initialize(fasp_link)
|
13
15
|
@fasp_uri = URI.parse(fasp_link.gsub(' ', '%20'))
|
14
|
-
# TODO: check scheme is faspe
|
16
|
+
# TODO: check scheme is 'faspe'
|
15
17
|
end
|
16
18
|
|
17
19
|
def transfer_spec
|
@@ -23,9 +25,7 @@ module Aspera
|
|
23
25
|
# faspex does not encode trailing base64 padding, fix that to be able to decode properly
|
24
26
|
fixed_query = @fasp_uri.query.gsub(/(=+)$/){|x|'%3D' * x.length}
|
25
27
|
|
26
|
-
|
27
|
-
name = i[0]
|
28
|
-
value = i[1]
|
28
|
+
Rest.decode_query(fixed_query).each do |name, value|
|
29
29
|
case name
|
30
30
|
when 'cookie' then result_ts['cookie'] = value
|
31
31
|
when 'token' then result_ts['token'] = value
|
data/lib/aspera/faspex_gw.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'aspera/web_server_simple'
|
4
3
|
require 'aspera/log'
|
4
|
+
require 'aspera/assert'
|
5
|
+
require 'webrick'
|
5
6
|
require 'json'
|
6
7
|
|
7
8
|
module Aspera
|
8
|
-
#
|
9
|
+
# Simulate the Faspex 4 /send API and creates a package on Aspera on Cloud or Faspex 5
|
9
10
|
class Faspex4GWServlet < WEBrick::HTTPServlet::AbstractServlet
|
10
11
|
# @param app_api [Aspera::AoC]
|
11
12
|
# @param app_context [String]
|
12
13
|
def initialize(server, app_api, app_context)
|
14
|
+
assert_values(app_api.class.name, ['Aspera::AoC', 'Aspera::Rest'])
|
13
15
|
super(server)
|
14
16
|
# typed: Aspera::AoC
|
15
17
|
@app_api = app_api
|
@@ -67,13 +69,14 @@ module Aspera
|
|
67
69
|
raise 'no payload' if request.body.nil?
|
68
70
|
faspex_pkg_parameters = JSON.parse(request.body)
|
69
71
|
Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
|
72
|
+
# compare string, as class is not yet known here
|
70
73
|
faspex_package_create_result =
|
71
|
-
|
74
|
+
case @app_api.class.name
|
75
|
+
when 'Aspera::AoC'
|
72
76
|
faspex4_send_to_aoc(faspex_pkg_parameters)
|
73
|
-
|
77
|
+
when 'Aspera::Rest'
|
74
78
|
faspex4_send_to_faspex5(faspex_pkg_parameters)
|
75
|
-
else
|
76
|
-
raise "No such adapter: #{@app_api.class}"
|
79
|
+
else error_unexpected_value(@app_api.class.name)
|
77
80
|
end
|
78
81
|
Log.log.info{"faspex_package_create_result=#{faspex_package_create_result}"}
|
79
82
|
response.status = 200
|
@@ -89,8 +92,8 @@ module Aspera
|
|
89
92
|
else
|
90
93
|
response.status = 400
|
91
94
|
response['Content-Type'] = 'application/json'
|
92
|
-
response.body = {error: '
|
95
|
+
response.body = {error: 'Unsupported endpoint'}.to_json
|
93
96
|
end
|
94
97
|
end
|
95
98
|
end # Faspex4GWServlet
|
96
|
-
end #
|
99
|
+
end # Aspera
|
@@ -1,19 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'English'
|
4
|
-
require 'aspera/web_server_simple'
|
5
|
-
require 'aspera/log'
|
6
3
|
require 'json'
|
7
4
|
require 'timeout'
|
5
|
+
require 'English'
|
6
|
+
require 'webrick'
|
7
|
+
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
8
9
|
|
9
10
|
module Aspera
|
10
11
|
# this class answers the Faspex /send API and creates a package on Aspera on Cloud
|
11
12
|
class Faspex4PostProcServlet < WEBrick::HTTPServlet::AbstractServlet
|
12
13
|
ALLOWED_PARAMETERS = %i[root script_folder fail_on_error timeout_seconds].freeze
|
13
14
|
def initialize(server, parameters)
|
14
|
-
|
15
|
+
assert_type(parameters, Hash)
|
15
16
|
@parameters = parameters.symbolize_keys
|
16
|
-
Log.dump(:post_proc_parameters, @parameters)
|
17
|
+
Log.log.debug{Log.dump(:post_proc_parameters, @parameters)}
|
17
18
|
raise "unexpected key in parameters config: only: #{ALLOWED_PARAMETERS.join(', ')}" if @parameters.keys.any?{|k|!ALLOWED_PARAMETERS.include?(k)}
|
18
19
|
@parameters[:script_folder] ||= '.'
|
19
20
|
@parameters[:fail_on_error] ||= false
|
@@ -44,7 +45,7 @@ module Aspera
|
|
44
45
|
script_path = File.join(@parameters[:script_folder], script_file)
|
45
46
|
Log.log.debug{"script=#{script_path}"}
|
46
47
|
webhook_parameters = JSON.parse(request.body)
|
47
|
-
Log.dump(:webhook_parameters, webhook_parameters)
|
48
|
+
Log.log.debug{Log.dump(:webhook_parameters, webhook_parameters)}
|
48
49
|
# env expects only strings
|
49
50
|
environment = webhook_parameters.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
|
50
51
|
post_proc_pid = Process.spawn(environment, [script_path, script_path])
|
@@ -74,4 +75,4 @@ module Aspera
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
end # Faspex4PostProcServlet
|
77
|
-
end #
|
78
|
+
end # Aspera
|
data/lib/aspera/hash_ext.rb
CHANGED
@@ -24,8 +24,8 @@ end
|
|
24
24
|
unless Hash.method_defined?(:transform_keys)
|
25
25
|
class Hash
|
26
26
|
def transform_keys
|
27
|
-
|
28
|
-
|
27
|
+
raise 'missing block' unless block_given?
|
28
|
+
return each_with_object({}){|(k, v), memo|memo[yield(k)] = v}
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
data/lib/aspera/id_generator.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/assert'
|
3
4
|
require 'uri'
|
4
5
|
|
5
6
|
module Aspera
|
@@ -11,11 +12,12 @@ module Aspera
|
|
11
12
|
class << self
|
12
13
|
def from_list(object_id)
|
13
14
|
if object_id.is_a?(Array)
|
15
|
+
# compact: remove nils
|
14
16
|
object_id = object_id.compact.map do |i|
|
15
17
|
i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
|
16
18
|
end.join(ID_SEPARATOR)
|
17
19
|
end
|
18
|
-
|
20
|
+
assert_type(object_id, String)
|
19
21
|
return object_id
|
20
22
|
.gsub(WINDOWS_PROTECTED_CHAR, PROTECTED_CHAR_REPLACE) # remove windows forbidden chars
|
21
23
|
.gsub('.', PROTECTED_CHAR_REPLACE) # keep dot for extension only (nicer)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# cspell:ignore blankslate
|
4
|
+
|
5
|
+
require 'aspera/rest_error_analyzer'
|
6
|
+
require 'aspera/assert'
|
7
|
+
require 'blankslate'
|
8
|
+
|
9
|
+
Aspera::RestErrorAnalyzer.instance.add_simple_handler(name: 'JSON RPC', path: %w[error message], always: true)
|
10
|
+
|
11
|
+
module Aspera
|
12
|
+
# a very simple JSON RPC client
|
13
|
+
class JsonRpcClient < BlankSlate
|
14
|
+
JSON_RPC_VERSION = '2.0'
|
15
|
+
reveal :instance_variable_get
|
16
|
+
reveal :inspect
|
17
|
+
reveal :to_s
|
18
|
+
|
19
|
+
def initialize(api, namespace = nil)
|
20
|
+
super()
|
21
|
+
@api = api
|
22
|
+
@namespace = namespace
|
23
|
+
@request_id = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing?(sym, include_private = false)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method, *args, &block)
|
31
|
+
args = args.first if args.size == 1 && args.first.is_a?(Hash)
|
32
|
+
data = @api.create('', {
|
33
|
+
jsonrpc: JSON_RPC_VERSION,
|
34
|
+
method: "#{@namespace}#{method}",
|
35
|
+
params: args,
|
36
|
+
id: @request_id += 1
|
37
|
+
})[:data]
|
38
|
+
assert_type(data, Hash){'response'}
|
39
|
+
assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
|
40
|
+
assert(data.key?('id')){'missing id in response'}
|
41
|
+
assert(!(data.key?('error') && data.key?('result'))){'both error and response'}
|
42
|
+
assert(
|
43
|
+
!data.key?('error') ||
|
44
|
+
data['error'].is_a?(Hash) &&
|
45
|
+
data['error']['code'].is_a?(Integer) &&
|
46
|
+
data['error']['message'].is_a?(String)
|
47
|
+
){'bad error response'}
|
48
|
+
return data['result']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'aspera/hash_ext'
|
4
4
|
require 'aspera/environment'
|
5
|
+
require 'aspera/log'
|
6
|
+
require 'aspera/assert'
|
5
7
|
require 'symmetric_encryption/core'
|
6
8
|
require 'yaml'
|
7
9
|
|
@@ -9,33 +11,66 @@ module Aspera
|
|
9
11
|
module Keychain
|
10
12
|
# Manage secrets in a simple Hash
|
11
13
|
class EncryptedHash
|
12
|
-
|
14
|
+
LEGACY_CIPHER_NAME = 'aes-256-cbc'
|
15
|
+
DEFAULT_CIPHER_NAME = 'aes-256-cbc'
|
16
|
+
FILE_TYPE = 'encrypted_hash_vault'
|
13
17
|
CONTENT_KEYS = %i[label username password url description].freeze
|
18
|
+
FILE_KEYS = %w[version type cipher data].sort.freeze
|
14
19
|
def initialize(path, current_password)
|
20
|
+
assert_type(path, String){'path to vault file'}
|
15
21
|
@path = path
|
22
|
+
@all_secrets = {}
|
23
|
+
vault_encrypted_data = nil
|
24
|
+
if File.exist?(@path)
|
25
|
+
vault_file = File.read(@path)
|
26
|
+
if vault_file.start_with?('---')
|
27
|
+
vault_info = YAML.parse(vault_file).to_ruby
|
28
|
+
assert(vault_info.keys.sort == FILE_KEYS){'Invalid vault file'}
|
29
|
+
@cipher_name = vault_info['cipher']
|
30
|
+
vault_encrypted_data = vault_info['data']
|
31
|
+
else
|
32
|
+
# legacy vault file
|
33
|
+
@cipher_name = LEGACY_CIPHER_NAME
|
34
|
+
vault_encrypted_data = File.read(@path, mode: 'rb')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# setting password also creates the cipher
|
16
38
|
self.password = current_password
|
17
|
-
|
18
|
-
|
39
|
+
if !vault_encrypted_data.nil?
|
40
|
+
@all_secrets = YAML.load_stream(@cipher.decrypt(vault_encrypted_data)).first
|
41
|
+
end
|
19
42
|
end
|
20
43
|
|
44
|
+
# set the password and cipher
|
21
45
|
def password=(new_password)
|
22
46
|
# number of bits in second position
|
23
|
-
key_bytes =
|
47
|
+
key_bytes = DEFAULT_CIPHER_NAME.split('-')[1].to_i / Environment::BITS_PER_BYTE
|
24
48
|
# derive key from passphrase, add trailing zeros
|
25
49
|
key = "#{new_password}#{"\x0" * key_bytes}"[0..(key_bytes - 1)]
|
26
|
-
Log.log.
|
27
|
-
|
50
|
+
Log.log.trace1{"secret=[#{key}],#{key.length}"}
|
51
|
+
@cipher = SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(cipher_name: DEFAULT_CIPHER_NAME, key: key, encoding: :none)
|
28
52
|
end
|
29
53
|
|
54
|
+
# save current data to file with format
|
30
55
|
def save
|
31
|
-
|
56
|
+
vault_info = {
|
57
|
+
'version' => '1.0.0',
|
58
|
+
'type' => FILE_TYPE,
|
59
|
+
'cipher' => @cipher_name,
|
60
|
+
'data' => @cipher.encrypt(YAML.dump(@all_secrets))
|
61
|
+
}
|
62
|
+
File.write(@path, YAML.dump(vault_info))
|
32
63
|
end
|
33
64
|
|
65
|
+
# set a secret
|
66
|
+
# @param options [Hash] with keys :label, :username, :password, :url, :description
|
34
67
|
def set(options)
|
35
|
-
|
68
|
+
assert_type(options, Hash){'options'}
|
36
69
|
unsupported = options.keys - CONTENT_KEYS
|
37
|
-
|
38
|
-
|
70
|
+
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
71
|
+
options.each_pair do |k, v|
|
72
|
+
assert_type(v, String){k.to_s}
|
73
|
+
end
|
39
74
|
label = options.delete(:label)
|
40
75
|
raise "secret #{label} already exist, delete first" if @all_secrets.key?(label)
|
41
76
|
@all_secrets[label] = options.symbolize_keys
|
@@ -59,7 +94,7 @@ module Aspera
|
|
59
94
|
end
|
60
95
|
|
61
96
|
def get(label:, exception: true)
|
62
|
-
|
97
|
+
assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
|
63
98
|
result = @all_secrets[label].clone
|
64
99
|
result[:label] = label if result.is_a?(Hash)
|
65
100
|
return result
|