aspera-cli 4.27.2 → 4.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +50 -0
- data/bin/ascli +2 -1
- data/docs/README.md +804 -746
- data/lib/aspera/agent/connect.rb +6 -4
- data/lib/aspera/agent/desktop.rb +2 -2
- data/lib/aspera/agent/direct.rb +3 -1
- data/lib/aspera/agent/node.rb +3 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +9 -7
- data/lib/aspera/api/httpgw.rb +37 -33
- data/lib/aspera/api/node.rb +38 -33
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +62 -27
- data/lib/aspera/ascp/management.rb +1 -0
- data/lib/aspera/assert.rb +4 -0
- data/lib/aspera/cli/ascp_actions.rb +20 -41
- data/lib/aspera/cli/async_transfer_store.rb +2 -2
- data/lib/aspera/cli/bootstrapper.rb +11 -15
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +149 -33
- data/lib/aspera/cli/command_spec.rb +103 -14
- data/lib/aspera/cli/completion/ascli.bash +12 -0
- data/lib/aspera/cli/completion/ascli.fish +16 -0
- data/lib/aspera/cli/completion/ascli.zsh +19 -0
- data/lib/aspera/cli/context.rb +3 -0
- data/lib/aspera/cli/deprecation.rb +37 -0
- data/lib/aspera/cli/extended_value.rb +2 -0
- data/lib/aspera/cli/formatter.rb +87 -75
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +21 -21
- data/lib/aspera/cli/info.rb +3 -0
- data/lib/aspera/cli/mcp_tool.rb +47 -83
- data/lib/aspera/cli/option_declarator.rb +33 -42
- data/lib/aspera/cli/option_registry.rb +69 -0
- data/lib/aspera/cli/option_types.rb +103 -0
- data/lib/aspera/cli/option_value.rb +281 -0
- data/lib/aspera/cli/options.schema.yaml +38 -5
- data/lib/aspera/cli/parser.rb +307 -848
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +430 -380
- data/lib/aspera/cli/plugins/ats.rb +58 -73
- data/lib/aspera/cli/plugins/base.rb +190 -240
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +244 -178
- data/lib/aspera/cli/plugins/console.rb +102 -38
- data/lib/aspera/cli/plugins/cos.rb +6 -23
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex5.rb +176 -173
- data/lib/aspera/cli/plugins/faspio.rb +5 -10
- data/lib/aspera/cli/plugins/httpgw.rb +8 -11
- data/lib/aspera/cli/plugins/mcp.rb +20 -55
- data/lib/aspera/cli/plugins/node.rb +277 -311
- data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
- data/lib/aspera/cli/plugins/preview.rb +79 -90
- data/lib/aspera/cli/plugins/server.rb +76 -50
- data/lib/aspera/cli/plugins/shares.rb +68 -116
- data/lib/aspera/cli/preset_actions.rb +17 -10
- data/lib/aspera/cli/preset_manager.rb +12 -2
- data/lib/aspera/cli/prompt.rb +35 -0
- data/lib/aspera/cli/result.rb +13 -18
- data/lib/aspera/cli/runner.rb +31 -54
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +41 -37
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +0 -6
- data/lib/aspera/cli/transfer_agent.rb +29 -35
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +4 -2
- data/lib/aspera/coverage.rb +1 -0
- data/lib/aspera/environment.rb +6 -0
- data/lib/aspera/faspex_gw.rb +2 -1
- data/lib/aspera/faspex_postproc.rb +1 -0
- data/lib/aspera/graphql.rb +5 -5
- data/lib/aspera/json_rpc/client.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +22 -25
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_simulator.rb +1 -0
- data/lib/aspera/oauth/base.rb +35 -25
- data/lib/aspera/oauth/factory.rb +1 -0
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +7 -0
- data/lib/aspera/preview/options.rb +4 -4
- data/lib/aspera/preview/terminal.rb +4 -3
- data/lib/aspera/preview/utils.rb +9 -6
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/rainbow.rb +7 -0
- data/lib/aspera/rest/aspera_errors.rb +60 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +514 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +143 -0
- data/lib/aspera/rest/parameters.rb +55 -0
- data/lib/aspera/rest/util.rb +176 -0
- data/lib/aspera/rest.rb +7 -621
- data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/registry.rb +18 -1
- data/lib/aspera/schema/validator.rb +92 -0
- data/lib/aspera/secret_hider.rb +36 -25
- data/lib/aspera/string_ext.rb +15 -0
- data/lib/aspera/temp_file_manager.rb +6 -5
- data/lib/aspera/transfer/parameters.rb +2 -0
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/uri_reader.rb +11 -11
- data/lib/aspera/web_auth/index.html +147 -0
- data/lib/aspera/web_auth/server.rb +81 -0
- data.tar.gz.sig +0 -0
- metadata +39 -7
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- data/lib/aspera/rest_call_error.rb +0 -25
- data/lib/aspera/rest_error_analyzer.rb +0 -111
- data/lib/aspera/rest_errors_aspera.rb +0 -58
- data/lib/aspera/rest_list.rb +0 -136
- data/lib/aspera/web_auth.rb +0 -211
data/lib/aspera/agent/connect.rb
CHANGED
|
@@ -6,6 +6,8 @@ require 'aspera/products/other'
|
|
|
6
6
|
require 'aspera/rest'
|
|
7
7
|
require 'aspera/environment'
|
|
8
8
|
require 'securerandom'
|
|
9
|
+
require 'aspera/rainbow'
|
|
10
|
+
using Rainbow
|
|
9
11
|
|
|
10
12
|
module Aspera
|
|
11
13
|
module Agent
|
|
@@ -21,9 +23,9 @@ module Aspera
|
|
|
21
23
|
# agent_params must contain 'app_id'; the Connect URL is auto-discovered.
|
|
22
24
|
# @return [Hash] normalized status hash
|
|
23
25
|
def transfer_status(transfer_id, agent_params)
|
|
24
|
-
connect_api = Rest.new(
|
|
26
|
+
connect_api = Rest::Client.new(
|
|
25
27
|
base_url: "#{connect_api_url}/v5/connect",
|
|
26
|
-
headers: {'Origin' =>
|
|
28
|
+
headers: {'Origin' => Rest::Parameters.instance.user_agent}
|
|
27
29
|
)
|
|
28
30
|
tr_info = connect_api.create("transfers/info/#{transfer_id}", {'aspera_connect_settings' => {'app_id' => agent_params['app_id']}})
|
|
29
31
|
transfer = tr_info['transfer_info']
|
|
@@ -72,9 +74,9 @@ module Aspera
|
|
|
72
74
|
# raise exception if connect not started and file does not exist
|
|
73
75
|
connect_url = self.class.connect_api_url
|
|
74
76
|
Log.log.debug { "found: #{connect_url}" }
|
|
75
|
-
@connect_api = Rest.new(
|
|
77
|
+
@connect_api = Rest::Client.new(
|
|
76
78
|
base_url: "#{connect_url}/v5/connect", # could use v6 also now
|
|
77
|
-
headers: {'Origin' =>
|
|
79
|
+
headers: {'Origin' => Rest::Parameters.instance.user_agent}
|
|
78
80
|
)
|
|
79
81
|
connect_info = @connect_api.read('info/version')
|
|
80
82
|
Log.log.debug('Connect was reached') if method_index > 0
|
data/lib/aspera/agent/desktop.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Aspera
|
|
|
24
24
|
# @return [Hash] normalized status hash
|
|
25
25
|
def transfer_status(transfer_id, agent_params)
|
|
26
26
|
app_id = agent_params['application_id']
|
|
27
|
-
client = Aspera::JsonRpc::Client.new(Aspera::Rest.new(base_url: desktop_api_url))
|
|
27
|
+
client = Aspera::JsonRpc::Client.new(Aspera::Rest::Client.new(base_url: desktop_api_url))
|
|
28
28
|
raw = client.get_transfer(app_id: app_id, transfer_id: transfer_id)
|
|
29
29
|
normalize_status(raw['status'], bytes: raw['bytes_written'].to_i, error: raw['error_desc'])
|
|
30
30
|
end
|
|
@@ -70,7 +70,7 @@ module Aspera
|
|
|
70
70
|
begin
|
|
71
71
|
# curl 'http://127.0.0.1:33024/' -X POST -H 'content-type: application/json' --data-raw '{"jsonrpc":"2.0","params":[],"id":999999,"method":"rpc.discover"}'
|
|
72
72
|
# https://playground.open-rpc.org/?schemaUrl=http://127.0.0.1:33024
|
|
73
|
-
@client_app_api = Aspera::JsonRpc::Client.new(Aspera::Rest.new(base_url: aspera_client_api_url))
|
|
73
|
+
@client_app_api = Aspera::JsonRpc::Client.new(Aspera::Rest::Client.new(base_url: aspera_client_api_url))
|
|
74
74
|
client_info = @client_app_api.get_info
|
|
75
75
|
Log.dump(:client_version, client_info)
|
|
76
76
|
Log.log.debug('Client was reached') if method_index > 0
|
data/lib/aspera/agent/direct.rb
CHANGED
|
@@ -14,6 +14,8 @@ require 'socket'
|
|
|
14
14
|
require 'securerandom'
|
|
15
15
|
require 'shellwords'
|
|
16
16
|
require 'English'
|
|
17
|
+
require 'aspera/rainbow'
|
|
18
|
+
using Rainbow
|
|
17
19
|
|
|
18
20
|
module Aspera
|
|
19
21
|
module Agent
|
|
@@ -258,7 +260,7 @@ module Aspera
|
|
|
258
260
|
@resume_policy.execute_with_resume do
|
|
259
261
|
start_and_monitor_process(session: session, exec_spec: session[:exec_spec])
|
|
260
262
|
end
|
|
261
|
-
Log.log.debug('transfer ok'.
|
|
263
|
+
Log.log.debug('transfer ok'.bg(:green))
|
|
262
264
|
rescue => e
|
|
263
265
|
session[:error] = e
|
|
264
266
|
raise if Log.log.debug? || !e.is_a?(Transfer::Error)
|
data/lib/aspera/agent/node.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Aspera
|
|
|
24
24
|
else
|
|
25
25
|
rest_params[:auth] = {type: :basic, username: agent_params['username'], password: agent_params['password']}
|
|
26
26
|
end
|
|
27
|
-
node_api = Rest.new(**rest_params)
|
|
27
|
+
node_api = Rest::Client.new(**rest_params)
|
|
28
28
|
data = node_api.read("ops/transfers/#{transfer_id}") || {'status' => 'unknown'}
|
|
29
29
|
normalize_status(data)
|
|
30
30
|
end
|
|
@@ -71,7 +71,7 @@ module Aspera
|
|
|
71
71
|
password: password
|
|
72
72
|
}
|
|
73
73
|
end
|
|
74
|
-
@node_api = Rest.new(**rest_params)
|
|
74
|
+
@node_api = Rest::Client.new(**rest_params)
|
|
75
75
|
# TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
|
|
76
76
|
@transfer_id = nil
|
|
77
77
|
end
|
|
@@ -149,7 +149,7 @@ module Aspera
|
|
|
149
149
|
|
|
150
150
|
# used internally to ensure node api is set before using.
|
|
151
151
|
def node_api_
|
|
152
|
-
Aspera.assert(!@node_api.nil?, 'Before using this object, set the node_api attribute to a Aspera::Rest object')
|
|
152
|
+
Aspera.assert(!@node_api.nil?, 'Before using this object, set the node_api attribute to a Aspera::Rest::Client object')
|
|
153
153
|
return @node_api
|
|
154
154
|
end
|
|
155
155
|
end
|
data/lib/aspera/api/alee.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'aspera/api/aoc'
|
|
|
4
4
|
module Aspera
|
|
5
5
|
module Api
|
|
6
6
|
# Aspera License Entitlement Engine API client
|
|
7
|
-
class Alee < Aspera::Rest
|
|
7
|
+
class Alee < Aspera::Rest::Client
|
|
8
8
|
def initialize(entitlement_id, customer_id, api_domain: AoC::SAAS_DOMAIN_PROD, version: 'v1')
|
|
9
9
|
super(
|
|
10
10
|
base_url: "https://api.#{api_domain}/metering/#{version}",
|
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -7,16 +7,18 @@ require 'aspera/hash_ext'
|
|
|
7
7
|
require 'aspera/data_repository'
|
|
8
8
|
require 'aspera/transfer/spec'
|
|
9
9
|
require 'aspera/api/node'
|
|
10
|
-
require 'aspera/
|
|
10
|
+
require 'aspera/rest/list'
|
|
11
11
|
require 'base64'
|
|
12
|
+
require 'aspera/rainbow'
|
|
13
|
+
using Rainbow
|
|
12
14
|
|
|
13
15
|
module Aspera
|
|
14
16
|
module Api
|
|
15
17
|
# information relative to Files or Packages App
|
|
16
18
|
|
|
17
19
|
# Aspera on Cloud API client
|
|
18
|
-
class AoC < Rest
|
|
19
|
-
include
|
|
20
|
+
class AoC < Rest::Client
|
|
21
|
+
include Rest::List
|
|
20
22
|
|
|
21
23
|
module AppType
|
|
22
24
|
FILES = 'files'
|
|
@@ -140,7 +142,7 @@ module Aspera
|
|
|
140
142
|
# @param url [String] URL of AoC public link
|
|
141
143
|
# @return [Hash{Symbol => String, Hash}] Information about public link, or nil if not a public link
|
|
142
144
|
def link_info(url)
|
|
143
|
-
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET', ret: :resp).uri
|
|
145
|
+
final_uri = Rest::Client.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET', ret: :resp).uri
|
|
144
146
|
Log.dump(:final_uri, final_uri, level: :trace1)
|
|
145
147
|
org_domain = split_org_domain(final_uri)
|
|
146
148
|
if (m = final_uri.path.match(%r{/oauth2/([^/]+)/login$}))
|
|
@@ -202,8 +204,8 @@ module Aspera
|
|
|
202
204
|
Aspera.assert(block_given?, 'block required for call_paging')
|
|
203
205
|
# set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
|
|
204
206
|
query['per_page'] = 1000 unless query.key?('per_page')
|
|
205
|
-
max_items = query.delete(
|
|
206
|
-
max_pages = query.delete(
|
|
207
|
+
max_items = query.delete(Rest::List::MAX_ITEMS)
|
|
208
|
+
max_pages = query.delete(Rest::List::MAX_PAGES)
|
|
207
209
|
item_list = []
|
|
208
210
|
total_count = nil
|
|
209
211
|
current_page = query['page']
|
|
@@ -225,9 +227,9 @@ module Aspera
|
|
|
225
227
|
break if !max_items.nil? && item_list.count >= max_items
|
|
226
228
|
break if !max_pages.nil? && page_count >= max_pages
|
|
227
229
|
break if total_count&.<=(item_list.count)
|
|
228
|
-
|
|
230
|
+
Rest::Parameters.instance.spinner_cb.call("#{item_list.count} / #{total_count}") unless total_count.eql?(item_list.count.to_s)
|
|
229
231
|
end
|
|
230
|
-
|
|
232
|
+
Rest::Parameters.instance.spinner_cb.call(action: :success)
|
|
231
233
|
item_list = item_list[0..max_items - 1] if !max_items.nil? && item_list.count > max_items
|
|
232
234
|
return {items: item_list, total: total_count}
|
|
233
235
|
end
|
|
@@ -394,7 +396,7 @@ module Aspera
|
|
|
394
396
|
@cache_user_info =
|
|
395
397
|
begin
|
|
396
398
|
read('self')
|
|
397
|
-
rescue Aspera::
|
|
399
|
+
rescue Aspera::Rest::CallError => e
|
|
398
400
|
raise if exception || e.message.include?('invalid_grant')
|
|
399
401
|
Log.log.debug { "Ignoring error: (#{e.class}) #{e}" }
|
|
400
402
|
{}
|
|
@@ -496,9 +498,9 @@ module Aspera
|
|
|
496
498
|
}
|
|
497
499
|
else
|
|
498
500
|
# OAuth bearer token
|
|
499
|
-
|
|
500
|
-
node_params[:auth][:params]
|
|
501
|
-
node_params[:auth][:params][:scope] = Node.
|
|
501
|
+
# copy nested params, so that auth params of this API are not modified
|
|
502
|
+
node_params[:auth] = auth_params.merge(params: auth_params[:params]&.dup || {})
|
|
503
|
+
node_params[:auth][:params][:scope] = Node::Scope.join(access_key: node_info['access_key'], scope: scope)
|
|
502
504
|
node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::Scope::ADMIN)
|
|
503
505
|
# Special header required for bearer token only
|
|
504
506
|
node_params[:headers] = {Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
|
data/lib/aspera/api/ats.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'aspera/rest'
|
|
|
6
6
|
module Aspera
|
|
7
7
|
module Api
|
|
8
8
|
# Aspera Transfer Service API client without authentication
|
|
9
|
-
class Ats < Aspera::Rest
|
|
9
|
+
class Ats < Aspera::Rest::Client
|
|
10
10
|
SERVICE_BASE_URL = 'https://ats.aspera.io'
|
|
11
11
|
# currently supported clouds
|
|
12
12
|
# Note to Aspera: shall be an API call
|
data/lib/aspera/api/cos_node.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Aspera
|
|
|
22
22
|
Aspera.assert(service_credentials.key?(field)) { "service_credentials must have a field: #{field}" }
|
|
23
23
|
end
|
|
24
24
|
# read endpoints from service provided in service credentials
|
|
25
|
-
endpoints = Aspera::Rest.new(base_url: service_credentials['endpoints']).read('')
|
|
25
|
+
endpoints = Aspera::Rest::Client.new(base_url: service_credentials['endpoints']).read('')
|
|
26
26
|
Log.dump(:endpoints, endpoints)
|
|
27
27
|
endpoint = endpoints.dig('service-endpoints', 'regional', bucket_region, 'public', bucket_region)
|
|
28
28
|
Aspera.assert(!endpoint.nil?) { "no such region: #{bucket_region}" }
|
|
@@ -40,7 +40,7 @@ module Aspera
|
|
|
40
40
|
endpoint = "https://#{endpoint}" unless endpoint.start_with?('http')
|
|
41
41
|
@auth_url = auth_url
|
|
42
42
|
@api_key = api_key
|
|
43
|
-
s3_api = Aspera::Rest.new(
|
|
43
|
+
s3_api = Aspera::Rest::Client.new(
|
|
44
44
|
base_url: endpoint,
|
|
45
45
|
not_auth_codes: %w[401 403], # error codes when not authorized
|
|
46
46
|
headers: {'ibm-service-instance-id' => instance_id},
|
data/lib/aspera/api/faspex.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'aspera/assert'
|
|
4
4
|
require 'aspera/rest'
|
|
5
5
|
require 'aspera/oauth/base'
|
|
6
|
-
require 'aspera/
|
|
6
|
+
require 'aspera/rest/list'
|
|
7
7
|
require 'digest'
|
|
8
8
|
|
|
9
9
|
module Aspera
|
|
@@ -15,6 +15,7 @@ module Aspera
|
|
|
15
15
|
# @param context [String] The `context` query parameter in public link
|
|
16
16
|
# @param redirect_uri [String] URI of web UI login
|
|
17
17
|
# @param path_authorize [String] Path to provide passcode
|
|
18
|
+
# @param base_params [Hash] Parameters for base class
|
|
18
19
|
def initialize(
|
|
19
20
|
context:,
|
|
20
21
|
redirect_uri:,
|
|
@@ -48,18 +49,18 @@ module Aspera
|
|
|
48
49
|
raise Error, info['action_message'] if info['action_message']
|
|
49
50
|
Aspera.assert(info['code'], 'Missing code in answer')
|
|
50
51
|
# Exchange code for token
|
|
51
|
-
|
|
52
|
+
create_token_base(
|
|
52
53
|
grant_type: 'authorization_code',
|
|
53
54
|
code: info['code'],
|
|
54
55
|
redirect_uri: @redirect_uri
|
|
55
|
-
)
|
|
56
|
+
)
|
|
56
57
|
end
|
|
57
58
|
end
|
|
58
59
|
OAuth::Factory.instance.register_token_creator(FaspexPubLink)
|
|
59
60
|
module Api
|
|
60
61
|
# Aspera Faspex 5 API Client
|
|
61
|
-
class Faspex < Aspera::Rest
|
|
62
|
-
include
|
|
62
|
+
class Faspex < Aspera::Rest::Client
|
|
63
|
+
include Rest::List
|
|
63
64
|
|
|
64
65
|
# endpoint for authentication API
|
|
65
66
|
PATH_AUTH = 'auth'
|
|
@@ -141,6 +142,7 @@ module Aspera
|
|
|
141
142
|
|
|
142
143
|
# @param url [String] Faspex URL, can be a public link
|
|
143
144
|
# @param auth [Symbol] Authentication method: :boot (token in header), :web (open browser), :jwt (client_id + private key), :public_link (context in URL)
|
|
145
|
+
# @param root [String] Root path for API
|
|
144
146
|
# @param password [String, nil] For :boot auth, the token copied directly from browser in developer mode
|
|
145
147
|
# @param client_id [String, nil] For :web and :jwt auth, the client_id of web UI application
|
|
146
148
|
# @param client_secret [String, nil] For :web auth, the client_secret of web UI application (not needed for :jwt)
|
|
@@ -166,7 +168,7 @@ module Aspera
|
|
|
166
168
|
case auth
|
|
167
169
|
when :public_link
|
|
168
170
|
# Get URL of final redirect of provided public link
|
|
169
|
-
final_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', ret: :resp).uri.to_s
|
|
171
|
+
final_url = Rest::Client.new(base_url: url, redirect_max: 3).call(operation: 'GET', ret: :resp).uri.to_s
|
|
170
172
|
Log.dump(:final_url, final_url, level: :trace1)
|
|
171
173
|
# Get context from query
|
|
172
174
|
encoded_context = Rest.query_to_h(URI.parse(final_url).query)['context']
|
|
@@ -178,7 +180,7 @@ module Aspera
|
|
|
178
180
|
base_url = final_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
|
|
179
181
|
# Get web UI client_id and redirect_uri
|
|
180
182
|
# TODO: change this for something more reliable
|
|
181
|
-
config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET').sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
|
|
183
|
+
config = JSON.parse(Rest::Client.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET').sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
|
|
182
184
|
Log.dump(:config_js, config)
|
|
183
185
|
{
|
|
184
186
|
base_url: "#{base_url}/#{root}",
|
data/lib/aspera/api/httpgw.rb
CHANGED
|
@@ -9,8 +9,10 @@ require 'securerandom'
|
|
|
9
9
|
require 'websocket'
|
|
10
10
|
require 'base64'
|
|
11
11
|
require 'json'
|
|
12
|
+
require 'aspera/rainbow'
|
|
13
|
+
using Rainbow
|
|
12
14
|
|
|
13
|
-
#
|
|
15
|
+
# Throw exception on error, instead of error code
|
|
14
16
|
WebSocket.should_raise = true
|
|
15
17
|
|
|
16
18
|
module Aspera
|
|
@@ -29,7 +31,7 @@ module Aspera
|
|
|
29
31
|
# 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
|
|
30
32
|
# 2.. Binary File binary chunks "end upload" sent_general
|
|
31
33
|
# last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
|
|
32
|
-
class Httpgw < Aspera::Rest
|
|
34
|
+
class Httpgw < Aspera::Rest::Client
|
|
33
35
|
DEFAULT_BASE_PATH = '/aspera/http-gwy'
|
|
34
36
|
INFO_ENDPOINT = 'info'
|
|
35
37
|
MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
|
|
@@ -40,27 +42,24 @@ module Aspera
|
|
|
40
42
|
API_V1 = 'v1'
|
|
41
43
|
API_V2 = 'v2'
|
|
42
44
|
THR_RECV = 'recv'
|
|
43
|
-
LOG_WS_SEND = 'ws: send: '.red
|
|
44
|
-
LOG_WS_RECV = "ws: #{THR_RECV}: ".green
|
|
45
45
|
private_constant :MSG_RECV_DATA_RECEIVED_SIGNAL, :MSG_RECV_SLICE_UPLOAD_SIGNAL
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
# Send message on http gw web socket
|
|
48
|
+
# @param msg_type [String] type of message to send
|
|
49
|
+
# @param payload [Hash] Data to send
|
|
47
50
|
def ws_snd_json(msg_type, payload)
|
|
48
51
|
if msg_type.eql?(MSG_SEND_SLICE_UPLOAD) && @upload_version.eql?(API_V2)
|
|
49
52
|
@shared_info[:count][:sent_v2_delimiter] += 1
|
|
50
53
|
else
|
|
51
54
|
@shared_info[:count][:sent_general] += 1
|
|
52
55
|
end
|
|
53
|
-
Log.
|
|
54
|
-
log_data = payload.dup
|
|
55
|
-
log_data[:data] = "[data #{log_data[:data].length} bytes]" if log_data.key?(:data)
|
|
56
|
-
"#{LOG_WS_SEND}json: #{msg_type}: #{JSON.generate(log_data)}"
|
|
57
|
-
end
|
|
56
|
+
Log.dump("#{log_ws_send} #{msg_type}", level: :trace1) { payload.dup.tap { |d| d[:data] = "[data #{d[:data].length} bytes]" if d.key?(:data) } }
|
|
58
57
|
ws_send(ws_type: :text, data: JSON.generate({msg_type => payload}))
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
# send data on http gw web socket
|
|
62
61
|
def ws_send(ws_type:, data:)
|
|
63
|
-
Log.log.trace1 { "#{
|
|
62
|
+
Log.log.trace1 { "#{log_ws_send}sending: #{ws_type} (#{data&.length || 0} bytes)" }
|
|
64
63
|
@shared_info[:count][:sent_general] += 1 if ws_type.eql?(:binary)
|
|
65
64
|
frame_generator = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: ws_type, version: @ws_handshake.version)
|
|
66
65
|
@ws_io.write(frame_generator.to_s)
|
|
@@ -72,12 +71,12 @@ module Aspera
|
|
|
72
71
|
@shared_info[:read_exception].nil? &&
|
|
73
72
|
(((@shared_info[:count][:sent_general] - @shared_info[:count][:received_general]) > 1) ||
|
|
74
73
|
((@shared_info[:count][:received_v2_delimiter] - @shared_info[:count][:sent_v2_delimiter]) > 1))
|
|
75
|
-
Log.log.trace1 { "#{
|
|
74
|
+
Log.log.trace1 { "#{log_ws_send}#{'timeout'.blue}: #{@shared_info[:count]}" } if !@shared_info[:cond_var].wait(@shared_info[:mutex], 2.0)
|
|
76
75
|
end
|
|
77
76
|
end
|
|
78
77
|
end
|
|
79
78
|
raise @shared_info[:read_exception] unless @shared_info[:read_exception].nil?
|
|
80
|
-
Log.log.trace2 { "#{
|
|
79
|
+
Log.log.trace2 { "#{log_ws_send}counts: #{@shared_info[:count]}" }
|
|
81
80
|
end
|
|
82
81
|
|
|
83
82
|
# Check header ourself and give precise error message, as websocket will only throw error without details
|
|
@@ -85,13 +84,13 @@ module Aspera
|
|
|
85
84
|
# @return [String] Response Header
|
|
86
85
|
def validated_ws_response_header(header)
|
|
87
86
|
first_line = header.split("\r\n").first
|
|
88
|
-
raise
|
|
87
|
+
raise Rest::CallError.new({messages: ["Unexpected: #{first_line}", 'Expected: 101 Switching Protocols']}) unless first_line.split(/\s+/, 3)[1].eql?('101')
|
|
89
88
|
header
|
|
90
89
|
end
|
|
91
90
|
|
|
92
91
|
# message processing for read thread
|
|
93
92
|
def process_received_message(message)
|
|
94
|
-
Log.log.debug { "#{
|
|
93
|
+
Log.log.debug { "#{log_ws_recv}message: [#{message}] (#{message.class})" }
|
|
95
94
|
if message.eql?(MSG_RECV_DATA_RECEIVED_SIGNAL)
|
|
96
95
|
@shared_info[:mutex].synchronize do
|
|
97
96
|
@shared_info[:count][:received_general] += 1
|
|
@@ -117,23 +116,23 @@ module Aspera
|
|
|
117
116
|
end
|
|
118
117
|
end
|
|
119
118
|
|
|
120
|
-
#
|
|
119
|
+
# Main function of read thread
|
|
121
120
|
def process_read_thread
|
|
122
|
-
Log.log.debug { "#{
|
|
121
|
+
Log.log.debug { "#{log_ws_recv}read thread started" }
|
|
123
122
|
frame_parser = ::WebSocket::Frame::Incoming::Client.new(version: @ws_handshake.version)
|
|
124
123
|
until @ws_io.eof?
|
|
125
124
|
begin
|
|
126
125
|
# ready byte by byte until frame is ready
|
|
127
126
|
# blocking read
|
|
128
127
|
byte = @ws_io.read(1)
|
|
129
|
-
Log.log.trace2 { "#{
|
|
128
|
+
Log.log.trace2 { "#{log_ws_recv}read: #{byte} (#{byte.class}) eof=#{@ws_io.eof?}" }
|
|
130
129
|
frame_parser << byte
|
|
131
130
|
frame_ok = frame_parser.next
|
|
132
131
|
next if frame_ok.nil?
|
|
133
132
|
process_received_message(frame_ok.data.to_s)
|
|
134
|
-
Log.log.trace2 { "#{
|
|
133
|
+
Log.log.trace2 { "#{log_ws_recv}counts: #{@shared_info[:count]}" }
|
|
135
134
|
rescue => e
|
|
136
|
-
Log.log.debug { "#{
|
|
135
|
+
Log.log.debug { "#{log_ws_recv}Exception: #{e}" }
|
|
137
136
|
@shared_info[:mutex].synchronize do
|
|
138
137
|
@shared_info[:read_exception] = e
|
|
139
138
|
@shared_info[:cond_var].signal
|
|
@@ -142,9 +141,9 @@ module Aspera
|
|
|
142
141
|
end
|
|
143
142
|
end
|
|
144
143
|
Log.log.debug do
|
|
145
|
-
"#{
|
|
144
|
+
"#{log_ws_recv}exception: #{@shared_info[:read_exception]},cls=#{@shared_info[:read_exception].class})"
|
|
146
145
|
end unless @shared_info[:read_exception].nil?
|
|
147
|
-
Log.log.debug { "#{
|
|
146
|
+
Log.log.debug { "#{log_ws_recv}read thread stopped (ws eof=#{@ws_io.eof?})" }
|
|
148
147
|
end
|
|
149
148
|
|
|
150
149
|
def upload(transfer_spec)
|
|
@@ -170,7 +169,7 @@ module Aspera
|
|
|
170
169
|
# Get whole HTTP response header, Check and process
|
|
171
170
|
# no need to check `finished?` or result of `<<` (true), as we give the whole header at once
|
|
172
171
|
@ws_handshake << validated_ws_response_header(@ws_io.readuntil("\r\n\r\n"))
|
|
173
|
-
Log.log.debug { "#{
|
|
172
|
+
Log.log.debug { "#{log_ws_send}handshake success" }
|
|
174
173
|
# data shared between main thread and read thread
|
|
175
174
|
@shared_info = {
|
|
176
175
|
read_exception: nil, # error message if any in callback
|
|
@@ -224,7 +223,7 @@ module Aspera
|
|
|
224
223
|
# send once, before data, at beginning
|
|
225
224
|
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(0)
|
|
226
225
|
ws_send(ws_type: :binary, data: slice_bin_data)
|
|
227
|
-
Log.log.trace1 { "#{
|
|
226
|
+
Log.log.trace1 { "#{log_ws_send}buffer: file: #{file_index}, slice: #{slice_info[:slice]}/#{last_slice}" }
|
|
228
227
|
# send once, after data, at end
|
|
229
228
|
ws_snd_json(MSG_SEND_SLICE_UPLOAD, slice_info) if slice_info[:slice].eql?(last_slice)
|
|
230
229
|
end
|
|
@@ -283,17 +282,20 @@ module Aspera
|
|
|
283
282
|
# it is a plain file if we don't require zip and there is only one file
|
|
284
283
|
File.basename(default_file_name)
|
|
285
284
|
end
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
call(
|
|
286
|
+
operation: 'GET',
|
|
287
|
+
subpath: "download/#{transfer_uuid}",
|
|
288
|
+
save_to: File.join(transfer_spec['destination_root'], file_name)
|
|
289
|
+
)
|
|
288
290
|
end
|
|
289
291
|
|
|
290
292
|
def info
|
|
291
|
-
|
|
293
|
+
@api_info
|
|
292
294
|
end
|
|
293
295
|
|
|
294
296
|
# @return [String] the base url of the gateway
|
|
295
297
|
def base_url
|
|
296
|
-
|
|
298
|
+
@gw_root_url
|
|
297
299
|
end
|
|
298
300
|
|
|
299
301
|
# @param url [String] URL of the HTTP Gateway, without version
|
|
@@ -306,10 +308,10 @@ module Aspera
|
|
|
306
308
|
**opts
|
|
307
309
|
)
|
|
308
310
|
Log.dump(:gw_url, url)
|
|
309
|
-
#
|
|
311
|
+
# Add scheme if missing
|
|
310
312
|
url = "https://#{url}" unless url.match?(%r{^[a-z]{1,6}://})
|
|
311
313
|
Aspera.assert(url.start_with?('https://'), type: Error) { 'GW URL shall be with scheme https' }
|
|
312
|
-
#
|
|
314
|
+
# Remove trailing slash and version (o=only once) if present
|
|
313
315
|
# TODO: issue warning ?
|
|
314
316
|
url = url.chomp('/').gsub(%r{/#{API_V1}$}o, '')
|
|
315
317
|
# assume GW is always under specific path (TODO: remove this ?)
|
|
@@ -339,6 +341,10 @@ module Aspera
|
|
|
339
341
|
|
|
340
342
|
private
|
|
341
343
|
|
|
344
|
+
# Log prefixes (evaluated on each call, as colors can be enabled or disabled by option)
|
|
345
|
+
def log_ws_send = 'ws: send: '.red
|
|
346
|
+
def log_ws_recv = "ws: #{THR_RECV}: ".green
|
|
347
|
+
|
|
342
348
|
# compute total size of files to upload (for progress)
|
|
343
349
|
# modify transfer spec to be suitable for HTTPGW
|
|
344
350
|
# @param transfer_spec [Hash] transfer specification
|
|
@@ -372,8 +378,7 @@ module Aspera
|
|
|
372
378
|
until folders_to_process.empty?
|
|
373
379
|
folder = folders_to_process.shift
|
|
374
380
|
# read all entries
|
|
375
|
-
Dir.
|
|
376
|
-
next if entry.eql?('.') || entry.eql?('..')
|
|
381
|
+
Dir.each_child(folder) do |entry|
|
|
377
382
|
entry_path = File.join(folder, entry)
|
|
378
383
|
if File.directory?(entry_path)
|
|
379
384
|
folders_to_process.push(entry_path)
|
|
@@ -396,7 +401,6 @@ module Aspera
|
|
|
396
401
|
end
|
|
397
402
|
transfer_spec['paths'] = files_to_send.map { |i| {'source' => i[:name]} }
|
|
398
403
|
files_to_send.push(total_bytes_to_transfer)
|
|
399
|
-
return files_to_send
|
|
400
404
|
end
|
|
401
405
|
end
|
|
402
406
|
end
|