aspera-cli 4.13.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +28 -5
  4. data/CONTRIBUTING.md +17 -1
  5. data/README.md +782 -401
  6. data/examples/dascli +1 -1
  7. data/examples/rubyc +24 -0
  8. data/lib/aspera/aoc.rb +21 -32
  9. data/lib/aspera/ascmd.rb +1 -0
  10. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  11. data/lib/aspera/cli/formatter.rb +17 -25
  12. data/lib/aspera/cli/main.rb +21 -27
  13. data/lib/aspera/cli/manager.rb +128 -114
  14. data/lib/aspera/cli/plugin.rb +87 -38
  15. data/lib/aspera/cli/plugins/alee.rb +2 -2
  16. data/lib/aspera/cli/plugins/aoc.rb +216 -102
  17. data/lib/aspera/cli/plugins/ats.rb +16 -18
  18. data/lib/aspera/cli/plugins/bss.rb +3 -3
  19. data/lib/aspera/cli/plugins/config.rb +177 -367
  20. data/lib/aspera/cli/plugins/console.rb +4 -6
  21. data/lib/aspera/cli/plugins/cos.rb +12 -13
  22. data/lib/aspera/cli/plugins/faspex.rb +17 -18
  23. data/lib/aspera/cli/plugins/faspex5.rb +332 -216
  24. data/lib/aspera/cli/plugins/node.rb +171 -142
  25. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  26. data/lib/aspera/cli/plugins/preview.rb +38 -60
  27. data/lib/aspera/cli/plugins/server.rb +22 -15
  28. data/lib/aspera/cli/plugins/shares.rb +24 -33
  29. data/lib/aspera/cli/plugins/sync.rb +3 -3
  30. data/lib/aspera/cli/transfer_agent.rb +29 -26
  31. data/lib/aspera/cli/version.rb +1 -1
  32. data/lib/aspera/colors.rb +9 -7
  33. data/lib/aspera/data/6 +0 -0
  34. data/lib/aspera/environment.rb +7 -3
  35. data/lib/aspera/fasp/agent_connect.rb +5 -0
  36. data/lib/aspera/fasp/agent_direct.rb +5 -5
  37. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  38. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  39. data/lib/aspera/fasp/error_info.rb +2 -0
  40. data/lib/aspera/fasp/installation.rb +18 -19
  41. data/lib/aspera/fasp/parameters.rb +18 -17
  42. data/lib/aspera/fasp/parameters.yaml +2 -1
  43. data/lib/aspera/fasp/resume_policy.rb +3 -3
  44. data/lib/aspera/fasp/transfer_spec.rb +6 -5
  45. data/lib/aspera/fasp/uri.rb +23 -21
  46. data/lib/aspera/faspex_postproc.rb +1 -1
  47. data/lib/aspera/hash_ext.rb +12 -2
  48. data/lib/aspera/keychain/macos_security.rb +13 -13
  49. data/lib/aspera/log.rb +1 -0
  50. data/lib/aspera/node.rb +62 -80
  51. data/lib/aspera/oauth.rb +1 -1
  52. data/lib/aspera/persistency_action_once.rb +1 -1
  53. data/lib/aspera/preview/terminal.rb +61 -15
  54. data/lib/aspera/preview/utils.rb +3 -3
  55. data/lib/aspera/proxy_auto_config.js +2 -2
  56. data/lib/aspera/rest.rb +37 -0
  57. data/lib/aspera/secret_hider.rb +6 -1
  58. data/lib/aspera/ssh.rb +1 -1
  59. data/lib/aspera/sync.rb +2 -0
  60. data.tar.gz.sig +0 -0
  61. metadata +3 -4
  62. metadata.gz.sig +0 -0
  63. data/docs/test_env.conf +0 -186
  64. data/lib/aspera/data/7 +0 -0
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:words httpport targetrate minrate bwcap createpath lockpolicy lockminrate
4
+
3
5
  require 'aspera/log'
4
6
  require 'aspera/command_line_builder'
5
7
 
@@ -7,8 +9,8 @@ module Aspera
7
9
  module Fasp
8
10
  # translates a "faspe:" URI (used in Faspex 4) into transfer spec hash
9
11
  class Uri
10
- def initialize(fasplink)
11
- @fasp_uri = URI.parse(fasplink.gsub(' ', '%20'))
12
+ def initialize(fasp_link)
13
+ @fasp_uri = URI.parse(fasp_link.gsub(' ', '%20'))
12
14
  # TODO: check scheme is faspe
13
15
  end
14
16
 
@@ -18,32 +20,32 @@ module Aspera
18
20
  result_ts['remote_user'] = @fasp_uri.user
19
21
  result_ts['ssh_port'] = @fasp_uri.port
20
22
  result_ts['paths'] = [{'source' => URI.decode_www_form_component(@fasp_uri.path)}]
21
- # faspex does not encode trailing base64 encoded tags, fix that
23
+ # faspex does not encode trailing base64 padding, fix that to be able to decode properly
22
24
  fixed_query = @fasp_uri.query.gsub(/(=+)$/){|x|'%3D' * x.length}
23
25
 
24
26
  URI.decode_www_form(fixed_query).each do |i|
25
27
  name = i[0]
26
28
  value = i[1]
27
29
  case name
28
- when 'cookie' then result_ts['cookie'] = value
29
- when 'token' then result_ts['token'] = value
30
- when 'sshfp' then result_ts['sshfp'] = value
31
- when 'policy' then result_ts['rate_policy'] = value
32
- when 'httpport' then result_ts['http_fallback_port'] = value.to_i
33
- when 'targetrate' then result_ts['target_rate_kbps'] = value.to_i
34
- when 'minrate' then result_ts['min_rate_kbps'] = value.to_i
35
- when 'port' then result_ts['fasp_port'] = value.to_i
36
- when 'bwcap' then result_ts['target_rate_cap_kbps'] = value.to_i
37
- when 'enc' then result_ts['cipher'] = value.gsub(/^aes/, 'aes-').gsub(/cfb$/, '-cfb').gsub(/gcm$/, '-gcm').gsub(/--/, '-')
38
- when 'tags64' then result_ts['tags'] = JSON.parse(Base64.strict_decode64(value))
39
- when 'createpath' then result_ts['create_dir'] = CommandLineBuilder.yes_to_true(value)
40
- when 'fallback' then result_ts['http_fallback'] = CommandLineBuilder.yes_to_true(value)
41
- when 'lockpolicy' then result_ts['lock_rate_policy'] = CommandLineBuilder.yes_to_true(value)
30
+ when 'cookie' then result_ts['cookie'] = value
31
+ when 'token' then result_ts['token'] = value
32
+ when 'sshfp' then result_ts['sshfp'] = value
33
+ when 'policy' then result_ts['rate_policy'] = value
34
+ when 'httpport' then result_ts['http_fallback_port'] = value.to_i
35
+ when 'targetrate' then result_ts['target_rate_kbps'] = value.to_i
36
+ when 'minrate' then result_ts['min_rate_kbps'] = value.to_i
37
+ when 'port' then result_ts['fasp_port'] = value.to_i
38
+ when 'bwcap' then result_ts['target_rate_cap_kbps'] = value.to_i
39
+ when 'enc' then result_ts['cipher'] = value.gsub(/^aes/, 'aes-').gsub(/cfb$/, '-cfb').gsub(/gcm$/, '-gcm').gsub(/--/, '-')
40
+ when 'tags64' then result_ts['tags'] = JSON.parse(Base64.strict_decode64(value))
41
+ when 'createpath' then result_ts['create_dir'] = CommandLineBuilder.yes_to_true(value)
42
+ when 'fallback' then result_ts['http_fallback'] = CommandLineBuilder.yes_to_true(value)
43
+ when 'lockpolicy' then result_ts['lock_rate_policy'] = CommandLineBuilder.yes_to_true(value)
42
44
  when 'lockminrate' then result_ts['lock_min_rate'] = CommandLineBuilder.yes_to_true(value)
43
- when 'auth' then Log.log.debug{"ignoring auth #{name}=#{value}"} # TODO: translate into transfer spec ? yes/no
44
- when 'v' then Log.log.debug{"ignoring v #{name}=#{value}"} # TODO: translate into transfer spec ? 2
45
- when 'protect' then Log.log.debug{"ignoring protect #{name}=#{value}"} # TODO: translate into transfer spec ?
46
- else Log.log.warn{"URI parameter ignored: #{name} = #{value}"}
45
+ when 'auth' then Log.log.debug{"ignoring #{name}=#{value}"} # Not used (yes/no)
46
+ when 'v' then Log.log.debug{"ignoring #{name}=#{value}"} # rubocop:disable Lint/DuplicateBranch Not used (2)
47
+ when 'protect' then Log.log.debug{"ignoring #{name}=#{value}"} # rubocop:disable Lint/DuplicateBranch TODO: what is this ?
48
+ else Log.log.warn{"URI parameter ignored: #{name} = #{value}"}
47
49
  end
48
50
  end
49
51
  return result_ts
@@ -13,7 +13,7 @@ module Aspera
13
13
  def initialize(server, parameters)
14
14
  raise 'parameters must be Hash' unless parameters.is_a?(Hash)
15
15
  @parameters = parameters.symbolize_keys
16
- Log.dump(:postproc_parameters, @parameters)
16
+ Log.dump(:post_proc_parameters, @parameters)
17
17
  raise "unexpected key in parameters config: only: #{ALLOWED_PARAMETERS.join(', ')}" if @parameters.keys.any?{|k|!ALLOWED_PARAMETERS.include?(k)}
18
18
  @parameters[:script_folder] ||= '.'
19
19
  @parameters[:fail_on_error] ||= false
@@ -2,11 +2,21 @@
2
2
 
3
3
  class ::Hash
4
4
  def deep_merge(second)
5
- merge(second){|_key, v1, v2|Hash === v1 && Hash === v2 ? v1.deep_merge(v2) : v2}
5
+ merge(second){|_key, v1, v2|v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge(v2) : v2}
6
6
  end
7
7
 
8
8
  def deep_merge!(second)
9
- merge!(second){|_key, v1, v2|Hash === v1 && Hash === v2 ? v1.deep_merge!(v2) : v2}
9
+ merge!(second){|_key, v1, v2|v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
10
+ end
11
+
12
+ def deep_do(memory=nil, &block)
13
+ each do |key, value|
14
+ if value.is_a?(Hash)
15
+ value.deep_do(memory, &block)
16
+ else
17
+ yield(self, key, value, memory)
18
+ end
19
+ end
10
20
  end
11
21
  end
12
22
 
@@ -3,11 +3,11 @@
3
3
  # https://github.com/fastlane-community/security
4
4
  require 'aspera/cli/info'
5
5
 
6
- # enhance the gem to support other keychains
6
+ # enhance the gem to support other key chains
7
7
  module Aspera
8
8
  module Keychain
9
9
  module MacosSecurity
10
- # keychain based on macOS keychain, using `security` cmmand line
10
+ # keychain based on macOS keychain, using `security` command line
11
11
  class Keychain
12
12
  DOMAINS = %i[user system common dynamic].freeze
13
13
  LIST_OPTIONS = {
@@ -32,12 +32,12 @@ module Aspera
32
32
  getpass: :g
33
33
  }.freeze
34
34
  class << self
35
- def execute(command, options=nil, supported=nil, lastopt=nil)
35
+ def execute(command, options=nil, supported=nil, last_opt=nil)
36
36
  url = options&.delete(:url)
37
37
  if !url.nil?
38
38
  uri = URI.parse(url)
39
39
  raise 'only https' unless uri.scheme.eql?('https')
40
- options[:protocol] = 'htps'
40
+ options[:protocol] = 'htps' # cspell: disable-line
41
41
  raise 'host required in URL' if uri.host.nil?
42
42
  options[:server] = uri.host
43
43
  options[:path] = uri.path unless ['', '/'].include?(uri.path)
@@ -50,28 +50,28 @@ module Aspera
50
50
  cmd.push("-#{supported[k]}")
51
51
  cmd.push(v.shellescape) unless v.empty?
52
52
  end
53
- cmd.push(lastopt) unless lastopt.nil?
53
+ cmd.push(last_opt) unless last_opt.nil?
54
54
  Log.log.debug{"executing>>#{cmd.join(' ')}"}
55
55
  result = %x(#{cmd.join(' ')} 2>&1)
56
56
  Log.log.debug{"result>>[#{result}]"}
57
57
  return result
58
58
  end
59
59
 
60
- def keychains(output)
60
+ def key_chains(output)
61
61
  output.split("\n").collect { |line| new(line.strip.gsub(/^"|"$/, '')) }
62
62
  end
63
63
 
64
64
  def default
65
- keychains(execute('default-keychain')).first
65
+ key_chains(execute('default-keychain')).first
66
66
  end
67
67
 
68
68
  def login
69
- keychains(execute('login-keychain')).first
69
+ key_chains(execute('login-keychain')).first
70
70
  end
71
71
 
72
72
  def list(options={})
73
73
  raise ArgumentError, "Invalid domain #{options[:domain]}, expected one of: #{DOMAINS}" unless options[:domain].nil? || DOMAINS.include?(options[:domain])
74
- keychains(execute('list-keychains', options, LIST_OPTIONS))
74
+ key_chains(execute('list-key_chains', options, LIST_OPTIONS))
75
75
  end
76
76
 
77
77
  def by_name(name)
@@ -88,14 +88,14 @@ module Aspera
88
88
  [string].pack('H*').force_encoding('UTF-8')
89
89
  end
90
90
 
91
- def password(operation, passtype, options)
91
+ def password(operation, pass_type, options)
92
92
  raise "wrong operation: #{operation}" unless %i[add find delete].include?(operation)
93
- raise "wrong passtype: #{passtype}" unless %i[generic internet].include?(passtype)
93
+ raise "wrong pass_type: #{pass_type}" unless %i[generic internet].include?(pass_type)
94
94
  raise 'options shall be Hash' unless options.is_a?(Hash)
95
95
  missing = (operation.eql?(:add) ? %i[account service password] : %i[label]) - options.keys
96
96
  raise "missing options: #{missing}" unless missing.empty?
97
97
  options[:getpass] = '' if operation.eql?(:find)
98
- output = self.class.execute("#{operation}-#{passtype}-password", options, ADD_PASS_OPTIONS, @path)
98
+ output = self.class.execute("#{operation}-#{pass_type}-password", options, ADD_PASS_OPTIONS, @path)
99
99
  raise output.gsub(/^.*: /, '') if output.start_with?('security: ')
100
100
  return nil unless operation.eql?(:find)
101
101
  attributes = {}
@@ -143,7 +143,7 @@ module Aspera
143
143
  raise 'not found' if info.nil?
144
144
  result = options.clone
145
145
  result[:secret] = info['password']
146
- result[:description] = info['icmt']
146
+ result[:description] = info['icmt'] # cspell: disable-line
147
147
  return result
148
148
  end
149
149
 
data/lib/aspera/log.rb CHANGED
@@ -39,6 +39,7 @@ module Aspera
39
39
  end
40
40
  end
41
41
 
42
+ # Capture the output of $stderr and log it at debug level
42
43
  def capture_stderr
43
44
  real_stderr = $stderr
44
45
  $stderr = StringIO.new
data/lib/aspera/node.rb CHANGED
@@ -17,6 +17,7 @@ module Aspera
17
17
  MATCH_EXEC_PREFIX = 'exec:'
18
18
  HEADER_X_ASPERA_ACCESS_KEY = 'X-Aspera-AccessKey'
19
19
  PATH_SEPARATOR = '/'
20
+ TS_FIELDS_TO_COPY = %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].freeze
20
21
 
21
22
  # register node special token decoder
22
23
  Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
@@ -84,15 +85,16 @@ module Aspera
84
85
  return nil
85
86
  end
86
87
 
87
- # recursively browse in a folder (with non-recursive method)
88
+ # Recursively browse in a folder (with non-recursive method)
88
89
  # sub folders are processed if the processing method returns true
89
90
  # @param state [Object] state object sent to processing method
90
- # @param method [Symbol] processing method name
91
91
  # @param top_file_id [String] file id to start at (default = access key root file id)
92
92
  # @param top_file_path [String] path of top folder (default = /)
93
- def process_folder_tree(state:, method:, top_file_id:, top_file_path: '/')
93
+ # @param block [Proc] processing method, args: entry, path, state
94
+ def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
94
95
  raise 'INTERNAL ERROR: top_file_path not set' if top_file_path.nil?
95
- raise "INTERNAL ERROR: Missing method #{method}" unless respond_to?(method)
96
+ raise 'INTERNAL ERROR: Missing block' unless block
97
+ # start at top folder
96
98
  folders_to_explore = [{id: top_file_id, path: top_file_path}]
97
99
  Log.dump(:folders_to_explore, folders_to_explore)
98
100
  until folders_to_explore.empty?
@@ -109,9 +111,9 @@ module Aspera
109
111
  Log.dump(:folder_contents, folder_contents)
110
112
  folder_contents.each do |entry|
111
113
  relative_path = File.join(current_item[:path], entry['name'])
112
- Log.log.debug{"looking #{relative_path}".bg_green}
114
+ Log.log.debug{"process_folder_tree checking #{relative_path}"}
113
115
  # continue only if method returns true
114
- next unless send(method, entry, relative_path, state)
116
+ next unless yield(entry, relative_path, state)
115
117
  # entry type is file, folder or link
116
118
  case entry['type']
117
119
  when 'folder'
@@ -119,85 +121,74 @@ module Aspera
119
121
  when 'link'
120
122
  node_id_to_node(entry['target_node_id'])&.process_folder_tree(
121
123
  state: state,
122
- method: method,
123
124
  top_file_id: entry['target_id'],
124
- top_file_path: relative_path)
125
+ top_file_path: relative_path,
126
+ &block)
125
127
  end
126
128
  end
127
129
  end
128
130
  end # process_folder_tree
129
131
 
130
- # processing method to resolve a file path to id
131
- # @returns true if processing need to continue
132
- def process_resolve_node_path(entry, _path, state)
133
- # stop digging here if not in right path
134
- return false unless entry['name'].eql?(state[:path].first)
135
- # ok it matches, so we remove the match
136
- state[:path].shift
137
- case entry['type']
138
- when 'file'
139
- # file must be terminal
140
- raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
141
- # it's terminal, we found it
142
- state[:result] = {api: self, file_id: entry['id']}
143
- return false
144
- when 'folder'
145
- if state[:path].empty?
146
- # we found it
147
- state[:result] = {api: self, file_id: entry['id']}
148
- return false
149
- end
150
- when 'link'
151
- if state[:path].empty?
152
- # we found it
153
- other_node = node_id_to_node(entry['target_node_id'])
154
- raise 'cannot resolve link' if other_node.nil?
155
- state[:result] = {api: other_node, file_id: entry['target_id']}
156
- return false
157
- end
158
- else
159
- Log.log.warn{"Unknown element type: #{entry['type']}"}
160
- end
161
- # continue to dig folder
162
- return true
163
- end
164
-
165
132
  # Navigate the path from given file id
166
133
  # @param top_file_id [String] id initial file id
167
134
  # @param path [String] file path
168
135
  # @return [Hash] {.api,.file_id}
169
136
  def resolve_api_fid(top_file_id, path)
170
137
  raise 'file id shall be String' unless top_file_id.is_a?(String)
138
+ process_last_link = path.end_with?(PATH_SEPARATOR)
171
139
  path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
172
140
  return {api: self, file_id: top_file_id} if path_elements.empty?
173
141
  resolve_state = {path: path_elements, result: nil}
174
- process_folder_tree(state: resolve_state, method: :process_resolve_node_path, top_file_id: top_file_id)
175
- raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
176
- return resolve_state[:result]
177
- end
178
-
179
- # add entry to list if test block is success
180
- # @return [TrueClass,FalseClass]
181
- def process_find_files(entry, path, state)
182
- begin
183
- # add to result if match filter
184
- state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
185
- # process link
186
- if entry[:type].eql?('link')
187
- other_node = node_id_to_node(entry['target_node_id'])
188
- other_node.process_folder_tree(state: state, method: process_find_files, top_file_id: entry['target_id'], top_file_path: path)
142
+ process_folder_tree(state: resolve_state, top_file_id: top_file_id) do |entry, _path, state|
143
+ # this block is called recursively for each entry in folder
144
+ # stop digging here if not in right path
145
+ next false unless entry['name'].eql?(state[:path].first)
146
+ # ok it matches, so we remove the match
147
+ state[:path].shift
148
+ case entry['type']
149
+ when 'file'
150
+ # file must be terminal
151
+ raise "#{entry['name']} is a file, expecting folder to find: #{state[:path]}" unless state[:path].empty?
152
+ # it's terminal, we found it
153
+ state[:result] = {api: self, file_id: entry['id']}
154
+ next false
155
+ when 'folder'
156
+ if state[:path].empty?
157
+ # we found it
158
+ state[:result] = {api: self, file_id: entry['id']}
159
+ next false
160
+ end
161
+ when 'link'
162
+ if state[:path].empty?
163
+ if process_last_link
164
+ # we found it
165
+ other_node = node_id_to_node(entry['target_node_id'])
166
+ raise 'cannot resolve link' if other_node.nil?
167
+ state[:result] = {api: other_node, file_id: entry['target_id']}
168
+ else
169
+ # we found it but we do not process the link
170
+ state[:result] = {api: self, file_id: entry['id']}
171
+ end
172
+ next false
173
+ end
174
+ else
175
+ Log.log.warn{"Unknown element type: #{entry['type']}"}
189
176
  end
190
- rescue StandardError => e
191
- Log.log.error{"#{path}: #{e.message}"}
177
+ # continue to dig folder
178
+ next true
192
179
  end
193
- # process all folders
194
- return true
180
+ raise "entry not found: #{resolve_state[:path]}" if resolve_state[:result].nil?
181
+ return resolve_state[:result]
195
182
  end
196
183
 
197
184
  def find_files(top_file_id, test_block)
198
185
  Log.log.debug{"find_files: file id=#{top_file_id}"}
199
186
  find_state = {found: [], test_block: test_block}
200
- process_folder_tree(state: find_state, method: :process_find_files, top_file_id: top_file_id)
187
+ process_folder_tree(state: find_state, top_file_id: top_file_id) do |entry, path, state|
188
+ state[:found].push(entry.merge({'path' => path})) if state[:test_block].call(entry)
189
+ # test all files deeply
190
+ true
191
+ end
201
192
  return find_state[:found]
202
193
  end
203
194
 
@@ -212,6 +203,8 @@ module Aspera
212
203
  case params[:auth][:type]
213
204
  when :basic
214
205
  ak_name = params[:auth][:username]
206
+ raise 'ERROR: no secret in node object' unless params[:auth][:password]
207
+ ak_token = Rest.basic_creds(params[:auth][:username], params[:auth][:password])
215
208
  when :oauth2
216
209
  ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
217
210
  # TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
@@ -235,12 +228,7 @@ module Aspera
235
228
  add_tspec_info(transfer_spec)
236
229
  transfer_spec.deep_merge!(ts_merge) unless ts_merge.nil?
237
230
  # add application specific tags (AoC)
238
- the_app = app_info
239
- the_app[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: the_app) unless the_app.nil?
240
- # add basic token
241
- if transfer_spec['token'].nil?
242
- ts_basic_token(transfer_spec)
243
- end
231
+ app_info[:api].add_ts_tags(transfer_spec: transfer_spec, app_info: app_info) unless app_info.nil?
244
232
  # add remote host info
245
233
  if self.class.use_standard_ports
246
234
  # get default TCP/UDP ports and transfer user
@@ -251,23 +239,17 @@ module Aspera
251
239
  transfer_spec['remote_host'] = @app_info[:node_info]['transfer_url']
252
240
  end
253
241
  else
254
- # retrieve values from API
255
- std_t_spec = create(
242
+ # retrieve values from API (and keep a copy/cache)
243
+ @std_t_spec_cache ||= create(
256
244
  'files/download_setup',
257
245
  {transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
258
246
  )[:data]['transfer_specs'].first['transfer_spec']
259
247
  # copy some parts
260
- %w[remote_host remote_user ssh_port fasp_port wss_enabled wss_port].each {|i| transfer_spec[i] = std_t_spec[i] if std_t_spec.key?(i)}
248
+ TS_FIELDS_TO_COPY.each {|i| transfer_spec[i] = @std_t_spec_cache[i] if @std_t_spec_cache.key?(i)}
261
249
  end
250
+ Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{transfer_spec['remote_user']}"} \
251
+ unless transfer_spec['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
262
252
  return transfer_spec
263
253
  end
264
-
265
- # set basic token in transfer spec
266
- def ts_basic_token(ts)
267
- Log.log.warn{"Expected transfer user: #{Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER}, but have #{ts['remote_user']}"} \
268
- unless ts['remote_user'].eql?(Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER)
269
- raise 'ERROR: no secret in node object' unless params[:auth][:password]
270
- ts['token'] = Rest.basic_creds(params[:auth][:username], params[:auth][:password])
271
- end
272
254
  end
273
255
  end
data/lib/aspera/oauth.rb CHANGED
@@ -192,7 +192,7 @@ module Aspera
192
192
  # replace default values
193
193
  @generic_parameters = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
194
194
  # legacy
195
- @generic_parameters[:grant_method] ||= @generic_parameters.delete(:crtype) if @generic_parameters.key?(:crtype)
195
+ @generic_parameters[:grant_method] ||= @generic_parameters.delete(:crtype) if @generic_parameters.key?(:crtype) # cspell: disable-line
196
196
  # check that type is known
197
197
  self.class.token_creator(@generic_parameters[:grant_method])
198
198
  # specific parameters for the creation type
@@ -8,7 +8,7 @@ module Aspera
8
8
  class PersistencyActionOnce
9
9
  # @param :manager Mandatory Database
10
10
  # @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
11
- # @param :id Mandatory identifiers
11
+ # @param :id Mandatory identifiers
12
12
  # @param :delete Optional delete persistency condition
13
13
  # @param :parse Optional parse method (default to JSON)
14
14
  # @param :format Optional dump method (default to JSON)
@@ -1,34 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:words Magick MAGICKCORE ITERM mintty winsize termcap
4
+
3
5
  require 'rmagick' # https://rmagick.github.io/index.html
4
6
  require 'rainbow'
5
7
  require 'io/console'
6
8
  module Aspera
7
9
  module Preview
8
- # function conversion_type returns one of the types: CONVERSION_TYPES
10
+ # Generates a string that can display an image in a terminal
9
11
  class Terminal
12
+ # quantum depth is 8 or 16: convert xc: -format "%q" info:
13
+ # Rainbow only supports 8-bit colors
14
+ SHIFT_FOR_8_BIT = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
15
+ ITERM_NAMES = %w[iTerm WezTerm mintty].freeze
16
+ TERM_ENV_VARS = %w[TERM_PROGRAM LC_TERMINAL].freeze
17
+ private_constant :SHIFT_FOR_8_BIT, :ITERM_NAMES, :TERM_ENV_VARS
10
18
  class << self
11
- def build(blob, reserved_lines: 0)
12
- # TODO: retrieve terminal ratio using
13
- font_ratio = 1.7
19
+ def build(blob, reserved_lines: 0, double_precision: true)
20
+ return iterm_display_image(blob) if iterm_supported?
21
+ image = Magick::ImageList.new.from_blob(blob)
14
22
  (term_rows, term_columns) = IO.console.winsize
15
23
  term_rows -= reserved_lines
16
- image = Magick::ImageList.new.from_blob(blob)
17
- chosen_factor = [term_rows / image.rows.to_f, term_columns / image.columns.to_f].min
18
- image = image.scale((image.columns * chosen_factor * font_ratio).to_i, (image.rows * chosen_factor).to_i)
19
- text_pixels = []
24
+ # compute scaling to fit terminal
25
+ fit_term_ratio = [term_rows / image.rows.to_f, term_columns / image.columns.to_f].min
26
+ # TODO: retrieve terminal font ratio using some termcap ?
27
+ font_ratio = 1.7
28
+ height_ratio = double_precision ? 2.0 : 1.0
29
+ image = image.scale((image.columns * fit_term_ratio * font_ratio).to_i, (image.rows * fit_term_ratio * height_ratio).to_i)
30
+ # get all pixel colors, adjusted for Rainbow
31
+ pixel_colors = []
20
32
  image.each_pixel do |pixel, col, row|
21
- text_pixels.push("\n") if col.eql?(0) && !row.eql?(0)
22
- pixel_rgb = [pixel.red, pixel.green, pixel.blue].map do |color|
23
- # quantum depth is 8 or 16: convert xc: -format "%q" info:
24
- # Rainbow only supports 8-bit colors
25
- color >> (Magick::MAGICKCORE_QUANTUM_DEPTH - 8)
33
+ pixel_rgb = [pixel.red, pixel.green, pixel.blue]
34
+ pixel_rgb = pixel_rgb.map { |color| color >> SHIFT_FOR_8_BIT } unless SHIFT_FOR_8_BIT.eql?(0)
35
+ # init 2-dim array
36
+ pixel_colors[row] ||= []
37
+ pixel_colors[row][col] = pixel_rgb
38
+ end
39
+ # now generate text
40
+ text_pixels = []
41
+ pixel_colors.each_with_index do |row_data, row|
42
+ next if double_precision && row.odd?
43
+ row_data.each_with_index do |pixel_rgb, col|
44
+ text_pixels.push("\n") if col.eql?(0) && !row.eql?(0)
45
+ if double_precision
46
+ text_pixels.push(Rainbow('▄').background(pixel_rgb).foreground(pixel_colors[row + 1][col]))
47
+ else
48
+ text_pixels.push(Rainbow(' ').background(pixel_rgb))
49
+ end
26
50
  end
27
- text_pixels.push(Rainbow(' ').background(pixel_rgb))
28
51
  end
29
52
  return text_pixels.join
30
53
  end
31
- end # class << self
54
+
55
+ # display image in iTerm2
56
+ def iterm_display_image(blob)
57
+ # image = Magick::ImageList.new.from_blob(blob)
58
+ arguments = {
59
+ inline: 1,
60
+ preserveAspectRatio: 1,
61
+ size: blob.length
62
+ # width: image.columns,
63
+ # height: image.rows
64
+ }.map { |k, v| "#{k}=#{v}" }.join(';')
65
+ # \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
66
+ # https://iterm2.com/documentation-images.html
67
+ return "\e]1337;File=#{arguments}:#{Base64.encode64(blob)}\a"
68
+ end
69
+
70
+ # @return [Boolean] true if the terminal supports iTerm2 image display
71
+ def iterm_supported?
72
+ TERM_ENV_VARS.each do |env_var|
73
+ return true if ITERM_NAMES.any? { |term| ENV[env_var]&.include?(term) }
74
+ end
75
+ false
76
+ end
77
+ end # class << self
32
78
  end # class Terminal
33
79
  end # module Preview
34
80
  end # module Aspera
@@ -19,7 +19,7 @@ module Aspera
19
19
  private_constant :BASH_SPECIAL_CHARACTERS, :BASH_EXIT_NOT_FOUND, :EXTERNAL_TOOLS, :TEMP_FORMAT
20
20
 
21
21
  class << self
22
- # returns string with single quotes suitable for bash if there is any bash metacharacter
22
+ # returns string with single quotes suitable for bash if there is any bash meta-character
23
23
  def shell_quote(argument)
24
24
  return argument unless argument.chars.any?{|c|BASH_SPECIAL_CHARACTERS.include?(c)}
25
25
  return "'" + argument.gsub(/'/){|_s| "'\"'\"'"} + "'"
@@ -55,7 +55,7 @@ module Aspera
55
55
  raise "Error: #{command_symb} is not in the PATH"
56
56
  end
57
57
  unless exit_status.success?
58
- Log.log.error{"commandline: #{command}"}
58
+ Log.log.error{"command line: #{command}"}
59
59
  Log.log.error{"Error code: #{exit_status}"}
60
60
  Log.log.error{"stdout: #{stdout}"}
61
61
  Log.log.error{"stderr: #{stderr}"}
@@ -82,7 +82,7 @@ module Aspera
82
82
  result = external_command(:ffprobe, [
83
83
  '-loglevel', 'error',
84
84
  '-show_entries', 'format=duration',
85
- '-print_format', 'default=noprint_wrappers=1:nokey=1',
85
+ '-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
86
86
  input_file])
87
87
  return result[:stdout].to_f
88
88
  end
@@ -70,7 +70,7 @@ function shExpMatch(str, shell_expr) {
70
70
  }
71
71
  function weekdayRange(wd1, wd2, gmt) {
72
72
  var today = new Date();
73
- var days = 'SUNMONTUEWEDTHUFRISAT';
73
+ var days = 'SUNMONTUEWEDTHUFRISAT'; // cspell: disable-line
74
74
  wd1 = wd1.toUpperCase();
75
75
  if (wd2 == undefined)
76
76
  wd2 = wd1;
@@ -138,7 +138,7 @@ function dateRange() {
138
138
  else
139
139
  return false;
140
140
  } else if (typeof arg == 'string') {
141
- var months = 'JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC';
141
+ var months = 'JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC'; // cspell: disable-line
142
142
  arg = arg.toUpperCase();
143
143
  arg = months.indexOf(arg);
144
144
  if (arg == -1)
data/lib/aspera/rest.rb CHANGED
@@ -38,6 +38,11 @@ module Aspera
38
38
 
39
39
  ARRAY_PARAMS = '[]'
40
40
 
41
+ private_constant :ARRAY_PARAMS
42
+
43
+ # error message when entity not found
44
+ ENTITY_NOT_FOUND = 'No such'
45
+
41
46
  class << self
42
47
  # define accessors
43
48
  @@global.each_key do |p|
@@ -50,6 +55,12 @@ module Aspera
50
55
 
51
56
  def basic_creds(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
52
57
 
58
+ # used to build a parameter list prefixed with "[]"
59
+ # @param values [Array] list of values
60
+ def array_params(values)
61
+ return [ARRAY_PARAMS].concat(values)
62
+ end
63
+
53
64
  # build URI from URL and parameters and check it is http or https
54
65
  def build_uri(url, params=nil)
55
66
  uri = URI.parse(url)
@@ -337,5 +348,31 @@ module Aspera
337
348
  def cancel(subpath)
338
349
  return call({operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'}})
339
350
  end
351
+
352
+ # Query by name and returns a single result, else it throws an exception (no or multiple results)
353
+ # @param subpath path of entity in API
354
+ # @param search_name name of searched entity
355
+ # @param options additional search options
356
+ def lookup_by_name(subpath, search_name, options={})
357
+ # returns entities whose name contains value (case insensitive)
358
+ matching_items = read(subpath, options.merge({'q' => CGI.escape(search_name)}))[:data]
359
+ # API style: {totalcount:, ...} cspell: disable-line
360
+ # TODO: not generic enough ? move somewhere ? inheritance ?
361
+ matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
362
+ raise "Internal error: expecting array, have #{matching_items.class}" unless matching_items.is_a?(Array)
363
+ case matching_items.length
364
+ when 1 then return matching_items.first
365
+ when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{subpath}: "#{search_name}"}
366
+ else
367
+ # multiple case insensitive partial matches, try case insensitive full match
368
+ # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
369
+ name_matches = matching_items.select{|i|i['name'].casecmp?(search_name)}
370
+ case name_matches.length
371
+ when 1 then return name_matches.first
372
+ when 0 then raise %Q(#{subpath}: multiple case insensitive partial match for: "#{search_name}": #{matching_items.map{|i|i['name']}} but no case insensitive full match. Please be more specific or give exact name.) # rubocop:disable Layout/LineLength
373
+ else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{|i|i['name']}}"
374
+ end
375
+ end
376
+ end
340
377
  end
341
378
  end # module Aspera