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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +20 -0
  4. data/CONTRIBUTING.md +16 -4
  5. data/README.md +344 -164
  6. data/bin/asession +26 -19
  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 +8 -8
  11. data/lib/aspera/agent/base.rb +2 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +23 -24
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +126 -97
  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 +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +99 -42
  25. data/lib/aspera/ascp/management.rb +3 -2
  26. data/lib/aspera/ascp/products.rb +12 -0
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +27 -17
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +5 -0
  33. data/lib/aspera/cli/plugin.rb +15 -29
  34. data/lib/aspera/cli/plugins/alee.rb +3 -3
  35. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  36. data/lib/aspera/cli/plugins/ats.rb +16 -14
  37. data/lib/aspera/cli/plugins/config.rb +53 -45
  38. data/lib/aspera/cli/plugins/console.rb +3 -3
  39. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  40. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  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 +153 -95
  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 +35 -17
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/command_line_builder.rb +3 -4
  53. data/lib/aspera/coverage.rb +13 -1
  54. data/lib/aspera/environment.rb +34 -18
  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/oauth/base.rb +39 -45
  60. data/lib/aspera/oauth/factory.rb +11 -4
  61. data/lib/aspera/oauth/generic.rb +4 -8
  62. data/lib/aspera/oauth/jwt.rb +3 -3
  63. data/lib/aspera/oauth/url_json.rb +1 -2
  64. data/lib/aspera/oauth/web.rb +5 -2
  65. data/lib/aspera/persistency_action_once.rb +16 -8
  66. data/lib/aspera/preview/utils.rb +5 -16
  67. data/lib/aspera/rest.rb +100 -76
  68. data/lib/aspera/transfer/faux_file.rb +4 -4
  69. data/lib/aspera/transfer/parameters.rb +14 -16
  70. data/lib/aspera/transfer/spec.rb +12 -12
  71. data/lib/aspera/transfer/sync.rb +1 -5
  72. data/lib/aspera/transfer/uri.rb +1 -1
  73. data/lib/aspera/uri_reader.rb +1 -1
  74. data/lib/aspera/web_auth.rb +166 -17
  75. data/lib/aspera/web_server_simple.rb +4 -3
  76. data/lib/transfer_pb.rb +84 -0
  77. data/lib/transfer_services_pb.rb +82 -0
  78. data.tar.gz.sig +0 -0
  79. metadata +24 -5
  80. metadata.gz.sig +0 -0
@@ -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
- # 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: '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
- @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.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
- payload[:body] = {}
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(token_id) if cache
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(token_id)
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(token_id, json_data)
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(token_id, json_data)
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
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
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
- def id(*params)
33
- return [PERSIST_CATEGORY_TOKEN, *params].flatten
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 is the identifier
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
@@ -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 if response_type
21
- @create_params[:apikey] = apikey if apikey
22
- @create_params[:receiver_client_ids] = receiver_client_ids if 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
@@ -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: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
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)
@@ -13,10 +13,9 @@ module Aspera
13
13
  json:,
14
14
  **generic_params
15
15
  )
16
- super(**generic_params)
16
+ super(**generic_params, cache_ids: [json[:url_token]])
17
17
  @body = json
18
18
  @query = url
19
- @identifiers.push(@body[:url_token])
20
19
  end
21
20
 
22
21
  def create_token
@@ -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: nil, parse: nil, format: nil, merge: nil)
18
- Aspera.assert(!manager.nil?)
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
- # by default , at save time, file is deleted if data is nil
26
- @delete_condition = delete || lambda{|d|d.empty?}
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
- persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
39
+ merge.call(@persisted_object, parse.call(value)) unless value.nil?
32
40
  end
33
41
 
34
42
  def save
@@ -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'], check_code: false)
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, check_code: true)
46
+ def external_command(command_sym, command_args)
47
47
  Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
48
- # build command line, and quote special characters
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
- result = external_command(:ffprobe, [
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)