aspera-cli 4.19.0 → 4.21.1

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 (91) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -0
  4. data/CONTRIBUTING.md +18 -4
  5. data/README.md +886 -510
  6. data/bin/asession +27 -20
  7. data/examples/build_exec +65 -76
  8. data/examples/build_exec_rubyc +40 -0
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +18 -24
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +34 -15
  13. data/lib/aspera/agent/direct.rb +44 -54
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +11 -21
  16. data/lib/aspera/agent/{trsdk.rb → transferd.rb} +27 -51
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +139 -105
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +15 -10
  22. data/lib/aspera/api/node.rb +70 -32
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +166 -70
  25. data/lib/aspera/ascp/management.rb +30 -8
  26. data/lib/aspera/assert.rb +10 -5
  27. data/lib/aspera/cli/formatter.rb +166 -162
  28. data/lib/aspera/cli/hints.rb +2 -1
  29. data/lib/aspera/cli/info.rb +12 -10
  30. data/lib/aspera/cli/main.rb +28 -13
  31. data/lib/aspera/cli/manager.rb +7 -2
  32. data/lib/aspera/cli/plugin.rb +17 -31
  33. data/lib/aspera/cli/plugins/alee.rb +3 -3
  34. data/lib/aspera/cli/plugins/aoc.rb +246 -208
  35. data/lib/aspera/cli/plugins/ats.rb +16 -14
  36. data/lib/aspera/cli/plugins/config.rb +154 -94
  37. data/lib/aspera/cli/plugins/console.rb +3 -3
  38. data/lib/aspera/cli/plugins/cos.rb +1 -0
  39. data/lib/aspera/cli/plugins/faspex.rb +15 -23
  40. data/lib/aspera/cli/plugins/faspex5.rb +64 -50
  41. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  42. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  43. data/lib/aspera/cli/plugins/node.rb +174 -109
  44. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  45. data/lib/aspera/cli/plugins/preview.rb +8 -9
  46. data/lib/aspera/cli/plugins/server.rb +5 -9
  47. data/lib/aspera/cli/plugins/shares.rb +2 -2
  48. data/lib/aspera/cli/sync_actions.rb +2 -2
  49. data/lib/aspera/cli/transfer_agent.rb +12 -14
  50. data/lib/aspera/cli/transfer_progress.rb +37 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +4 -5
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +75 -25
  55. data/lib/aspera/faspex_gw.rb +2 -2
  56. data/lib/aspera/json_rpc.rb +1 -1
  57. data/lib/aspera/keychain/macos_security.rb +7 -12
  58. data/lib/aspera/log.rb +3 -4
  59. data/lib/aspera/node_simulator.rb +230 -112
  60. data/lib/aspera/oauth/base.rb +64 -83
  61. data/lib/aspera/oauth/factory.rb +52 -6
  62. data/lib/aspera/oauth/generic.rb +4 -8
  63. data/lib/aspera/oauth/jwt.rb +6 -3
  64. data/lib/aspera/oauth/url_json.rb +1 -2
  65. data/lib/aspera/oauth/web.rb +5 -2
  66. data/lib/aspera/persistency_action_once.rb +16 -8
  67. data/lib/aspera/persistency_folder.rb +20 -2
  68. data/lib/aspera/preview/generator.rb +1 -1
  69. data/lib/aspera/preview/utils.rb +11 -17
  70. data/lib/aspera/products/alpha.rb +30 -0
  71. data/lib/aspera/products/connect.rb +48 -0
  72. data/lib/aspera/products/other.rb +82 -0
  73. data/lib/aspera/products/transferd.rb +54 -0
  74. data/lib/aspera/rest.rb +116 -87
  75. data/lib/aspera/secret_hider.rb +2 -2
  76. data/lib/aspera/ssh.rb +31 -24
  77. data/lib/aspera/transfer/faux_file.rb +4 -4
  78. data/lib/aspera/transfer/parameters.rb +16 -17
  79. data/lib/aspera/transfer/spec.rb +12 -12
  80. data/lib/aspera/transfer/spec.yaml +22 -20
  81. data/lib/aspera/transfer/sync.rb +2 -10
  82. data/lib/aspera/transfer/uri.rb +3 -3
  83. data/lib/aspera/uri_reader.rb +1 -1
  84. data/lib/aspera/web_auth.rb +166 -17
  85. data/lib/aspera/web_server_simple.rb +4 -3
  86. data/lib/transferd_pb.rb +86 -0
  87. data/lib/transferd_services_pb.rb +84 -0
  88. data.tar.gz.sig +0 -0
  89. metadata +58 -22
  90. metadata.gz.sig +0 -0
  91. data/lib/aspera/ascp/products.rb +0 -156
@@ -4,7 +4,7 @@
4
4
  require 'aspera/cli/info'
5
5
  require 'aspera/log'
6
6
  require 'aspera/assert'
7
- require 'open3'
7
+ require 'aspera/environment'
8
8
 
9
9
  # enhance the gem to support other key chains
10
10
  module Aspera
@@ -48,20 +48,15 @@ module Aspera
48
48
  options[:path] = uri.path unless ['', '/'].include?(uri.path)
49
49
  options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
50
50
  end
51
- command_line = [SECURITY_UTILITY, command]
51
+ command_args = [command]
52
52
  options&.each do |k, v|
53
53
  Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
54
54
  next if v.nil?
55
- command_line.push("-#{supported[k]}")
56
- command_line.push(v.shellescape) unless v.empty?
55
+ command_args.push("-#{supported[k]}")
56
+ command_args.push(v.shellescape) unless v.empty?
57
57
  end
58
- command_line.push(last_opt) unless last_opt.nil?
59
- Log.log.debug{"executing>>#{command_line.join(' ')}"}
60
- stdout, stderr, status = Open3.capture3(*command_line)
61
- Log.log.debug{"status=#{status}, stderr=#{stderr}"}
62
- Log.log.trace1{"stdout=#{stdout}"}
63
- raise "#{SECURITY_UTILITY} failed: #{status.exitstatus} : #{stderr}" unless status.success?
64
- return stdout
58
+ command_args.push(last_opt) unless last_opt.nil?
59
+ return Environment.secure_capture(exec: SECURITY_UTILITY, args: command_args)
65
60
  end
66
61
 
67
62
  def key_chains(output)
@@ -78,7 +73,7 @@ module Aspera
78
73
 
79
74
  def list(options={})
80
75
  Aspera.assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
81
- key_chains(execute('list-key_chains', options, LIST_OPTIONS))
76
+ key_chains(execute('list-keychains', options, LIST_OPTIONS))
82
77
  end
83
78
 
84
79
  def by_name(name)
data/lib/aspera/log.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/assert'
3
4
  require 'aspera/colors'
4
5
  require 'aspera/secret_hider'
5
6
  require 'logger'
@@ -73,8 +74,7 @@ module Aspera
73
74
  JSON.pretty_generate(object) rescue PP.pp(object, +'')
74
75
  when :ruby
75
76
  PP.pp(object, +'')
76
- else
77
- raise 'wrong parameter, expect ruby or json'
77
+ else error_unexpected_value(@@format){'dump format'}
78
78
  end
79
79
  "#{name.to_s.green} (#{@@format})=\n#{result}"
80
80
  end
@@ -127,8 +127,7 @@ module Aspera
127
127
  Syslog::Logger.make_methods(severity.downcase)
128
128
  end
129
129
  @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
130
- else
131
- raise "unknown log type: #{new_log_type}, use one of: #{LOG_TYPES.join(', ')}"
130
+ else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
132
131
  end
133
132
  @logger.level = current_severity_integer
134
133
  @logger_type = new_log_type
@@ -7,28 +7,241 @@ require 'webrick'
7
7
  require 'json'
8
8
 
9
9
  module Aspera
10
+ class NodeSimulator
11
+ def initialize
12
+ @agent = Agent::Direct.new(management_cb: ->(event){process_event(event)})
13
+ @sessions = {}
14
+ end
15
+
16
+ def start(ts)
17
+ @agent.start_transfer(ts)
18
+ end
19
+
20
+ def all_sessions
21
+ @agent.sessions.map { |session| session[:job_id] }.uniq.each.map{|job_id|job_to_transfer(job_id)}
22
+ end
23
+
24
+ # status: ('waiting', 'partially_completed', 'unknown', 'waiting(read error)',] 'running', 'completed', 'failed'
25
+ def job_to_transfer(job_id)
26
+ jobs = @agent.sessions_by_job(job_id)
27
+ ts = nil
28
+ sessions = jobs.map do |j|
29
+ ts ||= j[:ts]
30
+ {
31
+ id: j[:id],
32
+ client_node_id: '',
33
+ server_node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
34
+ client_ip_address: '192.168.0.100',
35
+ server_ip_address: '5.10.114.4',
36
+ status: 'running',
37
+ retry_timeout: 3600,
38
+ retry_count: 0,
39
+ start_time_usec: 1701094040000000,
40
+ end_time_usec: nil,
41
+ elapsed_usec: 405312,
42
+ bytes_transferred: 26,
43
+ bytes_written: 26,
44
+ bytes_lost: 0,
45
+ files_completed: 1,
46
+ directories_completed: 0,
47
+ target_rate_kbps: 500000,
48
+ min_rate_kbps: 0,
49
+ calc_rate_kbps: 9900,
50
+ network_delay_usec: 40000,
51
+ avg_rate_kbps: 0.51,
52
+ error_code: 0,
53
+ error_desc: '',
54
+ source_statistics: {
55
+ args_scan_attempted: 1,
56
+ args_scan_completed: 1,
57
+ paths_scan_attempted: 1,
58
+ paths_scan_failed: 0,
59
+ paths_scan_skipped: 0,
60
+ paths_scan_excluded: 0,
61
+ dirs_scan_completed: 0,
62
+ files_scan_completed: 1,
63
+ dirs_xfer_attempted: 0,
64
+ dirs_xfer_fail: 0,
65
+ files_xfer_attempted: 1,
66
+ files_xfer_fail: 0,
67
+ files_xfer_noxfer: 0
68
+ },
69
+ precalc: {
70
+ enabled: true,
71
+ status: 'ready',
72
+ bytes_expected: 0,
73
+ directories_expected: 0,
74
+ files_expected: 0,
75
+ files_excluded: 0,
76
+ files_special: 0,
77
+ files_failed: 1
78
+ }
79
+ }
80
+ end
81
+ ts ||= {}
82
+ result = {
83
+ id: job_id,
84
+ status: 'running',
85
+ start_spec: ts,
86
+ sessions: sessions,
87
+ bytes_transferred: 26,
88
+ bytes_written: 26,
89
+ bytes_lost: 0,
90
+ avg_rate_kbps: 0.51,
91
+ files_completed: 1,
92
+ files_skipped: 0,
93
+ directories_completed: 0,
94
+ start_time_usec: 1701094040000000,
95
+ end_time_usec: 1701094040405312,
96
+ elapsed_usec: 405312,
97
+ error_code: 0,
98
+ error_desc: '',
99
+ precalc: {
100
+ status: 'ready',
101
+ bytes_expected: 0,
102
+ files_expected: 0,
103
+ directories_expected: 0,
104
+ files_special: 0,
105
+ files_failed: 1
106
+ },
107
+ files: [{
108
+ id: 'd1b5c112-82b75425-860745fc-93851671-64541bdd',
109
+ path: '/workspaces/45071/packages/bYA_ilq73g.asp-package/contents/data_file.bin',
110
+ start_time_usec: 1701094040000000,
111
+ elapsed_usec: 105616,
112
+ end_time_usec: 1701094040001355,
113
+ status: 'completed',
114
+ error_code: 0,
115
+ error_desc: '',
116
+ size: 26,
117
+ type: 'file',
118
+ checksum_type: 'none',
119
+ checksum: nil,
120
+ start_byte: 0,
121
+ bytes_written: 26,
122
+ session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
123
+ }
124
+ Log.log.trace2{Log.dump(:job, result)}
125
+ return result
126
+ end
127
+
128
+ # Process event from management port
129
+ def process_event(event)
130
+ case event['Type']
131
+ when 'NOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
132
+ # rubocop:disable Lint/DuplicateBranch
133
+ when 'START' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
134
+ when 'QUERY' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
135
+ when 'QUERYRSP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
136
+ when 'STATS' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
137
+ when 'STOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
138
+ when 'ERROR' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
139
+ when 'CANCEL' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
140
+ when 'DONE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
141
+ when 'RATE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
142
+ when 'FILEERROR' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
143
+ when 'SESSION' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
144
+ when 'NOTIFICATION' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
145
+ when 'INIT' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
146
+ when 'VLINK' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
147
+ when 'PUT' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
148
+ when 'WRITE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
149
+ when 'CLOSE' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
150
+ when 'SKIP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
151
+ when 'ARGSTOP' then Aspera.Log.debug{"event not managed: #{event['Type']}"}
152
+ # rubocop:enable Lint/DuplicateBranch
153
+ else Aspera.error_unreachable_line
154
+ end
155
+ end
156
+ end
157
+
10
158
  # this class answers the Faspex /send API and creates a package on Aspera on Cloud
159
+ # a new instance is created for each request
11
160
  class NodeSimulatorServlet < WEBrick::HTTPServlet::AbstractServlet
12
161
  PATH_TRANSFERS = '/ops/transfers'
13
162
  PATH_ONE_TRANSFER = %r{/ops/transfers/(.+)$}
163
+ PATH_BROWSE = '/files/browse'
14
164
  # @param app_api [Api::AoC]
15
165
  # @param app_context [String]
16
- def initialize(server, credentials, transfer)
166
+ def initialize(server, credentials, simulator)
17
167
  super(server)
18
168
  @credentials = credentials
19
- @xfer_manager = Agent::Direct.new
169
+ @simulator = simulator
170
+ end
171
+
172
+ require 'json'
173
+ require 'time'
174
+
175
+ def folder_to_structure(folder_path)
176
+ raise "Path does not exist or is not a directory: #{folder_path}" unless Dir.exist?(folder_path)
177
+
178
+ # Build self structure
179
+ folder_stat = File.stat(folder_path)
180
+ structure = {
181
+ 'self' => {
182
+ 'path' => folder_path,
183
+ 'basename' => File.basename(folder_path),
184
+ 'type' => 'directory',
185
+ 'size' => folder_stat.size,
186
+ 'mtime' => folder_stat.mtime.utc.iso8601,
187
+ 'permissions' => [
188
+ { 'name' => 'view' },
189
+ { 'name' => 'edit' },
190
+ { 'name' => 'delete' }
191
+ ]
192
+ },
193
+ 'items' => []
194
+ }
195
+
196
+ # Iterate over folder contents
197
+ Dir.foreach(folder_path) do |entry|
198
+ next if entry == '.' || entry == '..' # Skip current and parent directory
199
+
200
+ item_path = File.join(folder_path, entry)
201
+ item_type = File.ftype(item_path) rescue 'unknown' # Get the type of file
202
+ item_stat = File.lstat(item_path) # Use lstat to handle symbolic links correctly
203
+
204
+ item = {
205
+ 'path' => item_path,
206
+ 'basename' => entry,
207
+ 'type' => item_type,
208
+ 'size' => item_stat.size,
209
+ 'mtime' => item_stat.mtime.utc.iso8601,
210
+ 'permissions' => [
211
+ { 'name' => 'view' },
212
+ { 'name' => 'edit' },
213
+ { 'name' => 'delete' }
214
+ ]
215
+ }
216
+
217
+ # Add additional details for specific types
218
+ case item_type
219
+ when 'file'
220
+ item['partial_file'] = false
221
+ when 'link'
222
+ item['target'] = File.readlink(item_path) rescue nil # Add the target of the symlink
223
+ when 'unknown'
224
+ item['note'] = 'File type could not be determined'
225
+ end
226
+
227
+ structure['items'] << item
228
+ end
229
+
230
+ structure
20
231
  end
21
232
 
22
233
  def do_POST(request, response)
23
234
  case request.path
24
235
  when PATH_TRANSFERS
25
- job_id = @xfer_manager.start_transfer(JSON.parse(request.body))
26
- session = @xfer_manager.sessions_by_job(job_id).first
27
- result = session[:ts].clone
28
- result['id'] = job_id
29
- set_json_response(response, result)
236
+ job_id = @simulator.start(JSON.parse(request.body))
237
+ sleep(0.5)
238
+ set_json_response(request, response, @simulator.job_to_transfer(job_id))
239
+ when PATH_BROWSE
240
+ req = JSON.parse(request.body)
241
+ # req['count']
242
+ set_json_response(request, response, folder_to_structure(req['path']))
30
243
  else
31
- set_json_response(response, [{error: 'Bad request'}], code: 400)
244
+ set_json_response(request, response, [{error: 'Bad request'}], code: 400)
32
245
  end
33
246
  end
34
247
 
@@ -36,7 +249,7 @@ module Aspera
36
249
  case request.path
37
250
  when '/info'
38
251
  info = Ascp::Installation.instance.ascp_info
39
- set_json_response(response, {
252
+ set_json_response(request, response, {
40
253
  application: 'node',
41
254
  current_time: Time.now.utc.iso8601(0),
42
255
  version: info['sdk_ascp_version'].gsub(/ .*$/, ''),
@@ -44,13 +257,13 @@ module Aspera
44
257
  license_max_rate: info['maximum_bandwidth'],
45
258
  os: %x(uname -srv).chomp,
46
259
  aej_status: 'disconnected',
47
- async_reporting: 'yes',
48
- transfer_activity_reporting: 'yes',
260
+ async_reporting: 'no',
261
+ transfer_activity_reporting: 'no',
49
262
  transfer_user: 'xfer',
50
263
  docroot: 'file:////data/aoc/eudemo-sedemo',
51
264
  node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
52
265
  cluster_id: '6dae2844-d1a9-47a5-916d-9b3eac3ea466',
53
- acls: [],
266
+ acls: ['impersonation'],
54
267
  access_key_configuration_capabilities: {
55
268
  transfer: %w[
56
269
  cipher
@@ -99,115 +312,20 @@ module Aspera
99
312
  {name: 'wss_port', value: 443}
100
313
  ]})
101
314
  when PATH_TRANSFERS
102
- result = @xfer_manager.sessions.map { |session| job_to_transfer(session) }
103
- set_json_response(response, result)
315
+ set_json_response(request, response, @simulator.all_sessions)
104
316
  when PATH_ONE_TRANSFER
105
317
  job_id = request.path.match(PATH_ONE_TRANSFER)[1]
106
- set_json_response(response, job_to_transfer(@xfer_manager.sessions_by_job(job_id).first))
318
+ set_json_response(request, response, @simulator.job_to_transfer(job_id))
107
319
  else
108
- set_json_response(response, [{error: 'Unknown request'}], code: 400)
320
+ set_json_response(request, response, [{error: 'Unknown request'}], code: 400)
109
321
  end
110
322
  end
111
323
 
112
- def set_json_response(response, json, code: 200)
324
+ def set_json_response(request, response, json, code: 200)
113
325
  response.status = code
114
326
  response['Content-Type'] = 'application/json'
115
327
  response.body = json.to_json
116
- Log.log.trace1{Log.dump('response', json)}
117
- end
118
-
119
- def job_to_transfer(job)
120
- session = {
121
- id: 'bafc72b8-366c-4501-8095-47208183d6b8',
122
- client_node_id: '',
123
- server_node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
124
- client_ip_address: '192.168.0.100',
125
- server_ip_address: '5.10.114.4',
126
- status: 'running',
127
- retry_timeout: 3600,
128
- retry_count: 0,
129
- start_time_usec: 1701094040000000,
130
- end_time_usec: nil,
131
- elapsed_usec: 405312,
132
- bytes_transferred: 26,
133
- bytes_written: 26,
134
- bytes_lost: 0,
135
- files_completed: 1,
136
- directories_completed: 0,
137
- target_rate_kbps: 500000,
138
- min_rate_kbps: 0,
139
- calc_rate_kbps: 9900,
140
- network_delay_usec: 40000,
141
- avg_rate_kbps: 0.51,
142
- error_code: 0,
143
- error_desc: '',
144
- source_statistics: {
145
- args_scan_attempted: 1,
146
- args_scan_completed: 1,
147
- paths_scan_attempted: 1,
148
- paths_scan_failed: 0,
149
- paths_scan_skipped: 0,
150
- paths_scan_excluded: 0,
151
- dirs_scan_completed: 0,
152
- files_scan_completed: 1,
153
- dirs_xfer_attempted: 0,
154
- dirs_xfer_fail: 0,
155
- files_xfer_attempted: 1,
156
- files_xfer_fail: 0,
157
- files_xfer_noxfer: 0
158
- },
159
- precalc: {
160
- enabled: true,
161
- status: 'ready',
162
- bytes_expected: 0,
163
- directories_expected: 0,
164
- files_expected: 0,
165
- files_excluded: 0,
166
- files_special: 0,
167
- files_failed: 1
168
- }}
169
- return {
170
- id: '609a667d-642e-4290-9312-b4d20d3c0159',
171
- status: 'running',
172
- start_spec: job[:ts],
173
- sessions: [session],
174
- bytes_transferred: 26,
175
- bytes_written: 26,
176
- bytes_lost: 0,
177
- avg_rate_kbps: 0.51,
178
- files_completed: 1,
179
- files_skipped: 0,
180
- directories_completed: 0,
181
- start_time_usec: 1701094040000000,
182
- end_time_usec: 1701094040405312,
183
- elapsed_usec: 405312,
184
- error_code: 0,
185
- error_desc: '',
186
- precalc: {
187
- status: 'ready',
188
- bytes_expected: 0,
189
- files_expected: 0,
190
- directories_expected: 0,
191
- files_special: 0,
192
- files_failed: 1
193
- },
194
- files: [{
195
- id: 'd1b5c112-82b75425-860745fc-93851671-64541bdd',
196
- path: '/workspaces/45071/packages/bYA_ilq73g.asp-package/contents/data_file.bin',
197
- start_time_usec: 1701094040000000,
198
- elapsed_usec: 105616,
199
- end_time_usec: 1701094040001355,
200
- status: 'completed',
201
- error_code: 0,
202
- error_desc: '',
203
- size: 26,
204
- type: 'file',
205
- checksum_type: 'none',
206
- checksum: nil,
207
- start_byte: 0,
208
- bytes_written: 26,
209
- session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
210
- }
328
+ Log.log.trace1{Log.dump("response for #{request.request_method} #{request.path}", json)}
211
329
  end
212
330
  end
213
331
  end
@@ -3,64 +3,64 @@
3
3
  require 'aspera/oauth/factory'
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
- require 'aspera/id_generator'
7
6
  require 'date'
8
7
 
9
8
  module Aspera
10
9
  module OAuth
11
- # Implement OAuth 2 for the REST client and generate a bearer token
12
- # bearer tokens are cached in memory and in a file cache for later re-use
10
+ # OAuth 2 client for the REST client
11
+ # Generate bearer token
12
+ # Bearer tokens are cached in memory and in a file cache for later re-use
13
13
  # https://tools.ietf.org/html/rfc6749
14
14
  class Base
15
- # scope can be modified after creation
16
- attr_writer :scope
17
-
18
- # [M]=mandatory [D]=has default value [O]=Optional/nil
19
- # @param base_url [M] URL of authentication API
20
- # @param auth [O] basic auth parameters
21
- # @param client_id [O]
22
- # @param client_secret [O]
23
- # @param scope [O]
24
- # @param path_token [D] API end point to create a token
25
- # @param token_field [D] field in result that contains the token
15
+ # @param ** Parameters for REST
16
+ # @param client_id [String, nil]
17
+ # @param client_secret [String, nil]
18
+ # @param scope [String, nil]
19
+ # @param use_query [bool] Provide parameters in query instead of body
20
+ # @param path_token [String] API end point to create a token
21
+ # @param token_field [String] Field in result that contains the token
22
+ # @param cache_ids [Array, nil] List of unique identifiers for cache id generation
26
23
  def initialize(
27
- base_url:,
28
- auth: nil,
29
24
  client_id: nil,
30
25
  client_secret: nil,
31
26
  scope: nil,
32
27
  use_query: false,
33
- path_token: 'token', # default endpoint for /token to generate token
34
- token_field: 'access_token' # field with token in result of call to path_token
28
+ path_token: 'token',
29
+ token_field: Factory::TOKEN_FIELD,
30
+ cache_ids: nil,
31
+ **rest_params
35
32
  )
36
- Aspera.assert_type(base_url, String)
37
33
  Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', exception_class: InternalError)
38
- @base_url = base_url
34
+ # this is the OAuth API
35
+ @api = Rest.new(**rest_params)
39
36
  @path_token = path_token
40
37
  @token_field = token_field
41
38
  @client_id = client_id
42
39
  @client_secret = client_secret
43
- @scope = scope
44
40
  @use_query = use_query
45
- @identifiers = []
46
- @identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
47
- # this is the OAuth API
48
- @api = Rest.new(
49
- base_url: @base_url,
50
- redirect_max: 2,
51
- auth: auth)
41
+ @base_cache_ids = cache_ids.nil? ? [] : cache_ids.clone
42
+ Aspera.assert_type(@base_cache_ids, Array)
43
+ @base_cache_ids.push(@api.auth_params[:username]) if @api.auth_params.key?(:username)
44
+ @base_cache_ids.compact!
45
+ @base_cache_ids.freeze
46
+ self.scope = scope
47
+ end
48
+
49
+ # Scope can be modified after creation, then update identifier for cache
50
+ def scope=(scope)
51
+ @scope = scope
52
+ # generate token unique identifier for persistency (memory/disk cache)
53
+ @token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
52
54
  end
53
55
 
54
56
  # helper method to create token as per RFC
55
57
  def create_token_call(creation_params)
56
58
  Log.log.debug{'Generating a new token'.bg_green}
57
- payload = {
58
- body: creation_params,
59
- body_type: :www
60
- }
59
+ payload = { body_type: :www }
61
60
  if @use_query
62
61
  payload[:query] = creation_params
63
- payload[:body] = {}
62
+ else
63
+ payload[:body] = creation_params
64
64
  end
65
65
  return @api.call(
66
66
  operation: 'POST',
@@ -85,56 +85,37 @@ module Aspera
85
85
  # @param cache set to false to disable cache
86
86
  # @param refresh set to true to force refresh or re-generation (if previous failed)
87
87
  def token(cache: true, refresh: false)
88
- # generate token unique identifier for persistency (memory/disk cache)
89
- token_id = IdGenerator.from_list(Factory.id(
90
- @base_url,
91
- Factory.class_to_id(self.class),
92
- @identifiers,
93
- @scope
94
- ))
95
-
96
- # get token_data from cache (or nil), token_data is what is returned by /token
97
- token_data = Factory.instance.persist_mgr.get(token_id) if cache
98
- token_data = JSON.parse(token_data) unless token_data.nil?
99
- # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
100
- # might help in case the transfer agent cannot refresh himself
101
- # `direct` agent is equipped with refresh code
102
- if !refresh && !token_data.nil?
103
- decoded_token = OAuth::Factory.instance.decode_token(token_data[@token_field])
104
- Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
105
- if decoded_token.is_a?(Hash)
106
- expires_at_sec =
107
- if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
108
- elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
88
+ # get token info from cache (or nil), decoded with date and expiration status
89
+ token_info = Factory.instance.get_token_info(@token_cache_id) if cache
90
+ token_data = nil
91
+ unless token_info.nil?
92
+ token_data = token_info[:data]
93
+ # Optional optimization:
94
+ # check if node token is expired based on decoded content then force refresh if close enough
95
+ # might help in case the transfer agent cannot refresh himself
96
+ # `direct` agent is equipped with refresh code
97
+ # an API was already called, but failed, we need to regenerate or refresh
98
+ if refresh || token_info[:expired]
99
+ if token_data.key?('refresh_token') && token_data['refresh_token'].eql?('not_supported')
100
+ # save possible refresh token, before deleting the cache
101
+ refresh_token = token_data['refresh_token']
102
+ end
103
+ # delete cache
104
+ Factory.instance.persist_mgr.delete(@token_cache_id)
105
+ token_data = nil
106
+ # lets try the existing refresh token
107
+ if !refresh_token.nil?
108
+ Log.log.info{"refresh=[#{refresh_token}]".bg_green}
109
+ # NOTE: AoC admin token has no refresh, and lives by default 1800secs
110
+ resp = create_token_call(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
111
+ if resp[:http].code.start_with?('2')
112
+ # save only if success
113
+ json_data = resp[:http].body
114
+ token_data = JSON.parse(json_data)
115
+ Factory.instance.persist_mgr.put(@token_cache_id, json_data)
116
+ else
117
+ Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
109
118
  end
110
- # force refresh if we see a token too close from expiration
111
- refresh = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.parameters[:token_expiration_guard_sec]
112
- Log.log.debug{"Expiration: #{expires_at_sec} / #{refresh}"}
113
- end
114
- end
115
-
116
- # an API was already called, but failed, we need to regenerate or refresh
117
- if refresh
118
- if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
119
- # save possible refresh token, before deleting the cache
120
- refresh_token = token_data['refresh_token']
121
- end
122
- # delete cache
123
- Factory.instance.persist_mgr.delete(token_id)
124
- token_data = nil
125
- # lets try the existing refresh token
126
- if !refresh_token.nil?
127
- Log.log.info{"refresh=[#{refresh_token}]".bg_green}
128
- # try to refresh
129
- # note: AoC admin token has no refresh, and lives by default 1800secs
130
- resp = create_token_call(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
131
- if resp[:http].code.start_with?('2')
132
- # save only if success
133
- json_data = resp[:http].body
134
- token_data = JSON.parse(json_data)
135
- Factory.instance.persist_mgr.put(token_id, json_data)
136
- else
137
- Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
138
119
  end
139
120
  end
140
121
  end
@@ -144,7 +125,7 @@ module Aspera
144
125
  resp = create_token
145
126
  json_data = resp[:http].body
146
127
  token_data = JSON.parse(json_data)
147
- Factory.instance.persist_mgr.put(token_id, json_data)
128
+ Factory.instance.persist_mgr.put(@token_cache_id, json_data)
148
129
  end
149
130
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
150
131
  # ok we shall have a token here