aspera-cli 4.15.0 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +375 -280
  5. data/CONTRIBUTING.md +71 -18
  6. data/README.md +1978 -1656
  7. data/bin/ascli +13 -31
  8. data/bin/asession +32 -22
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/agent/alpha.rb +117 -0
  11. data/lib/aspera/agent/base.rb +61 -0
  12. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  13. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
  14. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
  15. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
  16. data/lib/aspera/agent/trsdk.rb +188 -0
  17. data/lib/aspera/api/aoc.rb +586 -0
  18. data/lib/aspera/api/ats.rb +46 -0
  19. data/lib/aspera/api/cos_node.rb +95 -0
  20. data/lib/aspera/api/node.rb +344 -0
  21. data/lib/aspera/ascmd.rb +47 -14
  22. data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
  23. data/lib/aspera/{fasp → ascp}/management.rb +14 -14
  24. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  25. data/lib/aspera/assert.rb +45 -0
  26. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  27. data/lib/aspera/cli/extended_value.rb +5 -5
  28. data/lib/aspera/cli/formatter.rb +27 -14
  29. data/lib/aspera/cli/hints.rb +7 -6
  30. data/lib/aspera/cli/main.rb +49 -29
  31. data/lib/aspera/cli/manager.rb +46 -36
  32. data/lib/aspera/cli/plugin.rb +34 -20
  33. data/lib/aspera/cli/plugin_factory.rb +61 -0
  34. data/lib/aspera/cli/plugins/alee.rb +7 -7
  35. data/lib/aspera/cli/plugins/aoc.rb +168 -132
  36. data/lib/aspera/cli/plugins/ats.rb +33 -33
  37. data/lib/aspera/cli/plugins/bss.rb +3 -4
  38. data/lib/aspera/cli/plugins/config.rb +250 -272
  39. data/lib/aspera/cli/plugins/console.rb +8 -6
  40. data/lib/aspera/cli/plugins/cos.rb +20 -19
  41. data/lib/aspera/cli/plugins/faspex.rb +71 -60
  42. data/lib/aspera/cli/plugins/faspex5.rb +212 -133
  43. data/lib/aspera/cli/plugins/node.rb +83 -75
  44. data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
  45. data/lib/aspera/cli/plugins/preview.rb +33 -31
  46. data/lib/aspera/cli/plugins/server.rb +33 -32
  47. data/lib/aspera/cli/plugins/shares.rb +39 -33
  48. data/lib/aspera/cli/sync_actions.rb +9 -9
  49. data/lib/aspera/cli/transfer_agent.rb +45 -25
  50. data/lib/aspera/cli/transfer_progress.rb +2 -3
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/colors.rb +5 -0
  53. data/lib/aspera/command_line_builder.rb +16 -14
  54. data/lib/aspera/coverage.rb +21 -0
  55. data/lib/aspera/data_repository.rb +33 -2
  56. data/lib/aspera/environment.rb +5 -4
  57. data/lib/aspera/faspex_gw.rb +13 -11
  58. data/lib/aspera/faspex_postproc.rb +6 -5
  59. data/lib/aspera/id_generator.rb +4 -2
  60. data/lib/aspera/json_rpc.rb +10 -8
  61. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  62. data/lib/aspera/keychain/macos_security.rb +29 -22
  63. data/lib/aspera/log.rb +5 -4
  64. data/lib/aspera/nagios.rb +7 -2
  65. data/lib/aspera/node_simulator.rb +213 -0
  66. data/lib/aspera/oauth/base.rb +143 -0
  67. data/lib/aspera/oauth/factory.rb +124 -0
  68. data/lib/aspera/oauth/generic.rb +34 -0
  69. data/lib/aspera/oauth/jwt.rb +51 -0
  70. data/lib/aspera/oauth/url_json.rb +31 -0
  71. data/lib/aspera/oauth/web.rb +50 -0
  72. data/lib/aspera/oauth.rb +5 -328
  73. data/lib/aspera/open_application.rb +7 -7
  74. data/lib/aspera/persistency_action_once.rb +13 -14
  75. data/lib/aspera/persistency_folder.rb +3 -2
  76. data/lib/aspera/preview/file_types.rb +53 -267
  77. data/lib/aspera/preview/generator.rb +7 -5
  78. data/lib/aspera/preview/terminal.rb +17 -7
  79. data/lib/aspera/preview/utils.rb +8 -7
  80. data/lib/aspera/proxy_auto_config.rb +6 -3
  81. data/lib/aspera/rest.rb +187 -140
  82. data/lib/aspera/rest_error_analyzer.rb +1 -0
  83. data/lib/aspera/rest_errors_aspera.rb +5 -3
  84. data/lib/aspera/resumer.rb +77 -0
  85. data/lib/aspera/secret_hider.rb +5 -2
  86. data/lib/aspera/ssh.rb +15 -8
  87. data/lib/aspera/temp_file_manager.rb +1 -1
  88. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  89. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  90. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  91. data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
  92. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
  93. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  94. data/lib/aspera/transfer/sync.rb +273 -0
  95. data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
  96. data/lib/aspera/web_server_simple.rb +12 -3
  97. data.tar.gz.sig +0 -0
  98. metadata +92 -68
  99. metadata.gz.sig +0 -0
  100. data/lib/aspera/aoc.rb +0 -606
  101. data/lib/aspera/ats_api.rb +0 -47
  102. data/lib/aspera/cos_node.rb +0 -93
  103. data/lib/aspera/fasp/agent_aspera.rb +0 -126
  104. data/lib/aspera/fasp/agent_base.rb +0 -48
  105. data/lib/aspera/fasp/agent_trsdk.rb +0 -146
  106. data/lib/aspera/fasp/resume_policy.rb +0 -77
  107. data/lib/aspera/node.rb +0 -338
  108. data/lib/aspera/sync.rb +0 -219
data/lib/aspera/node.rb DELETED
@@ -1,338 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'aspera/cli/error'
4
- require 'aspera/fasp/transfer_spec'
5
- require 'aspera/rest'
6
- require 'aspera/oauth'
7
- require 'aspera/log'
8
- require 'aspera/environment'
9
- require 'zlib'
10
- require 'base64'
11
-
12
- module Aspera
13
- # Provides additional functions using node API with gen4 extensions (access keys)
14
- class Node < Aspera::Rest
15
- # permissions
16
- ACCESS_LEVELS = %w[delete list mkdir preview read rename write].freeze
17
- # prefix for ruby code for filter (deprecated)
18
- MATCH_EXEC_PREFIX = 'exec:'
19
- MATCH_TYPES = [String, Proc, Regexp, NilClass].freeze
20
- HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
21
- PATH_SEPARATOR = '/'
22
- TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
23
- SCOPE_USER = 'user:all'
24
- SCOPE_ADMIN = 'admin:all'
25
- SCOPE_PREFIX = 'node.'
26
- SCOPE_SEPARATOR = ':'
27
- SIGNATURE_DELIMITER = '==SIGNATURE=='
28
- BEARER_TOKEN_VALIDITY_DEFAULT = 86400
29
- BEARER_TOKEN_SCOPE_DEFAULT = SCOPE_USER
30
-
31
- # register node special token decoder
32
- Oauth.register_decoder(lambda{|token|Node.decode_bearer_token(token)})
33
-
34
- # class instance variable, access with accessors on class
35
- @use_standard_ports = true
36
-
37
- class << self
38
- attr_accessor :use_standard_ports
39
-
40
- # For access keys: provide expression to match entry in folder
41
- def file_matcher(match_expression)
42
- case match_expression
43
- when Proc then return match_expression
44
- when Regexp then return ->(f){f['name'].match?(match_expression)}
45
- when String
46
- if match_expression.start_with?(MATCH_EXEC_PREFIX)
47
- code = "->(f){#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
48
- Log.log.warn{"Use of prefix #{MATCH_EXEC_PREFIX} is deprecated (4.15), instead use: @ruby:'#{code}'"}
49
- return Environment.secure_eval(code, __FILE__, __LINE__)
50
- end
51
- return lambda{|f|File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
52
- when NilClass then return ->(_){true}
53
- else raise Cli::BadArgument, "Invalid match expression type: #{match_expression.class}"
54
- end
55
- end
56
-
57
- def file_matcher_from_argument(options)
58
- return file_matcher(options.get_next_argument('filter', type: MATCH_TYPES, mandatory: false))
59
- end
60
-
61
- # node API scopes
62
- def token_scope(access_key, scope)
63
- return [SCOPE_PREFIX, access_key, SCOPE_SEPARATOR, scope].join('')
64
- end
65
-
66
- def decode_scope(scope)
67
- items = scope.split(SCOPE_SEPARATOR, 2)
68
- raise "invalid scope: #{scope}" unless items.length.eql?(2)
69
- raise "invalid scope: #{scope}" unless items[0].start_with?(SCOPE_PREFIX)
70
- return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
71
- end
72
-
73
- # Create an Aspera Node bearer token
74
- # @param payload [String] JSON payload to be included in the token
75
- # @param private_key [OpenSSL::PKey::RSA] Private key to sign the token
76
- def bearer_token(access_key:, payload:, private_key:)
77
- raise 'payload shall be Hash' unless payload.is_a?(Hash)
78
- raise 'missing user_id' unless payload.key?('user_id')
79
- raise 'user_id must be a String' unless payload['user_id'].is_a?(String)
80
- raise 'user_id must not be empty' if payload['user_id'].empty?
81
- raise 'private_key shall be OpenSSL::PKey::RSA' unless private_key.is_a?(OpenSSL::PKey::RSA)
82
- # manage convenience parameters
83
- expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
84
- payload.delete('_validity')
85
- scope = payload['_scope'] || BEARER_TOKEN_SCOPE_DEFAULT
86
- payload.delete('_scope')
87
- payload['scope'] ||= token_scope(access_key, scope)
88
- payload['auth_type'] ||= 'access_key'
89
- payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
90
- payload_json = JSON.generate(payload)
91
- return Base64.strict_encode64(Zlib::Deflate.deflate([
92
- payload_json,
93
- SIGNATURE_DELIMITER,
94
- Base64.strict_encode64(private_key.sign(OpenSSL::Digest.new('sha512'), payload_json)).scan(/.{1,60}/).join("\n"),
95
- ''
96
- ].join("\n")))
97
- end
98
-
99
- def decode_bearer_token(token)
100
- return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition(SIGNATURE_DELIMITER).first)
101
- end
102
-
103
- def bearer_headers(bearer_auth, access_key: nil)
104
- # if username is not provided, use the access key from the token
105
- if access_key.nil?
106
- access_key = Aspera::Node.decode_scope(Aspera::Node.decode_bearer_token(Oauth.bearer_extract(bearer_auth))['scope'])[:access_key]
107
- raise "internal error #{access_key}" if access_key.nil?
108
- end
109
- return {
110
- Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => access_key,
111
- 'Authorization' => bearer_auth
112
- }
113
- end
114
- end
115
-
116
- # fields in @app_info
117
- REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
118
- # methods of @app_info[:api]
119
- REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
120
- private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
121
-
122
- attr_reader :app_info
123
-
124
- # @param params [Hash] Rest parameters
125
- # @param app_info [Hash,NilClass] special processing for AoC
126
- def initialize(params:, app_info: nil, add_tspec: nil)
127
- # init Rest
128
- super(params)
129
- @app_info = app_info
130
- # this is added to transfer spec, for instance to add tags (COS)
131
- @add_tspec = add_tspec
132
- if !@app_info.nil?
133
- REQUIRED_APP_INFO_FIELDS.each do |field|
134
- raise "INTERNAL ERROR: app_info lacks field #{field}" unless @app_info.key?(field)
135
- end
136
- REQUIRED_APP_API_METHODS.each do |method|
137
- raise "INTERNAL ERROR: #{@app_info[:api].class} lacks method #{method}" unless @app_info[:api].respond_to?(method)
138
- end
139
- end
140
- end
141
-
142
- # update transfer spec with special additional tags
143
- def add_tspec_info(tspec)
144
- tspec.deep_merge!(@add_tspec) unless @add_tspec.nil?
145
- return tspec
146
- end
147
-
148
- # @returns [Aspera::Node] a Node or nil
149
- def node_id_to_node(node_id)
150
- if !@app_info.nil?
151
- return self if node_id.eql?(@app_info[:node_info]['id'])
152
- return @app_info[:api].node_api_from(
153
- node_id: node_id,
154
- workspace_id: @app_info[:workspace_id],
155
- workspace_name: @app_info[:workspace_name])
156
- end
157
- Log.log.warn{"cannot resolve link with node id #{node_id}"}
158
- return nil
159
- end
160
-
161
- # Recursively browse in a folder (with non-recursive method)
162
- # sub folders are processed if the processing method returns true
163
- # @param state [Object] state object sent to processing method
164
- # @param top_file_id [String] file id to start at (default = access key root file id)
165
- # @param top_file_path [String] path of top folder (default = /)
166
- # @param block [Proc] processing method, arguments: entry, path, state
167
- def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
168
- raise 'INTERNAL ERROR: top_file_path not set' if top_file_path.nil?
169
- raise 'INTERNAL ERROR: Missing block' unless block
170
- # start at top folder
171
- folders_to_explore = [{id: top_file_id, path: top_file_path}]
172
- Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
173
- until folders_to_explore.empty?
174
- current_item = folders_to_explore.shift
175
- Log.log.debug{"searching #{current_item[:path]}".bg_green}
176
- # get folder content
177
- folder_contents =
178
- begin
179
- read("files/#{current_item[:id]}/files")[:data]
180
- rescue StandardError => e
181
- Log.log.warn{"#{current_item[:path]}: #{e.class} #{e.message}"}
182
- []
183
- end
184
- Log.log.debug{Log.dump(:folder_contents, folder_contents)}
185
- folder_contents.each do |entry|
186
- relative_path = File.join(current_item[:path], entry['name'])
187
- Log.log.debug{"process_folder_tree checking #{relative_path}"}
188
- # continue only if method returns true
189
- next unless yield(entry, relative_path, state)
190
- # entry type is file, folder or link
191
- case entry['type']
192
- when 'folder'
193
- folders_to_explore.push({id: entry['id'], path: relative_path})
194
- when 'link'
195
- node_id_to_node(entry['target_node_id'])&.process_folder_tree(
196
- state: state,
197
- top_file_id: entry['target_id'],
198
- top_file_path: relative_path,
199
- &block)
200
- end
201
- end
202
- end
203
- end # process_folder_tree
204
-
205
- # Navigate the path from given file id
206
- # @param top_file_id [String] id initial file id
207
- # @param path [String] file path
208
- # @return [Hash] {.api,.file_id}
209
- def resolve_api_fid(top_file_id, path)
210
- raise 'file id shall be String' unless top_file_id.is_a?(String)
211
- process_last_link = path.end_with?(PATH_SEPARATOR)
212
- path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
213
- return {api: self, file_id: top_file_id} if path_elements.empty?
214
- resolve_state = {path: path_elements, result: nil}
215
- process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
216
- # this block is called recursively for each entry in folder
217
- # stop digging here if not in right path
218
- next false unless entry['name'].eql?(state[:path].first)
219
- # ok it matches, so we remove the match
220
- state[:path].shift
221
- case entry['type']
222
- when 'file'
223
- # file must be terminal
224
- raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
225
- # it's terminal, we found it
226
- state[:result] = {api: self, file_id: entry['id']}
227
- next false
228
- when 'folder'
229
- if state[:path].empty?
230
- # we found it
231
- state[:result] = {api: self, file_id: entry['id']}
232
- next false
233
- end
234
- when 'link'
235
- if state[:path].empty?
236
- if process_last_link
237
- # we found it
238
- other_node = node_id_to_node(entry['target_node_id'])
239
- raise 'cannot resolve link' if other_node.nil?
240
- state[:result] = {api: other_node, file_id: entry['target_id']}
241
- else
242
- # we found it but we do not process the link
243
- state[:result] = {api: self, file_id: entry['id']}
244
- end
245
- next false
246
- end
247
- else
248
- Log.log.warn{"Unknown element type: #{entry['type']}"}
249
- end
250
- # continue to dig folder
251
- next true
252
- end
253
- raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
254
- return resolve_state[:result]
255
- end
256
-
257
- def find_files(top_file_id, test_block)
258
- Log.log.debug{"find_files: file id=#{top_file_id}"}
259
- find_state = {found: [], test_block: test_block}
260
- process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
261
- state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
262
- # test all files deeply
263
- true
264
- end
265
- return find_state[:found]
266
- end
267
-
268
- def refreshed_transfer_token
269
- return oauth_token(force_refresh: true)
270
- end
271
-
272
- # Create transfer spec for gen4
273
- def transfer_spec_gen4(file_id, direction, ts_merge=nil)
274
- ak_name = nil
275
- ak_token = nil
276
- case params[:auth][:type]
277
- when :basic
278
- ak_name = params[:auth][:username]
279
- raise 'ERROR: no secret in node object' unless params[:auth][:password]
280
- ak_token = Rest.basic_token(params[:auth][:username], params[:auth][:password])
281
- when :oauth2
282
- ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
283
- # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
284
- # get bearer token, possibly use cache
285
- ak_token = oauth_token(force_refresh: false)
286
- else raise "Unsupported auth method for node gen4: #{params[:auth][:type]}"
287
- end
288
- transfer_spec = {
289
- 'direction' => direction,
290
- 'token' => ak_token,
291
- 'tags' => {
292
- Fasp::TransferSpec::TAG_RESERVED => {
293
- 'node' => {
294
- 'access_key' => ak_name,
295
- 'file_id' => file_id
296
- } # node
297
- } # aspera
298
- } # tags
299
- }
300
- # add specials tags (cos)
301
- add_tspec_info(transfer_spec)
302
- transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
303
- # add application specific tags (AoC)
304
- app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
305
- # add remote host info
306
- if self.class.use_standard_ports
307
- # get default TCP/UDP ports and transfer user
308
- transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
309
- # by default: same address as node API
310
- transfer_spec['remote_host'] = URI.parse(params[:base_url]).host
311
- # AoC allows specification of other url
312
- if !@app_info.nil? && !@app_info[:node_info]['transfer_url'].nil? && !@app_info[:node_info]['transfer_url'].empty?
313
- transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
314
- end
315
- info = read('info')[:data]
316
- # get the transfer user from info on access key
317
- transfer_spec['remote_user'] = info['transfer_user'] if info['transfer_user']
318
- # get settings from name.value array to hash key.value
319
- settings = info['settings']&.each_with_object({}){|i, h|h[i['name']] = i['value']}
320
- # check WSS ports
321
- %w[wss_enabled wss_port].each do |i|
322
- transfer_spec[i] = settings[i] if settings.key?(i)
323
- end if settings.is_a?(Hash)
324
- else
325
- # retrieve values from API (and keep a copy/cache)
326
- @std_t_spec_cache ||= create(
327
- 'files/download_setup',
328
- {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
329
- )[:data]['transfer_specs'].first['transfer_spec']
330
- # copy some parts
331
- TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
332
- end
333
- Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
334
- unless transfer_spec['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
335
- return transfer_spec
336
- end
337
- end
338
- end
data/lib/aspera/sync.rb DELETED
@@ -1,219 +0,0 @@
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 'json'
9
- require 'base64'
10
- require 'open3'
11
- require 'English'
12
-
13
- module Aspera
14
- # builds command line arg for async
15
- module Sync
16
- # sync direction, default is push
17
- DIRECTIONS = %i[push pull bidi].freeze
18
- PARAMS_VX_INSTANCE =
19
- {
20
- 'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
21
- 'watchd' => { cli: { type: :opt_with_arg}, accepted_types: :string},
22
- 'apply_local_docroot' => { cli: { type: :opt_without_arg}},
23
- 'quiet' => { cli: { type: :opt_without_arg}},
24
- 'ws_connect' => { cli: { type: :opt_without_arg}}
25
- }.freeze
26
-
27
- # map sync session parameters to transfer spec: sync -> ts, true if same
28
- PARAMS_VX_SESSION =
29
- {
30
- 'name' => { cli: { type: :opt_with_arg}, accepted_types: :string},
31
- 'local_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
32
- 'remote_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
33
- 'local_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
34
- 'remote_db_dir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
35
- 'host' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_host},
36
- 'user' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: :remote_user},
37
- 'private_key_paths' => { cli: { type: :opt_with_arg, switch: '--private-key-path'}, accepted_types: :array},
38
- 'direction' => { cli: { type: :opt_with_arg}, accepted_types: :string},
39
- 'checksum' => { cli: { type: :opt_with_arg}, accepted_types: :string},
40
- 'tags' => { cli: { type: :opt_with_arg, switch: '--tags64', convert: 'Aspera::Fasp::Parameters.convert_json64'},
41
- accepted_types: :hash, ts: true},
42
- 'tcp_port' => { cli: { type: :opt_with_arg}, accepted_types: :int, ts: :ssh_port},
43
- 'rate_policy' => { cli: { type: :opt_with_arg}, accepted_types: :string},
44
- 'target_rate' => { cli: { type: :opt_with_arg}, accepted_types: :string},
45
- 'cooloff' => { cli: { type: :opt_with_arg}, accepted_types: :int},
46
- 'pending_max' => { cli: { type: :opt_with_arg}, accepted_types: :int},
47
- 'scan_intensity' => { cli: { type: :opt_with_arg}, accepted_types: :string},
48
- 'cipher' => { cli: { type: :opt_with_arg, convert: 'Aspera::Fasp::Parameters.convert_remove_hyphen'}, accepted_types: :string, ts: true},
49
- 'transfer_threads' => { cli: { type: :opt_with_arg}, accepted_types: :int},
50
- 'preserve_time' => { cli: { type: :opt_without_arg}, ts: :preserve_times},
51
- 'preserve_access_time' => { cli: { type: :opt_without_arg}, ts: nil},
52
- 'preserve_modification_time' => { cli: { type: :opt_without_arg}, ts: nil},
53
- 'preserve_uid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_uid},
54
- 'preserve_gid' => { cli: { type: :opt_without_arg}, ts: :preserve_file_owner_gid},
55
- 'create_dir' => { cli: { type: :opt_without_arg}, ts: true},
56
- 'reset' => { cli: { type: :opt_without_arg}},
57
- # NOTE: only one env var, but multiple sessions... could be a problem
58
- 'remote_password' => { cli: { type: :envvar, variable: 'ASPERA_SCP_PASS'}, ts: true},
59
- 'cookie' => { cli: { type: :envvar, variable: 'ASPERA_SCP_COOKIE'}, ts: true},
60
- 'token' => { cli: { type: :envvar, variable: 'ASPERA_SCP_TOKEN'}, ts: true},
61
- 'license' => { cli: { type: :envvar, variable: 'ASPERA_SCP_LICENSE'}}
62
- }.freeze
63
-
64
- Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_INSTANCE)
65
- Aspera::CommandLineBuilder.normalize_description(PARAMS_VX_SESSION)
66
-
67
- PARAMS_VX_KEYS = %w[instance sessions].freeze
68
-
69
- # new API
70
- TS_TO_PARAMS_V2 = {
71
- 'remote_host' => 'remote.host',
72
- 'remote_user' => 'remote.user',
73
- 'remote_password' => 'remote.pass',
74
- 'sshfp' => 'remote.fingerprint',
75
- 'ssh_port' => 'remote.port',
76
- 'wss_port' => 'remote.ws_port',
77
- 'proxy' => 'remote.proxy',
78
- 'token' => 'remote.token',
79
- 'tags' => 'tags'
80
- }.freeze
81
-
82
- ASYNC_EXECUTABLE = 'async'
83
- ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
84
-
85
- private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :TS_TO_PARAMS_V2, :ASYNC_EXECUTABLE, :ASYNC_ADMIN_EXECUTABLE
86
-
87
- class << self
88
- # Set remote_dir in sync parameters based on transfer spec
89
- # @param params [Hash] sync parameters, old or new format
90
- # @param remote_dir_key [String] key to update in above hash
91
- # @param transfer_spec [Hash] transfer spec
92
- def update_remote_dir(sync_params, remote_dir_key, transfer_spec)
93
- if transfer_spec.dig(*%w[tags aspera node file_id])
94
- # in AoC, use gen4
95
- sync_params[remote_dir_key] = '/'
96
- elsif transfer_spec['cookie']&.start_with?('aspera.shares2')
97
- # TODO : something more generic, independent of Shares
98
- # in Shares, the actual folder on remote end is not always the same as the name of the share
99
- actual_remote = transfer_spec['paths']&.first&.[]('source')
100
- sync_params[remote_dir_key] = actual_remote if actual_remote
101
- end
102
- nil
103
- end
104
-
105
- # @param sync_params [Hash] sync parameters, old or new format
106
- # @param block [nil, Proc] block to generate transfer spec, takes: direction (one of DIRECTIONS), local_dir, remote_dir
107
- def start(sync_params, &block)
108
- raise 'Internal Error: sync_params parameter must be Hash' unless sync_params.is_a?(Hash)
109
- env_args = {
110
- args: [],
111
- env: {}
112
- }
113
- if sync_params.key?('local')
114
- # async native JSON format (v2)
115
- raise StandardError, 'remote must be Hash' unless sync_params['remote'].is_a?(Hash)
116
- if block
117
- transfer_spec = yield((sync_params['direction'] || 'push').to_sym, sync_params['local']['path'], sync_params['remote']['path'])
118
- # async native JSON format
119
- raise StandardError, 'sync parameter "local" must be Hash' unless sync_params['local'].is_a?(Hash)
120
- TS_TO_PARAMS_V2.each do |ts_param, sy_path|
121
- next unless transfer_spec.key?(ts_param)
122
- sy_dig = sy_path.split('.')
123
- param = sy_dig.pop
124
- hash = sy_dig.empty? ? sync_params : sync_params[sy_dig.first]
125
- hash = sync_params[sy_dig.first] = {} if hash.nil?
126
- hash[param] = transfer_spec[ts_param]
127
- end
128
- sync_params['remote']['connect_mode'] ||= sync_params['remote'].key?('ws_port') ? 'ws' : 'ssh'
129
- sync_params['remote']['private_key_paths'] ||= Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
130
- update_remote_dir(sync_params['remote'], 'path', transfer_spec)
131
- end
132
- env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
133
- elsif sync_params.key?('sessions')
134
- # ascli JSON format (v1)
135
- if block
136
- sync_params['sessions'].each do |session|
137
- transfer_spec = yield((session['direction'] || 'push').to_sym, session['local_dir'], session['remote_dir'])
138
- PARAMS_VX_SESSION.each do |async_param, behavior|
139
- if behavior.key?(:ts)
140
- tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
141
- session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
142
- end
143
- end
144
- session['private_key_paths'] = Fasp::Installation.instance.aspera_token_ssh_key_paths if transfer_spec.key?('token')
145
- update_remote_dir(session, 'remote_dir', transfer_spec)
146
- end
147
- end
148
- raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
149
- sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
150
- raise StandardError, 'sessions key must be Array' unless sync_params['sessions'].is_a?(Array)
151
- raise StandardError, 'sessions key requires at least one Hash' unless sync_params['sessions'].first.is_a?(Hash)
152
-
153
- if sync_params.key?('instance')
154
- raise StandardError, 'instance key must be Hash' unless sync_params['instance'].is_a?(Hash)
155
- instance_builder = Aspera::CommandLineBuilder.new(sync_params['instance'], PARAMS_VX_INSTANCE)
156
- instance_builder.process_params
157
- instance_builder.add_env_args(env_args)
158
- end
159
-
160
- sync_params['sessions'].each do |session_params|
161
- raise StandardError, 'sessions must contain hashes' unless session_params.is_a?(Hash)
162
- raise StandardError, 'session must contain at least name' unless session_params.key?('name')
163
- session_builder = Aspera::CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
164
- session_builder.process_params
165
- session_builder.add_env_args(env_args)
166
- end
167
- else
168
- raise 'At least one of `local` or `sessions` must be present in async parameters'
169
- end
170
- Log.log.debug{Log.dump(:sync_params, sync_params)}
171
-
172
- Log.log.debug{"execute: #{env_args[:env].map{|k, v| "#{k}=\"#{v}\""}.join(' ')} \"#{ASYNC_EXECUTABLE}\" \"#{env_args[:args].join('" "')}\""}
173
- res = system(env_args[:env], [ASYNC_EXECUTABLE, ASYNC_EXECUTABLE], *env_args[:args])
174
- Log.log.debug{"result=#{res}"}
175
- case res
176
- when true then return nil
177
- when false then raise "failed: #{$CHILD_STATUS}"
178
- when nil then raise "not started: #{$CHILD_STATUS}"
179
- else raise 'internal error: unspecified case'
180
- end
181
- end
182
-
183
- def admin_status(sync_params, session_name)
184
- command_line = [ASYNC_ADMIN_EXECUTABLE, '--quiet']
185
- if sync_params.key?('local')
186
- raise 'Missing session name' if sync_params['name'].nil?
187
- raise 'Session not found' unless session_name.nil? || session_name.eql?(sync_params['name'])
188
- command_line.push("--name=#{sync_params['name']}")
189
- if sync_params.key?('local_db_dir')
190
- command_line.push("--local-db-dir=#{sync_params['local_db_dir']}")
191
- elsif sync_params.dig('local', 'path')
192
- command_line.push("--local-dir=#{sync_params.dig('local', 'path')}")
193
- else
194
- raise 'Missing either local_db_dir or local.path'
195
- end
196
- elsif sync_params.key?('sessions')
197
- session = session_name.nil? ? sync_params['sessions'].first : sync_params['sessions'].find{|s|s['name'].eql?(session_name)}
198
- raise "Session #{session_name} not found in #{sync_params['sessions'].map{|s|s['name']}.join(',')}" if session.nil?
199
- raise 'Missing session name' if session['name'].nil?
200
- command_line.push("--name=#{session['name']}")
201
- if session.key?('local_db_dir')
202
- command_line.push("--local-db-dir=#{session['local_db_dir']}")
203
- elsif session.key?('local_dir')
204
- command_line.push("--local-dir=#{session['local_dir']}")
205
- else
206
- raise 'Missing either local_db_dir or local_dir'
207
- end
208
- else
209
- raise 'At least one of `local` or `sessions` must be present in async parameters'
210
- end
211
- Log.log.debug{"execute: #{command_line.join(' ')}"}
212
- stdout, stderr, status = Open3.capture3(*command_line)
213
- Log.log.debug{"status=#{status}, stderr=#{stderr}"}
214
- raise "Sync failed: #{status.exitstatus} : #{stderr}" unless status.success?
215
- return stdout.split("\n").each_with_object({}){|l, m|i = l.split(':', 2); m[i.first.lstrip] = i.last.lstrip} # rubocop:disable Style/Semicolon
216
- end
217
- end
218
- end # end Sync
219
- end # end Aspera