aspera-cli 4.13.0 → 4.14.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 +28 -5
- data/CONTRIBUTING.md +17 -1
- data/README.md +782 -401
- data/examples/dascli +1 -1
- data/examples/rubyc +24 -0
- data/lib/aspera/aoc.rb +21 -32
- data/lib/aspera/ascmd.rb +1 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
- data/lib/aspera/cli/formatter.rb +17 -25
- data/lib/aspera/cli/main.rb +21 -27
- data/lib/aspera/cli/manager.rb +128 -114
- data/lib/aspera/cli/plugin.rb +87 -38
- data/lib/aspera/cli/plugins/alee.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +216 -102
- data/lib/aspera/cli/plugins/ats.rb +16 -18
- data/lib/aspera/cli/plugins/bss.rb +3 -3
- data/lib/aspera/cli/plugins/config.rb +177 -367
- data/lib/aspera/cli/plugins/console.rb +4 -6
- data/lib/aspera/cli/plugins/cos.rb +12 -13
- data/lib/aspera/cli/plugins/faspex.rb +17 -18
- data/lib/aspera/cli/plugins/faspex5.rb +332 -216
- data/lib/aspera/cli/plugins/node.rb +171 -142
- data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
- data/lib/aspera/cli/plugins/preview.rb +38 -60
- data/lib/aspera/cli/plugins/server.rb +22 -15
- data/lib/aspera/cli/plugins/shares.rb +24 -33
- data/lib/aspera/cli/plugins/sync.rb +3 -3
- data/lib/aspera/cli/transfer_agent.rb +29 -26
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +9 -7
- data/lib/aspera/data/6 +0 -0
- data/lib/aspera/environment.rb +7 -3
- data/lib/aspera/fasp/agent_connect.rb +5 -0
- data/lib/aspera/fasp/agent_direct.rb +5 -5
- data/lib/aspera/fasp/agent_httpgw.rb +138 -60
- data/lib/aspera/fasp/agent_trsdk.rb +2 -0
- data/lib/aspera/fasp/error_info.rb +2 -0
- data/lib/aspera/fasp/installation.rb +18 -19
- data/lib/aspera/fasp/parameters.rb +18 -17
- data/lib/aspera/fasp/parameters.yaml +2 -1
- data/lib/aspera/fasp/resume_policy.rb +3 -3
- data/lib/aspera/fasp/transfer_spec.rb +6 -5
- data/lib/aspera/fasp/uri.rb +23 -21
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/hash_ext.rb +12 -2
- data/lib/aspera/keychain/macos_security.rb +13 -13
- data/lib/aspera/log.rb +1 -0
- data/lib/aspera/node.rb +62 -80
- data/lib/aspera/oauth.rb +1 -1
- data/lib/aspera/persistency_action_once.rb +1 -1
- data/lib/aspera/preview/terminal.rb +61 -15
- data/lib/aspera/preview/utils.rb +3 -3
- data/lib/aspera/proxy_auto_config.js +2 -2
- data/lib/aspera/rest.rb +37 -0
- data/lib/aspera/secret_hider.rb +6 -1
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/sync.rb +2 -0
- data.tar.gz.sig +0 -0
- metadata +3 -4
- metadata.gz.sig +0 -0
- data/docs/test_env.conf +0 -186
- data/lib/aspera/data/7 +0 -0
data/lib/aspera/fasp/uri.rb
CHANGED
@@ -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(
|
11
|
-
@fasp_uri = URI.parse(
|
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
|
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'
|
29
|
-
when 'token'
|
30
|
-
when 'sshfp'
|
31
|
-
when 'policy'
|
32
|
-
when 'httpport'
|
33
|
-
when 'targetrate'
|
34
|
-
when 'minrate'
|
35
|
-
when 'port'
|
36
|
-
when 'bwcap'
|
37
|
-
when 'enc'
|
38
|
-
when 'tags64'
|
39
|
-
when 'createpath'
|
40
|
-
when 'fallback'
|
41
|
-
when 'lockpolicy'
|
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'
|
44
|
-
when 'v'
|
45
|
-
when 'protect'
|
46
|
-
else
|
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(:
|
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
|
data/lib/aspera/hash_ext.rb
CHANGED
@@ -2,11 +2,21 @@
|
|
2
2
|
|
3
3
|
class ::Hash
|
4
4
|
def deep_merge(second)
|
5
|
-
merge(second){|_key, v1, v2|Hash
|
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
|
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
|
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`
|
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,
|
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(
|
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
|
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
|
-
|
65
|
+
key_chains(execute('default-keychain')).first
|
66
66
|
end
|
67
67
|
|
68
68
|
def login
|
69
|
-
|
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
|
-
|
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,
|
91
|
+
def password(operation, pass_type, options)
|
92
92
|
raise "wrong operation: #{operation}" unless %i[add find delete].include?(operation)
|
93
|
-
raise "wrong
|
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}-#{
|
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
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
|
-
#
|
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
|
-
|
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
|
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{"
|
114
|
+
Log.log.debug{"process_folder_tree checking #{relative_path}"}
|
113
115
|
# continue only if method returns true
|
114
|
-
next unless
|
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,
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
191
|
-
|
177
|
+
# continue to dig folder
|
178
|
+
next true
|
192
179
|
end
|
193
|
-
#
|
194
|
-
return
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
#
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
pixel_rgb =
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
data/lib/aspera/preview/utils.rb
CHANGED
@@ -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
|
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{"
|
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
|