aspera-cli 4.25.6 → 4.26.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 +74 -47
- data/CONTRIBUTING.md +1 -1
- data/lib/aspera/api/aoc.rb +118 -78
- data/lib/aspera/api/node.rb +101 -49
- data/lib/aspera/ascp/installation.rb +94 -30
- data/lib/aspera/cli/extended_value.rb +1 -0
- data/lib/aspera/cli/formatter.rb +47 -40
- data/lib/aspera/cli/manager.rb +30 -4
- data/lib/aspera/cli/plugins/aoc.rb +214 -136
- data/lib/aspera/cli/plugins/ats.rb +3 -3
- data/lib/aspera/cli/plugins/base.rb +17 -42
- data/lib/aspera/cli/plugins/config.rb +5 -3
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +5 -5
- data/lib/aspera/cli/plugins/faspex5.rb +20 -18
- data/lib/aspera/cli/plugins/node.rb +66 -70
- data/lib/aspera/cli/plugins/oauth.rb +5 -12
- data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
- data/lib/aspera/cli/plugins/preview.rb +116 -80
- data/lib/aspera/cli/plugins/server.rb +2 -10
- data/lib/aspera/cli/plugins/shares.rb +7 -7
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/dot_container.rb +7 -3
- data/lib/aspera/environment.rb +3 -2
- data/lib/aspera/log.rb +1 -1
- data/lib/aspera/preview/file_types.rb +1 -1
- data/lib/aspera/preview/generator.rb +146 -91
- data/lib/aspera/preview/options.rb +4 -1
- data/lib/aspera/preview/terminal.rb +50 -20
- data/lib/aspera/preview/utils.rb +76 -34
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/rest.rb +1 -0
- data/lib/aspera/rest_list.rb +23 -16
- data/lib/aspera/secret_hider.rb +3 -1
- data/lib/aspera/uri_reader.rb +17 -2
- data.tar.gz.sig +0 -0
- metadata +5 -5
- metadata.gz.sig +0 -0
|
@@ -14,6 +14,7 @@ require 'aspera/api/node'
|
|
|
14
14
|
require 'aspera/hash_ext'
|
|
15
15
|
require 'aspera/timer_limiter'
|
|
16
16
|
require 'aspera/id_generator'
|
|
17
|
+
require 'aspera/uri_reader'
|
|
17
18
|
require 'aspera/log'
|
|
18
19
|
require 'aspera/assert'
|
|
19
20
|
require 'securerandom'
|
|
@@ -22,31 +23,30 @@ module Aspera
|
|
|
22
23
|
module Cli
|
|
23
24
|
module Plugins
|
|
24
25
|
class Preview < BasicAuth
|
|
25
|
-
#
|
|
26
|
+
# Reserved transfer tag used to identify preview-generation transfers.
|
|
26
27
|
PREV_GEN_TAG = 'preview_generator'
|
|
27
|
-
#
|
|
28
|
+
# Node API suffix for the per-file preview folder.
|
|
28
29
|
PREVIEW_FOLDER_SUFFIX = '.asp-preview'
|
|
29
|
-
# basename
|
|
30
|
+
# Default basename for generated preview files.
|
|
30
31
|
PREVIEW_BASENAME = 'preview'
|
|
31
|
-
#
|
|
32
|
+
# Prefix for the temporary working directory created under the system temp folder.
|
|
32
33
|
TMP_DIR_PREFIX = 'prev_tmp'
|
|
33
|
-
#
|
|
34
|
+
# Default preview root folder name, aligned with `aspera.conf`.
|
|
34
35
|
DEFAULT_PREVIEWS_FOLDER = 'previews'
|
|
35
|
-
#
|
|
36
|
+
# Marker file recording which access key owns a cached preview area.
|
|
36
37
|
AK_MARKER_FILE = '.aspera_access_key'
|
|
37
|
-
# URL prefix for local storage
|
|
38
|
-
PVCL_LOCAL_STORAGE = 'file:///'
|
|
39
38
|
LOG_LIMITER_SEC = 30.0
|
|
39
|
+
REMOTE_ACCESS = 'aspera:'
|
|
40
40
|
private_constant :PREV_GEN_TAG,
|
|
41
41
|
:PREVIEW_FOLDER_SUFFIX,
|
|
42
42
|
:PREVIEW_BASENAME,
|
|
43
43
|
:TMP_DIR_PREFIX,
|
|
44
44
|
:DEFAULT_PREVIEWS_FOLDER,
|
|
45
|
-
:PVCL_LOCAL_STORAGE,
|
|
46
45
|
:AK_MARKER_FILE,
|
|
47
|
-
:LOG_LIMITER_SEC
|
|
46
|
+
:LOG_LIMITER_SEC,
|
|
47
|
+
:REMOTE_ACCESS
|
|
48
48
|
|
|
49
|
-
attr_accessor :option_skip_types, :option_previews_folder, :option_folder_reset_cache, :option_skip_folders, :option_overwrite
|
|
49
|
+
attr_accessor :option_skip_types, :option_previews_folder, :option_folder_reset_cache, :option_skip_folders, :option_overwrite
|
|
50
50
|
|
|
51
51
|
def initialize(**_)
|
|
52
52
|
super
|
|
@@ -55,17 +55,19 @@ module Aspera
|
|
|
55
55
|
@option_previews_folder = nil
|
|
56
56
|
@option_overwrite = nil
|
|
57
57
|
@option_folder_reset_cache = nil
|
|
58
|
-
#
|
|
58
|
+
# Generator configuration populated from CLI options.
|
|
59
59
|
@gen_options = Aspera::Preview::Options.new
|
|
60
|
-
#
|
|
60
|
+
# Used to rate-limit periodic progress logging and checkpoint persistence.
|
|
61
61
|
@periodic = TimerLimiter.new(LOG_LIMITER_SEC)
|
|
62
|
-
#
|
|
62
|
+
# Optional callback used to filter entries before generation.
|
|
63
63
|
@filter_block = nil
|
|
64
|
-
|
|
64
|
+
@access_remote = true
|
|
65
|
+
# Bind CLI options directly to generator option attributes.
|
|
65
66
|
options.declare(
|
|
66
67
|
:skip_format, 'Skip this preview format',
|
|
67
68
|
allowed: Aspera::Preview::Generator::PREVIEW_FORMATS
|
|
68
69
|
)
|
|
70
|
+
# TODO: use the same option as in `node` plugin
|
|
69
71
|
options.declare(
|
|
70
72
|
:folder_reset_cache, 'Force detection of generated preview by refresh cache',
|
|
71
73
|
allowed: %i[no header read],
|
|
@@ -81,12 +83,12 @@ module Aspera
|
|
|
81
83
|
options.declare(:mimemagic, 'Use Mime type detection of gem mimemagic', allowed: Allowed::TYPES_BOOLEAN, default: false)
|
|
82
84
|
options.declare(:overwrite, 'When to overwrite result file', handler: {o: self, m: :option_overwrite}, allowed: %i[always never mtime], default: :mtime)
|
|
83
85
|
options.declare(
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
default:
|
|
86
|
+
:root_url,
|
|
87
|
+
"How to read and write files on storage (<empty>, #{REMOTE_ACCESS}, or #{UriReader.file_url('<folder>')})",
|
|
88
|
+
allowed: Allowed::TYPES_STRING,
|
|
89
|
+
default: ''
|
|
88
90
|
)
|
|
89
|
-
#
|
|
91
|
+
# Declare generator-specific options and apply their default values.
|
|
90
92
|
Aspera::Preview::Options::DESCRIPTIONS.each do |opt|
|
|
91
93
|
values = if opt.key?(:values)
|
|
92
94
|
opt[:values]
|
|
@@ -97,7 +99,8 @@ module Aspera
|
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
options.parse_options!
|
|
100
|
-
|
|
102
|
+
Api::Node.api_options[:cache] = !@option_folder_reset_cache.eql?(:header)
|
|
103
|
+
# Start from the full supported format list, then remove any skipped format.
|
|
101
104
|
@preview_formats_to_generate = Aspera::Preview::Generator::PREVIEW_FORMATS.clone
|
|
102
105
|
skip = options.get_option(:skip_format)
|
|
103
106
|
@preview_formats_to_generate.delete(skip) if skip
|
|
@@ -106,22 +109,16 @@ module Aspera
|
|
|
106
109
|
Log.log.debug{"tmpdir: #{@tmp_folder}"}
|
|
107
110
|
end
|
|
108
111
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
headers['X-Aspera-Cache-Control'] = 'no-cache' if @option_folder_reset_cache.eql?(:header)
|
|
114
|
-
return @api_node.read("files/#{file_id}/files", request_args, headers: headers)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# old version based on folders
|
|
118
|
-
# @param iteration_persistency can be nil
|
|
112
|
+
# Process legacy transfer events and trigger preview generation for completed downloads.
|
|
113
|
+
#
|
|
114
|
+
# @param iteration_persistency [PersistencyActionOnce, nil] stores the last processed event id
|
|
115
|
+
# @return [void]
|
|
119
116
|
def process_trevents(iteration_persistency)
|
|
120
117
|
events_filter = {
|
|
121
118
|
'access_key' => @access_key_self['id'],
|
|
122
119
|
'type' => 'download.ended'
|
|
123
120
|
}
|
|
124
|
-
#
|
|
121
|
+
# Resume from the last persisted event id when available.
|
|
125
122
|
events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
|
|
126
123
|
begin
|
|
127
124
|
events = @api_node.read('events', events_filter)
|
|
@@ -146,10 +143,10 @@ module Aspera
|
|
|
146
143
|
scan_folder_files(folder_entry) unless folder_entry.nil?
|
|
147
144
|
end
|
|
148
145
|
end
|
|
149
|
-
# log
|
|
146
|
+
# Periodically log progress and persist the latest processed event.
|
|
150
147
|
next unless @periodic.trigger? || event.equal?(events.last)
|
|
151
148
|
Log.log.debug{"Processed event #{event['id']}"}
|
|
152
|
-
#
|
|
149
|
+
# Save a checkpoint to avoid replaying the full batch after a failure.
|
|
153
150
|
if !iteration_persistency.nil?
|
|
154
151
|
iteration_persistency.data[0] = event['id'].to_s
|
|
155
152
|
iteration_persistency.save
|
|
@@ -157,19 +154,22 @@ module Aspera
|
|
|
157
154
|
end
|
|
158
155
|
end
|
|
159
156
|
|
|
160
|
-
#
|
|
157
|
+
# Process recent Node API file events since the last persisted checkpoint.
|
|
158
|
+
#
|
|
159
|
+
# @param iteration_persistency [PersistencyActionOnce, nil] stores the last processed event id
|
|
160
|
+
# @return [void]
|
|
161
161
|
def process_events(iteration_persistency)
|
|
162
|
-
#
|
|
162
|
+
# Restrict the event stream to file-related changes for the current access key.
|
|
163
163
|
events_filter = {
|
|
164
164
|
'access_key' => @access_key_self['id'],
|
|
165
165
|
'type' => 'file.*'
|
|
166
166
|
}
|
|
167
|
-
#
|
|
167
|
+
# Resume from the last persisted event id when available.
|
|
168
168
|
events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
|
|
169
169
|
events = @api_node.read('events', events_filter)
|
|
170
170
|
return if events.empty?
|
|
171
171
|
events.each do |event|
|
|
172
|
-
#
|
|
172
|
+
# Ignore non-file events such as folder notifications.
|
|
173
173
|
if event.dig('data', 'type').eql?('file')
|
|
174
174
|
file_entry = @api_node.read("files/#{event['data']['id']}") rescue nil
|
|
175
175
|
if !file_entry.nil? &&
|
|
@@ -179,10 +179,10 @@ module Aspera
|
|
|
179
179
|
generate_preview(file_entry) if event['types'].include?('file.deleted')
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
|
-
# log
|
|
182
|
+
# Periodically log progress and persist the latest processed event.
|
|
183
183
|
next unless @periodic.trigger? || event.equal?(events.last)
|
|
184
184
|
Log.log.debug{"Processing event #{event['id']}"}
|
|
185
|
-
#
|
|
185
|
+
# Save a checkpoint to avoid replaying the full batch after a failure.
|
|
186
186
|
if !iteration_persistency.nil?
|
|
187
187
|
iteration_persistency.data[0] = event['id'].to_s
|
|
188
188
|
iteration_persistency.save
|
|
@@ -190,21 +190,34 @@ module Aspera
|
|
|
190
190
|
end
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# Transfer a file to or from the configured Node storage using a tagged transfer spec.
|
|
194
|
+
#
|
|
195
|
+
# @param direction [String] transfer direction, typically from [`Transfer::Spec`](lib/aspera/transfer/spec.rb)
|
|
196
|
+
# @param folder_id [String] Node API identifier of the reference folder
|
|
197
|
+
# @param source_filename [String] relative source path inside the transfer root
|
|
198
|
+
# @param destination [String, nil] local destination root for receive operations
|
|
199
|
+
# @return [Object] transfer result returned by [`Main.result_transfer`](lib/aspera/cli/main.rb)
|
|
193
200
|
def do_transfer(direction, folder_id, source_filename, destination = '/')
|
|
194
201
|
Aspera.assert(!(destination.nil? && direction.eql?(Transfer::Spec::DIRECTION_RECEIVE)))
|
|
195
202
|
t_spec = @api_node.transfer_spec_gen4(folder_id, direction, {
|
|
196
203
|
'paths' => [{'source' => source_filename}],
|
|
197
204
|
'tags' => {Transfer::Spec::TAG_RESERVED => {PREV_GEN_TAG => true}}
|
|
198
205
|
})
|
|
199
|
-
#
|
|
206
|
+
# Force the destination on the transfer agent object.
|
|
207
|
+
# Setting `t_spec['destination_root']` directly would later be overwritten.
|
|
200
208
|
transfer.user_transfer_spec['destination_root'] = destination
|
|
201
209
|
Main.result_transfer(transfer.start(t_spec))
|
|
202
210
|
end
|
|
203
211
|
|
|
212
|
+
# Populate generation metadata for a source file available on the local filesystem.
|
|
213
|
+
#
|
|
214
|
+
# @param gen_infos [Array<Hash>] preview generation descriptors to enrich
|
|
215
|
+
# @param entry [Hash] file entry containing at least the relative path
|
|
216
|
+
# @return [String] local directory where previews for this entry are stored
|
|
204
217
|
def get_infos_local(gen_infos, entry)
|
|
205
218
|
local_original_filepath = File.join(@local_storage_root, entry['path'])
|
|
206
219
|
original_mtime = File.mtime(local_original_filepath)
|
|
207
|
-
#
|
|
220
|
+
# Output directory for previews generated from the local source file.
|
|
208
221
|
local_entry_preview_dir = File.join(@local_preview_folder, entry_preview_folder_name(entry))
|
|
209
222
|
gen_infos.each do |gen_info|
|
|
210
223
|
gen_info[:src] = local_original_filepath
|
|
@@ -215,41 +228,57 @@ module Aspera
|
|
|
215
228
|
return local_entry_preview_dir
|
|
216
229
|
end
|
|
217
230
|
|
|
231
|
+
# Populate generation metadata for a source file stored remotely on Node.
|
|
232
|
+
#
|
|
233
|
+
# @param gen_infos [Array<Hash>] preview generation descriptors to enrich
|
|
234
|
+
# @param entry [Hash] remote file entry returned by Node API
|
|
235
|
+
# @return [String] temporary local directory where previews are generated
|
|
218
236
|
def get_infos_remote(gen_infos, entry)
|
|
219
|
-
#
|
|
237
|
+
# Download the source file into the temporary workspace before generating previews.
|
|
220
238
|
local_original_filepath = File.join(@tmp_folder, entry['name'])
|
|
221
239
|
# require 'date'
|
|
222
240
|
# original_mtime=DateTime.parse(entry['modified_time'])
|
|
223
|
-
#
|
|
241
|
+
# Local directory where previews are generated before being uploaded back.
|
|
224
242
|
local_entry_preview_dir = File.join(@tmp_folder, entry_preview_folder_name(entry))
|
|
225
243
|
file_info = @api_node.read("files/#{entry['id']}")
|
|
226
|
-
# TODO:
|
|
227
|
-
# this_preview_folder_entries
|
|
228
|
-
# TODO:
|
|
244
|
+
# TODO: This does not work with Gen4 because preview folders are hidden by the API.
|
|
245
|
+
# this_preview_folder_entries=@api_node.read_folder_content(@previews_folder_entry['id'],{name: @entry_preview_folder_name})
|
|
246
|
+
# TODO: Query Gen3 APIs to list preview files and retrieve timestamps.
|
|
229
247
|
gen_infos.each do |gen_info|
|
|
230
248
|
gen_info[:src] = local_original_filepath
|
|
231
249
|
gen_info[:dst] = File.join(local_entry_preview_dir, gen_info[:base_dest])
|
|
232
|
-
# TODO:
|
|
250
|
+
# TODO: Reuse `this_preview_folder_entries` once preview folders become visible.
|
|
233
251
|
gen_info[:preview_exist] = file_info.key?('preview')
|
|
234
|
-
# TODO:
|
|
252
|
+
# TODO: Compare source and preview modification times when remote timestamps are available.
|
|
235
253
|
gen_info[:preview_newer_than_original] = gen_info[:preview_exist]
|
|
236
254
|
end
|
|
237
255
|
return local_entry_preview_dir
|
|
238
256
|
end
|
|
239
257
|
|
|
240
|
-
#
|
|
258
|
+
# Build the preview folder name for a file entry using the Node API convention.
|
|
259
|
+
#
|
|
260
|
+
# @param entry [Hash] file entry containing an `id`
|
|
261
|
+
# @return [String] preview folder name for the entry
|
|
241
262
|
def entry_preview_folder_name(entry)
|
|
242
263
|
"#{entry['id']}#{PREVIEW_FOLDER_SUFFIX}"
|
|
243
264
|
end
|
|
244
265
|
|
|
245
|
-
#
|
|
266
|
+
# Build a preview filename from a basename and target format.
|
|
267
|
+
#
|
|
268
|
+
# @param preview_format [String, Symbol] preview format used as filename extension
|
|
269
|
+
# @param base_name [String, nil] basename to use before the extension
|
|
270
|
+
# @return [String] preview filename
|
|
246
271
|
def preview_filename(preview_format, base_name = nil)
|
|
247
272
|
base_name ||= PREVIEW_BASENAME
|
|
248
273
|
return "#{base_name}.#{preview_format}"
|
|
249
274
|
end
|
|
250
275
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
276
|
+
# Generate all required previews for a single file entry when regeneration is needed.
|
|
277
|
+
#
|
|
278
|
+
# Remote entries must include `parent_file_id`.
|
|
279
|
+
#
|
|
280
|
+
# @param entry [Hash] local or remote file entry to preview
|
|
281
|
+
# @return [void]
|
|
253
282
|
def generate_preview(entry)
|
|
254
283
|
# prepare generic information
|
|
255
284
|
gen_infos = @preview_formats_to_generate.map do |preview_format|
|
|
@@ -280,7 +309,7 @@ module Aspera
|
|
|
280
309
|
end
|
|
281
310
|
begin
|
|
282
311
|
# need generator for further checks
|
|
283
|
-
gen_info[:generator] = Aspera::Preview::Generator.new(gen_info[:src], gen_info[:dst], @gen_options, @tmp_folder, entry['content_type'])
|
|
312
|
+
gen_info[:generator] = Aspera::Preview::Generator.new(gen_info[:src], gen_info[:dst], @gen_options, @tmp_folder, mime: entry['content_type'])
|
|
284
313
|
rescue
|
|
285
314
|
# no conversion supported
|
|
286
315
|
next false
|
|
@@ -300,7 +329,12 @@ module Aspera
|
|
|
300
329
|
end
|
|
301
330
|
Log.log.debug{"source: #{entry['id']}: #{entry['path']}"}
|
|
302
331
|
gen_infos.each do |gen_info|
|
|
303
|
-
gen_info[:generator].generate
|
|
332
|
+
gen_info[:generator].generate
|
|
333
|
+
rescue => e
|
|
334
|
+
Log.log.error{"Ignoring: #{e.class} #{e.message}"}
|
|
335
|
+
Log.log.debug(e.backtrace.join("\n").red)
|
|
336
|
+
# in case of any error, place a standard error image
|
|
337
|
+
FileUtils.cp(gen_info[:generator].error_asset, @destination_file_path)
|
|
304
338
|
end
|
|
305
339
|
if @access_remote
|
|
306
340
|
# upload
|
|
@@ -316,8 +350,9 @@ module Aspera
|
|
|
316
350
|
Log.log.debug(e.backtrace.join("\n").red)
|
|
317
351
|
end
|
|
318
352
|
|
|
319
|
-
#
|
|
320
|
-
# @param
|
|
353
|
+
# Scan all files in provided folder entry
|
|
354
|
+
# @param top_entry [Hash] the top entry to scan
|
|
355
|
+
# @param top_path [String, nil] subpath to start folder scan inside
|
|
321
356
|
def scan_folder_files(top_entry, top_path = nil)
|
|
322
357
|
unless top_path.nil?
|
|
323
358
|
# canonical path: start with / and ends with /
|
|
@@ -353,7 +388,7 @@ module Aspera
|
|
|
353
388
|
else
|
|
354
389
|
Log.log.debug{"#{entry['path']} folder".green}
|
|
355
390
|
# get folder content
|
|
356
|
-
folder_entries =
|
|
391
|
+
folder_entries = @api_node.read_folder_content(entry['id'])
|
|
357
392
|
# process all items in current folder
|
|
358
393
|
folder_entries.each do |folder_entry|
|
|
359
394
|
# add path for older versions of ES
|
|
@@ -376,37 +411,35 @@ module Aspera
|
|
|
376
411
|
def execute_action
|
|
377
412
|
command = options.get_next_command(ACTIONS)
|
|
378
413
|
unless %i[check test show].include?(command)
|
|
379
|
-
#
|
|
414
|
+
# This will use node api
|
|
380
415
|
@api_node = Api::Node.new(**basic_auth_params)
|
|
381
416
|
@transfer_server_address = URI.parse(@api_node.base_url).host
|
|
382
|
-
#
|
|
417
|
+
# Get current access key information
|
|
383
418
|
@access_key_self = @api_node.read('access_keys/self')
|
|
384
419
|
# TODO: check events is activated here:
|
|
385
420
|
# note that docroot is good to look at as well
|
|
386
421
|
node_info = @api_node.read('info')
|
|
387
422
|
Log.log.debug{"root: #{node_info['docroot']}"}
|
|
388
|
-
|
|
423
|
+
# Default storage url to local file if not provided
|
|
424
|
+
option_root_url = options.get_option(:root_url, mandatory: true)
|
|
425
|
+
option_root_url = UriReader.file_url(@access_key_self['storage']['path']) if option_root_url.empty? && @access_key_self['storage']['type'].eql?('local')
|
|
426
|
+
@access_remote = !UriReader.file?(option_root_url)
|
|
389
427
|
Log.log.debug{"remote: #{@access_remote}"}
|
|
390
|
-
|
|
391
|
-
# TODO: can the previews folder parameter be read from node api ?
|
|
428
|
+
# TODO: can the `previews` folder parameter be read from Node API ?
|
|
392
429
|
@option_skip_folders.push("/#{@option_previews_folder}")
|
|
393
430
|
if @access_remote
|
|
394
431
|
# NOTE: the filter "name", it's why we take the first one
|
|
395
|
-
@previews_folder_entry =
|
|
432
|
+
@previews_folder_entry = @api_node.read_folder_content(@access_key_self['root_file_id'], {name: @option_previews_folder}).first
|
|
396
433
|
raise Cli::Error, "Folder #{@option_previews_folder} does not exist on node. " \
|
|
397
434
|
'Please create it in the storage root, or specify an alternate name.' if @previews_folder_entry.nil?
|
|
398
435
|
else
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
# TODO: option to override @local_storage_root='xxx'
|
|
402
|
-
@local_storage_root = @local_storage_root[PVCL_LOCAL_STORAGE.length..-1] if @local_storage_root.start_with?(PVCL_LOCAL_STORAGE)
|
|
403
|
-
# TODO: windows could have "C:" ?
|
|
436
|
+
@local_storage_root = UriReader.file_path(option_root_url)
|
|
437
|
+
# TODO: Windows could have "C:" ?
|
|
404
438
|
Aspera.assert(@local_storage_root.start_with?('/')){"not local storage: #{@local_storage_root}"}
|
|
405
439
|
Aspera.assert(File.directory?(@local_storage_root), type: Cli::Error){"Local storage root folder #{@local_storage_root} does not exist."}
|
|
406
440
|
@local_preview_folder = File.join(@local_storage_root, @option_previews_folder)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# protection to avoid clash of file id for two different access keys
|
|
441
|
+
Aspera.assert(File.directory?(@local_preview_folder), type: Cli::Error){"Folder #{@local_preview_folder} does not exist locally. Please create it, or specify an alternate name."}
|
|
442
|
+
# Protection to avoid clash of file id for two different access keys
|
|
410
443
|
marker_file = File.join(@local_preview_folder, AK_MARKER_FILE)
|
|
411
444
|
Log.log.debug{"marker file: #{marker_file}"}
|
|
412
445
|
if File.exist?(marker_file)
|
|
@@ -459,18 +492,21 @@ module Aspera
|
|
|
459
492
|
return Main.result_status("#{command} finished")
|
|
460
493
|
when :check
|
|
461
494
|
return Main.result_status('Tools validated')
|
|
462
|
-
when :test
|
|
495
|
+
when :test
|
|
463
496
|
source = options.get_next_argument('source file')
|
|
464
497
|
format = options.get_next_argument('format', accept_list: Aspera::Preview::Generator::PREVIEW_FORMATS, default: :png)
|
|
465
498
|
generated_file_path = preview_filename(format, options.get_option(:base))
|
|
466
|
-
|
|
467
|
-
g.generate
|
|
468
|
-
if command.eql?(:show)
|
|
469
|
-
terminal_options = (options.get_option(:query) || {}).symbolize_keys
|
|
470
|
-
Log.log.debug{"preview: #{generated_file_path}"}
|
|
471
|
-
formatter.display_status(Aspera::Preview::Terminal.build(File.read(generated_file_path), **terminal_options))
|
|
472
|
-
end
|
|
499
|
+
Aspera::Preview::Generator.new(source, generated_file_path, @gen_options, @tmp_folder).generate
|
|
473
500
|
return Main.result_status("generated: #{generated_file_path}")
|
|
501
|
+
when :show
|
|
502
|
+
source = options.get_next_argument('source file')
|
|
503
|
+
# terminal_options = options.get_next_argument('options', validation: Hash, default: {}).symbolize_keys
|
|
504
|
+
generated_file_path = preview_filename(:png, options.get_option(:base))
|
|
505
|
+
Aspera::Preview::Generator.new(source, generated_file_path, @gen_options, @tmp_folder).generate
|
|
506
|
+
formatter.display_status("generated: #{generated_file_path}")
|
|
507
|
+
# formatter.display_status(Aspera::Preview::Terminal.build(File.read(generated_file_path), **terminal_options))
|
|
508
|
+
# return Main.result_status("generated: #{generated_file_path}")
|
|
509
|
+
return Main.result_image(UriReader.file_url(generated_file_path))
|
|
474
510
|
else Aspera.error_unexpected_value(command)
|
|
475
511
|
end
|
|
476
512
|
ensure
|
|
@@ -86,21 +86,13 @@ module Aspera
|
|
|
86
86
|
|
|
87
87
|
def initialize(**_)
|
|
88
88
|
super
|
|
89
|
-
@ssh_opts = {}
|
|
90
89
|
@connection_type = :ssh
|
|
91
90
|
options.declare(:ssh_keys, 'SSH key path list', allowed: Allowed::TYPES_STRING_ARRAY)
|
|
92
91
|
options.declare(:passphrase, 'SSH private key passphrase')
|
|
93
|
-
options.declare(:ssh_options, 'SSH options', allowed: Hash,
|
|
92
|
+
options.declare(:ssh_options, 'SSH options', allowed: Hash, default: {})
|
|
94
93
|
SyncActions.declare_options(options)
|
|
95
94
|
options.parse_options!
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def option_ssh_opts; @ssh_opts; end
|
|
99
|
-
|
|
100
|
-
# multiple option are merged
|
|
101
|
-
def option_ssh_opts=(value)
|
|
102
|
-
Aspera.assert_type(value, Hash)
|
|
103
|
-
@ssh_opts.deep_merge!(value.compact.symbolize_keys)
|
|
95
|
+
@ssh_opts = options.get_option(:ssh_options).symbolize_keys
|
|
104
96
|
end
|
|
105
97
|
|
|
106
98
|
# Read command line options
|
|
@@ -135,9 +135,9 @@ module Aspera
|
|
|
135
135
|
when :node
|
|
136
136
|
return entity_execute(api: api_shares_admin, entity: 'data/nodes')
|
|
137
137
|
when :share
|
|
138
|
-
share_command = options.get_next_command(%i[user_permissions group_permissions].concat(
|
|
138
|
+
share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Operations::ALL))
|
|
139
139
|
case share_command
|
|
140
|
-
when *
|
|
140
|
+
when *Operations::ALL
|
|
141
141
|
return entity_execute(
|
|
142
142
|
api: api_shares_admin,
|
|
143
143
|
entity: 'data/shares',
|
|
@@ -146,7 +146,7 @@ module Aspera
|
|
|
146
146
|
&lookup_share
|
|
147
147
|
)
|
|
148
148
|
when :user_permissions, :group_permissions
|
|
149
|
-
share_id = instance_identifier(&lookup_share)
|
|
149
|
+
share_id = options.instance_identifier(&lookup_share)
|
|
150
150
|
return entity_execute(api: api_shares_admin, entity: "data/shares/#{share_id}/#{share_command}")
|
|
151
151
|
end
|
|
152
152
|
when :transfer_settings
|
|
@@ -181,9 +181,9 @@ module Aspera
|
|
|
181
181
|
entity_verb = options.get_next_command(entity_commands)
|
|
182
182
|
lookup_block = ->(field, value){RestList.lookup_entity_generic(entity: entity_type, field: field, value: value){api_shares_admin.read(entities_path)}['id']}
|
|
183
183
|
case entity_verb
|
|
184
|
-
when *
|
|
184
|
+
when *Operations::ALL
|
|
185
185
|
display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
|
|
186
|
-
display_fields.push(
|
|
186
|
+
display_fields.push('directory_user') if entity_type.eql?(:user) && entities_location.eql?(:all)
|
|
187
187
|
return entity_execute(
|
|
188
188
|
api: api_shares_admin,
|
|
189
189
|
entity: entities_path,
|
|
@@ -192,7 +192,7 @@ module Aspera
|
|
|
192
192
|
&lookup_block
|
|
193
193
|
)
|
|
194
194
|
when *USR_GRP_SETTINGS # transfer_settings, app_authorizations, share_permissions
|
|
195
|
-
group_id = instance_identifier(&lookup_block)
|
|
195
|
+
group_id = options.instance_identifier(&lookup_block)
|
|
196
196
|
entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
|
|
197
197
|
return entity_execute(api: api_shares_admin, entity: entities_path, is_singleton: !entity_verb.eql?(:share_permissions), &lookup_share)
|
|
198
198
|
when :import # saml
|
|
@@ -210,7 +210,7 @@ module Aspera
|
|
|
210
210
|
api_shares_admin.create(entities_path, {entity_type=>entity_name})
|
|
211
211
|
end
|
|
212
212
|
when :users # group
|
|
213
|
-
return entity_execute(api: api_shares_admin, entity: "#{entities_path}/#{instance_identifier(&lookup_block)}/#{entities_prefix}users")
|
|
213
|
+
return entity_execute(api: api_shares_admin, entity: "#{entities_path}/#{options.instance_identifier(&lookup_block)}/#{entities_prefix}users")
|
|
214
214
|
else Aspera.error_unexpected_value(entity_verb)
|
|
215
215
|
end
|
|
216
216
|
end
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/dot_container.rb
CHANGED
|
@@ -61,6 +61,7 @@ module Aspera
|
|
|
61
61
|
def to_dotted
|
|
62
62
|
result = {}
|
|
63
63
|
until @stack.empty?
|
|
64
|
+
# path: Array, current: Array or Hash or other
|
|
64
65
|
path, current = @stack.pop
|
|
65
66
|
to_insert = nil
|
|
66
67
|
# empty things are left intact
|
|
@@ -78,8 +79,11 @@ module Aspera
|
|
|
78
79
|
elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
|
|
79
80
|
to_insert = current.map{ |i| i['name']}
|
|
80
81
|
# Array of Hashes with only 'name' and 'value' keys -> Hash of key/values
|
|
81
|
-
elsif current.all?{ |i| i.is_a?(Hash) && i.
|
|
82
|
-
|
|
82
|
+
elsif current.all?{ |i| i.is_a?(Hash) && i.key?('name') && i.key?('value') && i.length <= 3}
|
|
83
|
+
# if there is an extra key, other than 'name' and 'value', insert that key as is
|
|
84
|
+
add_elements(path, current.flat_map{ |h| h.except('name', 'value').to_a})
|
|
85
|
+
# Insert name/value pairs as Hash
|
|
86
|
+
add_elements(path, current.to_h{ |h| h.values_at('name', 'value')})
|
|
83
87
|
else
|
|
84
88
|
add_elements(path, current.each_with_index.map{ |v, i| [i, v]})
|
|
85
89
|
end
|
|
@@ -97,7 +101,7 @@ module Aspera
|
|
|
97
101
|
# Add elements of enumerator to the @stack, in reverse order
|
|
98
102
|
def add_elements(path, enum)
|
|
99
103
|
enum.reverse_each do |key, value|
|
|
100
|
-
@stack.push([path + [key], value])
|
|
104
|
+
@stack.push([path + [key.to_s], value])
|
|
101
105
|
end
|
|
102
106
|
nil
|
|
103
107
|
end
|
data/lib/aspera/environment.rb
CHANGED
|
@@ -91,9 +91,9 @@ module Aspera
|
|
|
91
91
|
#
|
|
92
92
|
# @param cmd [Array<#to_s>] The executable and its arguments.
|
|
93
93
|
# @param mode [:execute, :background, :capture] The execution strategy:
|
|
94
|
-
# - `:execute` Uses {Kernel.system}. Returns `true`, `false`, or `nil`.
|
|
94
|
+
# - `:execute` Uses {Kernel.system}. Returns `true`, `false`, or `nil`. (Default)
|
|
95
95
|
# - `:background` Uses {Process.spawn}. Returns the spawned process PID.
|
|
96
|
-
# - `:capture` Uses {Open3.capture3}. Returns captured
|
|
96
|
+
# - `:capture` Uses {Open3.capture3}. Returns captured out, err, and status.
|
|
97
97
|
#
|
|
98
98
|
# @param kwargs [Hash] Additional options forwarded to the underlying call.
|
|
99
99
|
#
|
|
@@ -316,6 +316,7 @@ module Aspera
|
|
|
316
316
|
|
|
317
317
|
# Replacement character for illegal filename characters
|
|
318
318
|
# Can also be used as safe "join" character
|
|
319
|
+
# @return [String] One character
|
|
319
320
|
def safe_filename_character
|
|
320
321
|
return REPLACE_CHARACTER if @file_illegal_characters.nil? || @file_illegal_characters.empty?
|
|
321
322
|
@file_illegal_characters[0]
|
data/lib/aspera/log.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Aspera
|
|
|
95
95
|
PP.pp(object, +'')
|
|
96
96
|
else error_unexpected_value(instance.dump_format){'dump format'}
|
|
97
97
|
end
|
|
98
|
-
"#{name.to_s.green}
|
|
98
|
+
"#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Capture the output of $stderr and log it at debug level
|
|
@@ -14,7 +14,7 @@ module Aspera
|
|
|
14
14
|
# values for conversion_type : input format
|
|
15
15
|
CONVERSION_TYPES = %i[image office pdf plaintext video].freeze
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Special cases for MIME types
|
|
18
18
|
# spellchecker:disable
|
|
19
19
|
SUPPORTED_MIME_TYPES = {
|
|
20
20
|
'application/json' => :plaintext,
|