aspera-cli 4.19.0 → 4.20.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +20 -0
- data/CONTRIBUTING.md +16 -4
- data/README.md +344 -164
- data/bin/asession +26 -19
- data/examples/build_exec +65 -76
- data/examples/build_exec_rubyc +40 -0
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +2 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +23 -24
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +126 -97
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +15 -10
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +99 -42
- data/lib/aspera/ascp/management.rb +3 -2
- data/lib/aspera/ascp/products.rb +12 -0
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +27 -17
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +5 -0
- data/lib/aspera/cli/plugin.rb +15 -29
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +53 -45
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +153 -95
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +5 -9
- data/lib/aspera/cli/plugins/shares.rb +2 -2
- data/lib/aspera/cli/sync_actions.rb +2 -2
- data/lib/aspera/cli/transfer_agent.rb +12 -14
- data/lib/aspera/cli/transfer_progress.rb +35 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +34 -18
- data/lib/aspera/faspex_gw.rb +2 -2
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +3 -4
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +3 -3
- data/lib/aspera/oauth/url_json.rb +1 -2
- data/lib/aspera/oauth/web.rb +5 -2
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/transfer/faux_file.rb +4 -4
- data/lib/aspera/transfer/parameters.rb +14 -16
- data/lib/aspera/transfer/spec.rb +12 -12
- data/lib/aspera/transfer/sync.rb +1 -5
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -17
- data/lib/aspera/web_server_simple.rb +4 -3
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +24 -5
- metadata.gz.sig +0 -0
data/lib/aspera/oauth/base.rb
CHANGED
@@ -3,64 +3,66 @@
|
|
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
|
-
#
|
12
|
-
# bearer
|
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
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
# @param
|
20
|
-
# @param
|
21
|
-
# @param
|
22
|
-
# @param
|
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',
|
34
|
-
token_field: 'access_token'
|
28
|
+
path_token: 'token',
|
29
|
+
token_field: 'access_token',
|
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
|
-
|
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
|
-
@
|
46
|
-
@
|
47
|
-
|
48
|
-
@api
|
49
|
-
|
50
|
-
|
51
|
-
|
41
|
+
@base_cache_ids = cache_ids.clone
|
42
|
+
@base_cache_ids = [] if @base_cache_ids.nil?
|
43
|
+
Aspera.assert_type(@base_cache_ids, Array)
|
44
|
+
if @api.auth_params.key?(:username)
|
45
|
+
cache_ids.push(@api.auth_params[:username])
|
46
|
+
end
|
47
|
+
@base_cache_ids.freeze
|
48
|
+
self.scope = scope
|
49
|
+
end
|
50
|
+
|
51
|
+
# Scope can be modified after creation, then update identifier for cache
|
52
|
+
def scope=(scope)
|
53
|
+
@scope = scope
|
54
|
+
# generate token unique identifier for persistency (memory/disk cache)
|
55
|
+
@token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
|
52
56
|
end
|
53
57
|
|
54
58
|
# helper method to create token as per RFC
|
55
59
|
def create_token_call(creation_params)
|
56
60
|
Log.log.debug{'Generating a new token'.bg_green}
|
57
|
-
payload = {
|
58
|
-
body: creation_params,
|
59
|
-
body_type: :www
|
60
|
-
}
|
61
|
+
payload = { body_type: :www }
|
61
62
|
if @use_query
|
62
63
|
payload[:query] = creation_params
|
63
|
-
|
64
|
+
else
|
65
|
+
payload[:body] = creation_params
|
64
66
|
end
|
65
67
|
return @api.call(
|
66
68
|
operation: 'POST',
|
@@ -85,16 +87,8 @@ module Aspera
|
|
85
87
|
# @param cache set to false to disable cache
|
86
88
|
# @param refresh set to true to force refresh or re-generation (if previous failed)
|
87
89
|
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
90
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
97
|
-
token_data = Factory.instance.persist_mgr.get(
|
91
|
+
token_data = Factory.instance.persist_mgr.get(@token_cache_id) if cache
|
98
92
|
token_data = JSON.parse(token_data) unless token_data.nil?
|
99
93
|
# Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
|
100
94
|
# might help in case the transfer agent cannot refresh himself
|
@@ -120,7 +114,7 @@ module Aspera
|
|
120
114
|
refresh_token = token_data['refresh_token']
|
121
115
|
end
|
122
116
|
# delete cache
|
123
|
-
Factory.instance.persist_mgr.delete(
|
117
|
+
Factory.instance.persist_mgr.delete(@token_cache_id)
|
124
118
|
token_data = nil
|
125
119
|
# lets try the existing refresh token
|
126
120
|
if !refresh_token.nil?
|
@@ -132,7 +126,7 @@ module Aspera
|
|
132
126
|
# save only if success
|
133
127
|
json_data = resp[:http].body
|
134
128
|
token_data = JSON.parse(json_data)
|
135
|
-
Factory.instance.persist_mgr.put(
|
129
|
+
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
136
130
|
else
|
137
131
|
Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
|
138
132
|
end
|
@@ -144,7 +138,7 @@ module Aspera
|
|
144
138
|
resp = create_token
|
145
139
|
json_data = resp[:http].body
|
146
140
|
token_data = JSON.parse(json_data)
|
147
|
-
Factory.instance.persist_mgr.put(
|
141
|
+
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
148
142
|
end
|
149
143
|
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
150
144
|
# ok we shall have a token here
|
data/lib/aspera/oauth/factory.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'aspera/id_generator'
|
4
4
|
require 'aspera/assert'
|
5
|
+
require 'singleton'
|
5
6
|
require 'base64'
|
6
7
|
module Aspera
|
7
8
|
module OAuth
|
@@ -29,11 +30,17 @@ module Aspera
|
|
29
30
|
return token[BEARER_PREFIX.length..-1]
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
33
|
+
# @return a cache identifier
|
34
|
+
def cache_id(url, creator_class, *params)
|
35
|
+
return IdGenerator.from_list([
|
36
|
+
PERSIST_CATEGORY_TOKEN,
|
37
|
+
url,
|
38
|
+
Factory.class_to_id(creator_class),
|
39
|
+
*params
|
40
|
+
].flatten)
|
34
41
|
end
|
35
42
|
|
36
|
-
# snake version of class name
|
43
|
+
# @return snake version of class name
|
37
44
|
def class_to_id(creator_class)
|
38
45
|
return creator_class.name.split('::').last.capital_to_snake.to_sym
|
39
46
|
end
|
data/lib/aspera/oauth/generic.rb
CHANGED
@@ -13,17 +13,13 @@ module Aspera
|
|
13
13
|
receiver_client_ids: nil,
|
14
14
|
**base_params
|
15
15
|
)
|
16
|
-
super(**base_params)
|
16
|
+
super(**base_params, cache_ids: [grant_type&.split(':')&.last, apikey, response_type])
|
17
17
|
@create_params = {
|
18
18
|
grant_type: grant_type
|
19
19
|
}
|
20
|
-
@create_params[:response_type] = response_type
|
21
|
-
@create_params[:apikey] = apikey
|
22
|
-
@create_params[:receiver_client_ids] = receiver_client_ids
|
23
|
-
@identifiers.push(
|
24
|
-
@create_params[:grant_type]&.split(':')&.last,
|
25
|
-
@create_params[:apikey],
|
26
|
-
@create_params[:response_type])
|
20
|
+
@create_params[:response_type] = response_type unless response_type.nil?
|
21
|
+
@create_params[:apikey] = apikey unless apikey.nil?
|
22
|
+
@create_params[:receiver_client_ids] = receiver_client_ids unless receiver_client_ids.nil?
|
27
23
|
end
|
28
24
|
|
29
25
|
def create_token
|
data/lib/aspera/oauth/jwt.rb
CHANGED
@@ -13,6 +13,7 @@ module Aspera
|
|
13
13
|
# https://tools.ietf.org/html/rfc7523
|
14
14
|
# https://tools.ietf.org/html/rfc7519
|
15
15
|
class Jwt < Base
|
16
|
+
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
16
17
|
# @param private_key_obj private key object
|
17
18
|
# @param payload payload to be included in the JWT
|
18
19
|
# @param headers headers to be included in the JWT
|
@@ -25,11 +26,10 @@ module Aspera
|
|
25
26
|
Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
|
26
27
|
Aspera.assert_type(payload, Hash){'payload'}
|
27
28
|
Aspera.assert_type(headers, Hash){'headers'}
|
28
|
-
super(**base_params)
|
29
|
+
super(**base_params, cache_ids: [payload[:sub]])
|
29
30
|
@private_key_obj = private_key_obj
|
30
31
|
@additional_payload = payload
|
31
32
|
@headers = headers
|
32
|
-
@identifiers.push(@additional_payload[:sub])
|
33
33
|
end
|
34
34
|
|
35
35
|
def create_token
|
@@ -46,7 +46,7 @@ module Aspera
|
|
46
46
|
Log.log.debug{"private=[#{@private_key_obj}]"}
|
47
47
|
assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
|
48
48
|
Log.log.debug{"assertion=[#{assertion}]"}
|
49
|
-
return create_token_call(optional_scope_client_id.merge(grant_type:
|
49
|
+
return create_token_call(optional_scope_client_id.merge(grant_type: GRANT_TYPE, assertion: assertion))
|
50
50
|
end
|
51
51
|
end
|
52
52
|
Factory.instance.register_token_creator(Jwt)
|
data/lib/aspera/oauth/web.rb
CHANGED
@@ -8,6 +8,9 @@ module Aspera
|
|
8
8
|
module OAuth
|
9
9
|
# Authentication using Web browser
|
10
10
|
class Web < Base
|
11
|
+
class << self
|
12
|
+
attr_accessor :additionnal_info
|
13
|
+
end
|
11
14
|
# @param redirect_uri url to receive the code after auth (to be exchanged for token)
|
12
15
|
# @param path_authorize path to login page on web app
|
13
16
|
def initialize(
|
@@ -28,12 +31,12 @@ module Aspera
|
|
28
31
|
# generate secure state to check later
|
29
32
|
random_state = SecureRandom.uuid
|
30
33
|
login_page_url = Rest.build_uri(
|
31
|
-
"#{@base_url}/#{@path_authorize}",
|
34
|
+
"#{@api.base_url}/#{@path_authorize}",
|
32
35
|
optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
|
33
36
|
# here, we need a human to authorize on a web page
|
34
37
|
Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
|
35
38
|
# start a web server to receive request code
|
36
|
-
web_server = WebAuth.new(@redirect_uri)
|
39
|
+
web_server = WebAuth.new(@redirect_uri, self.class.additionnal_info)
|
37
40
|
# start browser on login page
|
38
41
|
Environment.instance.open_uri(login_page_url)
|
39
42
|
# wait for code in request
|
@@ -7,6 +7,13 @@ require 'aspera/assert'
|
|
7
7
|
module Aspera
|
8
8
|
# Persist data on file system
|
9
9
|
class PersistencyActionOnce
|
10
|
+
DELETE_DEFAULT = lambda{|d|d.empty?}
|
11
|
+
PARSE_DEFAULT = lambda {|t| JSON.parse(t)}
|
12
|
+
FORMAT_DEFAULT = lambda {|h| JSON.generate(h)}
|
13
|
+
MERGE_DEFAULT = lambda {|current, file| current.concat(file).uniq rescue current}
|
14
|
+
MANAGER_METHODS = %i[get put delete]
|
15
|
+
private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
|
16
|
+
|
10
17
|
# @param :manager Mandatory Database
|
11
18
|
# @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
|
12
19
|
# @param :id Mandatory identifiers
|
@@ -14,21 +21,22 @@ module Aspera
|
|
14
21
|
# @param :parse Optional parse method (default to JSON)
|
15
22
|
# @param :format Optional dump method (default to JSON)
|
16
23
|
# @param :merge Optional merge data from file to current data
|
17
|
-
def initialize(manager:, data:, id:, delete:
|
18
|
-
Aspera.assert(
|
24
|
+
def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
|
25
|
+
Aspera.assert(MANAGER_METHODS.all?{|i|manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
|
19
26
|
Aspera.assert(!data.nil?)
|
20
27
|
Aspera.assert_type(id, String)
|
21
28
|
Aspera.assert(!id.empty?)
|
29
|
+
Aspera.assert_type(delete, Proc)
|
30
|
+
Aspera.assert_type(parse, Proc)
|
31
|
+
Aspera.assert_type(format, Proc)
|
32
|
+
Aspera.assert_type(merge, Proc)
|
22
33
|
@manager = manager
|
23
34
|
@persisted_object = data
|
24
35
|
@object_id = id
|
25
|
-
|
26
|
-
@
|
27
|
-
@persist_format = format || lambda {|h| JSON.generate(h)}
|
28
|
-
persist_parse = parse || lambda {|t| JSON.parse(t)}
|
29
|
-
persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
|
36
|
+
@delete_condition = delete
|
37
|
+
@persist_format = format
|
30
38
|
value = @manager.get(@object_id)
|
31
|
-
|
39
|
+
merge.call(@persisted_object, parse.call(value)) unless value.nil?
|
32
40
|
end
|
33
41
|
|
34
42
|
def save
|
data/lib/aspera/preview/utils.rb
CHANGED
@@ -32,7 +32,7 @@ module Aspera
|
|
32
32
|
tools_to_check.delete(:unoconv) if skip_types.include?(:office)
|
33
33
|
# Check for binaries
|
34
34
|
tools_to_check.each do |command_sym|
|
35
|
-
external_command(command_sym, ['-h']
|
35
|
+
external_command(command_sym, ['-h'])
|
36
36
|
rescue Errno::ENOENT => e
|
37
37
|
raise "missing #{command_sym} binary: #{e}"
|
38
38
|
rescue
|
@@ -43,19 +43,9 @@ module Aspera
|
|
43
43
|
# execute external command
|
44
44
|
# one could use "system", but we would need to redirect stdout/err
|
45
45
|
# @return true if su
|
46
|
-
def external_command(command_sym, command_args
|
46
|
+
def external_command(command_sym, command_args)
|
47
47
|
Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
|
48
|
-
|
49
|
-
command_line = command_args.clone.unshift(command_sym).map{|i| shell_quote(i.to_s)}.join(' ')
|
50
|
-
Log.log.debug{"cmd=#{command_line}".blue}
|
51
|
-
stdout, stderr, status = Open3.capture3(command_line)
|
52
|
-
if check_code && !status.success?
|
53
|
-
Log.log.error{"status: #{status}"}
|
54
|
-
Log.log.error{"stdout: #{stdout}"}
|
55
|
-
Log.log.error{"stderr: #{stderr}"}
|
56
|
-
raise "#{command_sym} error #{status}"
|
57
|
-
end
|
58
|
-
return {status: status, stdout: stdout}
|
48
|
+
return Environment.secure_capture(command_sym.to_s, *command_args)
|
59
49
|
end
|
60
50
|
|
61
51
|
def ffmpeg(a)
|
@@ -73,12 +63,11 @@ module Aspera
|
|
73
63
|
|
74
64
|
# @return Float in seconds
|
75
65
|
def video_get_duration(input_file)
|
76
|
-
|
66
|
+
return external_command(:ffprobe, [
|
77
67
|
'-loglevel', 'error',
|
78
68
|
'-show_entries', 'format=duration',
|
79
69
|
'-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
|
80
|
-
input_file])
|
81
|
-
return result[:stdout].to_f
|
70
|
+
input_file]).to_f
|
82
71
|
end
|
83
72
|
|
84
73
|
def ffmpeg_fmt(temp_folder)
|