aspera-cli 4.27.2 → 4.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -3,11 +3,11 @@
3
3
  # cspell:ignore trevents
4
4
  require 'aspera/cli/plugins/basic_auth'
5
5
  require 'aspera/preview/generator'
6
- require 'aspera/preview/options'
7
6
  require 'aspera/preview/utils'
8
7
  require 'aspera/preview/file_types'
9
8
  require 'aspera/preview/terminal'
10
9
  require 'aspera/transfer/spec'
10
+ require 'aspera/schema/registry'
11
11
  require 'aspera/persistency_action_once'
12
12
  require 'aspera/temp_file_manager'
13
13
  require 'aspera/api/node'
@@ -18,6 +18,8 @@ require 'aspera/uri_reader'
18
18
  require 'aspera/log'
19
19
  require 'aspera/assert'
20
20
  require 'securerandom'
21
+ require 'aspera/rainbow'
22
+ using Rainbow
21
23
 
22
24
  module Aspera
23
25
  module Cli
@@ -46,53 +48,59 @@ module Aspera
46
48
  :LOG_LIMITER_SEC,
47
49
  :REMOTE_ACCESS
48
50
 
49
- attr_accessor :option_skip_types, :option_previews_folder, :option_folder_reset_cache, :option_skip_folders, :option_overwrite
50
-
51
- # DSL option declarations (Pattern 1 and Pattern 2 - handler: Symbol resolved to self)
51
+ # DSL option declarations
52
52
  option :skip_format, description: 'Skip this preview format', allowed: Aspera::Preview::Generator::PREVIEW_FORMATS
53
53
  # TODO: use the same option as in `node` plugin
54
- option :folder_reset_cache, description: 'Force detection of generated preview by refresh cache', allowed: %i[no header read], handler: :option_folder_reset_cache, default: :no
55
- option :skip_types, description: 'Skip generation for those types of files', allowed: Type::SYMBOL_ARRAY + Aspera::Preview::FileTypes::CONVERSION_TYPES, handler: :option_skip_types
56
- option :previews_folder, description: 'Preview folder in storage root', handler: :option_previews_folder, default: DEFAULT_PREVIEWS_FOLDER
57
- option :skip_folders, description: 'List of folder to skip', allowed: Type::STRING_ARRAY, handler: :option_skip_folders
54
+ option :folder_reset_cache, description: 'Force detection of generated preview by refresh cache', allowed: %i[no header read], default: :no
55
+ option :skip_types, description: 'Skip generation for those types of files', allowed: Type::SYMBOL_ARRAY + Aspera::Preview::FileTypes::CONVERSION_TYPES
56
+ option :previews_folder, description: 'Preview folder in storage root', default: DEFAULT_PREVIEWS_FOLDER
57
+ option :skip_folders, description: 'List of folder to skip', allowed: Type::STRING_ARRAY
58
58
  option :base, description: 'Basename of output for for test'
59
- option :filter, description: 'File name filter: String (glob), Regexp, or Proc', allowed: [String, Regexp, Proc, NilClass]
60
- option :mimemagic, description: 'Use Mime type detection of gem mimemagic', allowed: Type::BOOLEAN, default: false
61
- option :overwrite, description: 'When to overwrite result file', allowed: %i[always never mtime], handler: :option_overwrite, default: :mtime
59
+ option :filter, description: 'File name filter: String (glob), Regexp, or Proc', allowed: [String, Regexp, Proc, NilClass]
60
+ option :detect_mime, description: 'Detect Mime type by analyzing file', allowed: Type::BOOLEAN, default: false
61
+ option :overwrite, description: 'When to overwrite result file', allowed: %i[always never mtime], default: :mtime
62
62
  option :root_url, description: "How to read and write files on storage (#{REMOTE_ACCESS}, or #{UriReader.file_url('<folder>')})", default: REMOTE_ACCESS
63
- # Generator-specific options (Category C - bound to @gen_options via set_handler in initialize)
64
- Aspera::Preview::Options::DESCRIPTIONS.each do |opt|
65
- values = if opt.key?(:values)
66
- opt[:values]
67
- elsif BoolValue.symbol?(opt[:default])
68
- BoolValue::TYPES
69
- end
70
- option(opt[:name], description: opt[:description].capitalize, allowed: values, default: opt[:default])
63
+ # Generator options, defined in schema: values set in @gen_options
64
+ Schema::Registry.instance.reader(Schema::Registry::PREVIEW_OPTIONS).current['properties'].each do |name, property|
65
+ key = name.to_sym
66
+ option(
67
+ key,
68
+ description: property['description'].lines.first.strip.delete_suffix('.'),
69
+ allowed: property.key?('enum') ? property['enum'].map(&:to_sym) : {'integer' => Type::INTEGER, 'number' => Type::FLOAT, 'boolean' => Type::BOOLEAN}[property['type']],
70
+ default: property['default'],
71
+ # `Hash` value: validated with schema
72
+ schema: ("#{Schema::Registry::PREVIEW_OPTIONS}:properties.#{name}" if property['type'].eql?('object')),
73
+ on_set: ->(value) { @gen_options[key] = value }
74
+ )
71
75
  end
72
76
 
73
77
  def initialize(**_)
78
+ # Generator options, see `options.schema.yaml`
79
+ # Created before `super`, which declares options: values are set through `on_set`.
80
+ @gen_options = Aspera::Preview::Generator::Options.new
74
81
  super
75
- # Generator configuration populated from CLI options.
76
- @gen_options = Aspera::Preview::Options.new
77
82
  # Used to rate-limit periodic progress logging and checkpoint persistence.
78
83
  @periodic = TimerLimiter.new(LOG_LIMITER_SEC)
79
84
  # Optional callback used to filter entries before generation.
80
85
  @filter_block = nil
81
86
  @access_remote = true
82
- # Bind generator-specific options to @gen_options (Category C: set_handler after object creation)
83
- Aspera::Preview::Options::DESCRIPTIONS.each do |opt|
84
- options.set_handler(opt[:name], object: @gen_options, method: opt[:name])
85
- end
86
-
87
+ # Values set through `on_set` callbacks are used below
87
88
  options.parse_options!
89
+ @option_skip_types = options.get_option(:skip_types)
90
+ @option_previews_folder = options.get_option(:previews_folder)
91
+ @option_folder_reset_cache = options.get_option(:folder_reset_cache)
92
+ # Modified during scan
93
+ @option_skip_folders = options.get_option(:skip_folders).dup
94
+ @option_overwrite = options.get_option(:overwrite)
95
+ # Tell which tool we will use for office docs
96
+ Aspera::Preview::Utils.office_tool = @gen_options.office_conversion
88
97
  Api::Node.api_options[:cache] = !@option_folder_reset_cache.eql?(:header)
89
98
  # Start from the full supported format list, then remove any skipped format.
90
99
  @preview_formats_to_generate = Aspera::Preview::Generator::PREVIEW_FORMATS.clone
91
100
  skip = options.get_option(:skip_format)
92
101
  @preview_formats_to_generate.delete(skip) if skip
102
+ # Created by `tools_cleanup`, only for commands that use it
93
103
  @tmp_folder = File.join(TempFileManager.instance.global_temp, "#{TMP_DIR_PREFIX}.#{SecureRandom.uuid}")
94
- FileUtils.mkdir_p(@tmp_folder)
95
- Log.log.debug { "tmpdir: #{@tmp_folder}" }
96
104
  end
97
105
 
98
106
  # Process legacy transfer events and trigger preview generation for completed downloads.
@@ -108,7 +116,7 @@ module Aspera
108
116
  events_filter['iteration_token'] = iteration_persistency.data.first unless iteration_persistency.nil?
109
117
  begin
110
118
  events = @api_node.read('events', events_filter)
111
- rescue RestCallError => e
119
+ rescue Rest::CallError => e
112
120
  if e.message.include?('Invalid iteration_token')
113
121
  Log.log.warn { "Retrying without iteration token: #{e}" }
114
122
  events_filter.delete('iteration_token')
@@ -360,7 +368,7 @@ module Aspera
360
368
  Log.log.debug { "processing entry #{entry_path_with_slash}" } if @periodic.trigger?
361
369
  entry_path_with_slash = "#{entry_path_with_slash}/" unless entry_path_with_slash.end_with?('/')
362
370
  if !top_path.nil? && !top_path.start_with?(entry_path_with_slash) && !entry_path_with_slash.start_with?(top_path)
363
- Log.log.debug { "#{entry['path']} folder (skip start)".bg_red }
371
+ Log.log.debug { "#{entry['path']} folder (skip start)".bg(:red) }
364
372
  next
365
373
  end
366
374
  Log.log.debug { "item:#{entry}" }
@@ -376,7 +384,7 @@ module Aspera
376
384
  Log.log.debug('Ignoring link.')
377
385
  when 'folder'
378
386
  if @option_skip_folders.include?(entry['path'])
379
- Log.log.debug { "#{entry['path']} folder (skip list)".bg_red }
387
+ Log.log.debug { "#{entry['path']} folder (skip list)".bg(:red) }
380
388
  else
381
389
  Log.log.debug { "#{entry['path']} folder".green }
382
390
  # get folder content
@@ -402,14 +410,16 @@ module Aspera
402
410
 
403
411
  # scan, events and trevents connect to the Node API (setup: :setup_node_api).
404
412
  # `check`, `test` and `show` work without Node API authentication.
405
- command :scan, description: 'Scan a file or folder and generate previews', setup: :setup_node_api, arguments: [{name: :path, type: String, description: 'File or folder from which to start scanning', mandatory: false, default: '/'}]
406
- command :events, description: 'Process file events and generate previews', setup: :setup_node_api
407
- command :trevents, description: 'Process transfer events and generate previews', setup: :setup_node_api
408
- command :check, description: 'Check required tools are installed'
409
- command :show, description: 'Generate and display preview of a source file', arguments: [{name: :source_file, type: String, description: 'Local file to preview'}]
413
+ command :scan, description: 'Scan a file or folder and generate previews', setup: :setup_node_api,
414
+ arguments: [{name: :path, description: 'File or folder from which to start scanning', mandatory: false, default: '/'}]
415
+ command :events, description: 'Process file events and generate previews', setup: :setup_node_api, action: ->(**) { run_event_loop(:events) }
416
+ command :trevents, description: 'Process transfer events and generate previews', setup: :setup_node_api, action: ->(**) { run_event_loop(:trevents) }
417
+ command :check, description: 'Check required tools are installed', action: ->(**) { tools_cleanup { Result::Status.new('Tools validated') } }
418
+ command :show, description: 'Generate and display preview of a source file',
419
+ arguments: [{name: :source_file, description: 'Local file to preview'}]
410
420
  command :test, description: 'Test preview generation for a source file',
411
421
  arguments: [
412
- {name: :source_file, type: String, description: 'Local file to preview'},
422
+ {name: :source_file, description: 'Local file to preview'},
413
423
  {name: :format, description: 'Preview output format', allowed: Aspera::Preview::Generator::PREVIEW_FORMATS, mandatory: false, default: :png}
414
424
  ]
415
425
 
@@ -428,7 +438,7 @@ module Aspera
428
438
  Log.log.debug { "root: #{node_info['docroot']}" }
429
439
  # Default storage url to local file if not provided
430
440
  option_root_url = options.get_option(:root_url, mandatory: true)
431
- option_root_url = UriReader.file_url(@access_key_self['storage']['path']) if option_root_url.eql?(REMOTE_ACCESS) && @access_key_self['storage']['type'].eql?('local')
441
+ option_root_url = UriReader.file_url(@access_key_self['storage']['path']) if option_root_url.eql?(REMOTE_ACCESS) && @access_key_self['storage']&.fetch('type').eql?('local')
432
442
  @access_remote = !UriReader.file?(option_root_url)
433
443
  Log.log.debug { "remote: #{@access_remote}" }
434
444
  # TODO: can the `previews` folder parameter be read from Node API ?
@@ -461,71 +471,49 @@ module Aspera
461
471
  # --- handlers ---
462
472
 
463
473
  def action_scan(path:, **)
464
- check_tools_and_mimemagic
465
- apifid =
466
- if (selector = Parser.percent_selector(path))
467
- Aspera.assert_values(selector[:field], ['id'], type: BadArgument) { 'file id' }
468
- Api::NodeFileId.new(@api_node, selector[:value].to_s.empty? ? @access_key_self['root_file_id'] : selector[:value])
469
- else
470
- @api_node.resolve_api_fid(@access_key_self['root_file_id'], path)
471
- end
472
- @filter_block = Base.file_matcher(options.get_option(:filter))
473
- scan_folder_files(@api_node.read("files/#{apifid.file_id}"))
474
- Result::Status.new('scan finished')
475
- ensure
476
- cleanup_tmp_folder
477
- end
478
-
479
- def action_events(**)
480
- check_tools_and_mimemagic
481
- run_event_loop(:events, filter: options.get_option(:filter))
482
- ensure
483
- cleanup_tmp_folder
484
- end
485
-
486
- def action_trevents(**)
487
- check_tools_and_mimemagic
488
- run_event_loop(:trevents, filter: options.get_option(:filter))
489
- ensure
490
- cleanup_tmp_folder
474
+ tools_cleanup do
475
+ apifid =
476
+ if (selector = Parser.percent_selector(path))
477
+ Aspera.assert_values(selector[:field], ['id'], type: BadArgument) { 'file id' }
478
+ Api::NodeFileId.new(@api_node, selector[:value].to_s.empty? ? @access_key_self['root_file_id'] : selector[:value])
479
+ else
480
+ @api_node.resolve_api_fid(@access_key_self['root_file_id'], path)
481
+ end
482
+ @filter_block = Base.file_matcher(options.get_option(:filter))
483
+ scan_folder_files(@api_node.read("files/#{apifid.file_id}"))
484
+ Result::Status.new('scan finished')
485
+ end
491
486
  end
492
487
 
493
- def action_check
494
- check_tools_and_mimemagic
495
- Result::Status.new('Tools validated')
496
- ensure
497
- cleanup_tmp_folder
498
- end
488
+ # Status message for generation
489
+ MSG_GEN_TITLE = 'Generated: '
499
490
 
500
491
  def action_test(source_file:, format:, **)
501
- check_tools_and_mimemagic
502
- generated_file_path = preview_filename(format, options.get_option(:base))
503
- Aspera::Preview::Generator.new(source_file, generated_file_path, @gen_options, @tmp_folder).generate
504
- Result::Status.new("generated: #{generated_file_path}")
505
- ensure
506
- cleanup_tmp_folder
492
+ tools_cleanup do
493
+ generated_file_path = preview_filename(format, options.get_option(:base))
494
+ Aspera::Preview::Generator.new(source_file, generated_file_path, @gen_options, @tmp_folder).generate
495
+ Result::Status.new("#{MSG_GEN_TITLE}#{generated_file_path}")
496
+ end
507
497
  end
508
498
 
509
499
  def action_show(source_file:, **)
510
- check_tools_and_mimemagic
511
- generated_file_path = preview_filename(:png, options.get_option(:base))
512
- Aspera::Preview::Generator.new(source_file, generated_file_path, @gen_options, @tmp_folder).generate
513
- formatter.display_status("generated: #{generated_file_path}")
500
+ status = action_test(source_file: source_file, format: :png)
501
+ formatter.display_status(status.data)
502
+ generated_file_path = status.data.delete_prefix(MSG_GEN_TITLE)
514
503
  Result::Image.new(UriReader.file_url(generated_file_path))
515
- ensure
516
- cleanup_tmp_folder
517
504
  end
518
505
 
519
506
  private
520
507
 
521
- # Check tools and set mimemagic flag (required for all commands).
522
- def check_tools_and_mimemagic
523
- Aspera::Preview::FileTypes.instance.use_mimemagic = options.get_option(:mimemagic, mandatory: true)
508
+ # Check tools and set option flag.
509
+ # Create the temporary folder for the command, and clean it up after.
510
+ def tools_cleanup
511
+ Aspera::Preview::FileTypes.instance.detect_mime = options.get_option(:detect_mime, mandatory: true)
524
512
  Aspera::Preview::Utils.check_tools(@option_skip_types)
525
- end
526
-
527
- # Clean up the temporary folder after each command.
528
- def cleanup_tmp_folder
513
+ FileUtils.mkdir_p(@tmp_folder)
514
+ Log.log.debug { "tmpdir: #{@tmp_folder}" }
515
+ yield
516
+ ensure
529
517
  Log.log.debug { "cleaning up temp folder #{@tmp_folder}" }
530
518
  FileUtils.rm_rf(@tmp_folder)
531
519
  end
@@ -533,23 +521,26 @@ module Aspera
533
521
  # Shared event-loop body for :events and :trevents.
534
522
  # @param command [:events, :trevents]
535
523
  # @param filter [String, Regexp, Proc, nil] optional filter expression
536
- def run_event_loop(command, filter: nil)
537
- @filter_block = Base.file_matcher(filter)
538
- iteration_persistency = nil
539
- if options.get_option(:once_only, mandatory: true)
540
- iteration_persistency = PersistencyActionOnce.new(
541
- manager: persistency,
542
- data: [],
543
- id: IdGenerator.from_list(
544
- 'preview_iteration',
545
- command.to_s,
546
- options.get_option(:url, mandatory: true),
547
- options.get_option(:username, mandatory: true)
524
+ def run_event_loop(command)
525
+ tools_cleanup do
526
+ filter = options.get_option(:filter)
527
+ @filter_block = Base.file_matcher(filter)
528
+ iteration_persistency = nil
529
+ if options.get_option(:once_only, mandatory: true)
530
+ iteration_persistency = PersistencyActionOnce.new(
531
+ manager: persistency,
532
+ data: [],
533
+ id: IdGenerator.from_list(
534
+ 'preview_iteration',
535
+ command.to_s,
536
+ options.get_option(:url, mandatory: true),
537
+ options.get_option(:username, mandatory: true)
538
+ )
548
539
  )
549
- )
540
+ end
541
+ send(:"process_#{command}", iteration_persistency)
542
+ Result::Status.new("#{command} finished")
550
543
  end
551
- send(:"process_#{command}", iteration_persistency)
552
- Result::Status.new("#{command} finished")
553
544
  end
554
545
  end
555
546
  end
@@ -72,7 +72,7 @@ module Aspera
72
72
  username: options.get_option(:username, mandatory: true),
73
73
  password: options.get_option(:password, mandatory: true)
74
74
  },
75
- test_args: 'files browse /'
75
+ test_args: 'browse /'
76
76
  }
77
77
  end
78
78
 
@@ -81,7 +81,6 @@ module Aspera
81
81
  @connection_type = :ssh
82
82
  @ascmd_executor = nil
83
83
  @server_transfer_spec = nil
84
- options.parse_options!
85
84
  @ssh_opts = options.get_option(:ssh_options).symbolize_keys
86
85
  end
87
86
 
@@ -160,7 +159,7 @@ module Aspera
160
159
  end
161
160
 
162
161
  Sync::Operations::DIRECTIONS.each do |dir|
163
- define_method(:"action_sync_#{dir}") { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info) { @server_transfer_spec } }
162
+ define_action_method([:sync, dir]) { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info) { server_transfer_spec } }
164
163
  end
165
164
 
166
165
  # --- DSL ---
@@ -169,22 +168,18 @@ module Aspera
169
168
  option :passphrase, description: 'SSH private key passphrase'
170
169
  option :ssh_options, description: 'SSH options', allowed: Hash
171
170
 
172
- # root_setup runs once before any command argument is consumed, populating
173
- # @server_transfer_spec and @ascmd_executor so that condition methods work.
174
- root_setup :setup_server
175
-
176
171
  command :health, description: 'Check transfer health'
177
- command(:upload, description: 'Upload files to server', transfer_paths: :send, action: lambda do
178
- @server_transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(:upload)
179
- Runner.result_transfer(transfer.start(@server_transfer_spec))
172
+ command(:upload, description: 'Upload files to server', transfer_paths: :send, action: lambda do |**|
173
+ server_transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(:upload)
174
+ Runner.result_transfer(transfer.start(server_transfer_spec))
180
175
  end)
181
176
  command(
182
177
  :download,
183
178
  description: 'Download files from server',
184
179
  transfer_paths: :receive,
185
- action: lambda do
186
- @server_transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(:download)
187
- Runner.result_transfer(transfer.start(@server_transfer_spec))
180
+ action: lambda do |**|
181
+ server_transfer_spec['direction'] = Transfer::Spec.transfer_type_to_direction(:download)
182
+ Runner.result_transfer(transfer.start(server_transfer_spec))
188
183
  end
189
184
  )
190
185
  command :sync, description: 'Synchronize files with server'
@@ -192,8 +187,7 @@ module Aspera
192
187
  Sync::Operations::DIRECTIONS.each do |dir|
193
188
  command dir, description: "#{dir.capitalize}-sync with server", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
194
189
  end
195
- command :admin, description: 'Manage sync database (admin operations)'
196
- SyncActions.register_sync_admin_commands(self, :admin)
190
+ sync_admin_commands :admin
197
191
  end
198
192
 
199
193
  commands_under :health do
@@ -201,43 +195,76 @@ module Aspera
201
195
  end
202
196
 
203
197
  # AsCmd operations (available only when SSH/local - not WSS)
204
- ASCMD_ARGS = [{name: :command_arguments, multiple: true, mandatory: false, default: nil}].freeze
205
- private_constant :ASCMD_ARGS
198
+ # Arguments follow AsCmd::OPS_ARGS: one path per operation, or source and destination
199
+ PATHS_ARGS = [{name: :paths, multiple: true}].freeze
200
+ private_constant :PATHS_ARGS
206
201
 
207
- command :ls, description: 'List files', condition: :ascmd_available?, aliases: [:browse], arguments: ASCMD_ARGS
208
- command :rm, description: 'Delete files', condition: :ascmd_available?, aliases: [:delete], arguments: ASCMD_ARGS
209
- command :mv, description: 'Rename/move files', condition: :ascmd_available?, aliases: [:rename], arguments: ASCMD_ARGS
210
- command :cp, description: 'Copy files', condition: :ascmd_available?, arguments: ASCMD_ARGS
211
- command :mkdir, description: 'Create directory', condition: :ascmd_available?, arguments: ASCMD_ARGS
212
- command :df, description: 'Show disk usage', condition: :ascmd_available?, arguments: ASCMD_ARGS
213
- command :du, description: 'Show file sizes', condition: :ascmd_available?, arguments: ASCMD_ARGS
214
- command :md5sum, description: 'Compute MD5 checksums', condition: :ascmd_available?, arguments: ASCMD_ARGS
215
- command :info, description: 'Show server system information', condition: :ascmd_available?, arguments: ASCMD_ARGS
202
+ command(
203
+ :ls, description: 'List files', condition: :ascmd_available?, aliases: [:browse], arguments: PATHS_ARGS,
204
+ action: lambda do |paths:, **|
205
+ execute_ascmd(:ls, paths) do |result|
206
+ Result::ObjectList.new(result.map(&:stringify_keys), fields: %w[zmode zuid zgid size mtime name])
207
+ end
208
+ end
209
+ )
210
+ command :rm, description: 'Delete files', condition: :ascmd_available?, aliases: [:delete], arguments: PATHS_ARGS
211
+ command :mv, description: 'Rename/move files: source destination pairs, or sources with --to-folder', condition: :ascmd_available?, aliases: [:rename], arguments: PATHS_ARGS
212
+ command :cp, description: 'Copy files: source destination pairs, or sources with --to-folder', condition: :ascmd_available?, arguments: PATHS_ARGS
213
+ command :mkdir, description: 'Create directory', condition: :ascmd_available?, arguments: PATHS_ARGS
214
+ command(
215
+ :df, description: 'Show disk usage', condition: :ascmd_available?,
216
+ action: lambda do |**|
217
+ execute_ascmd(:df, []) do |result|
218
+ Result::ObjectList.new(result.map(&:stringify_keys))
219
+ end
220
+ end
221
+ )
222
+ command :du, description: 'Show file sizes', condition: :ascmd_available?, arguments: PATHS_ARGS
223
+ command :md5sum, description: 'Compute MD5 checksums', condition: :ascmd_available?, arguments: PATHS_ARGS
224
+ command :info, description: 'Show server system information', condition: :ascmd_available?,
225
+ action: ->(**) { execute_ascmd(:info, []) { |r| Result::SingleObject.new(r.stringify_keys) } }
216
226
 
217
227
  # Generate ascmd handlers - convention: action_<op>
218
- %i[rm mv cp mkdir].each do |op|
219
- define_action_method([op]) do |command_arguments:, **|
220
- execute_ascmd(op, command_arguments) { Result::Success.new }
228
+ %i[rm mkdir].each do |op|
229
+ define_action_method([op]) do |paths:, **|
230
+ execute_ascmd(op, paths) { Result::Success.new }
231
+ end
232
+ end
233
+
234
+ %i[mv cp].each do |op|
235
+ define_action_method([op]) do |paths:, **|
236
+ execute_ascmd(op, source_destination_pairs(paths)) { Result::Success.new }
221
237
  end
222
238
  end
223
239
 
224
- %i[du md5sum info].each do |op|
225
- define_action_method([op]) do |command_arguments:, **|
226
- execute_ascmd(op, command_arguments) { |r| Result::SingleObject.new(r.stringify_keys) }
240
+ %i[du md5sum].each do |op|
241
+ define_action_method([op]) do |paths:, **|
242
+ execute_ascmd(op, paths) { |r| Result::SingleObject.new(r.stringify_keys) }
227
243
  end
228
244
  end
229
245
 
230
246
  # --- conditions ---
231
247
 
232
248
  def ascmd_available?
233
- !@ascmd_executor.nil?
249
+ !ascmd_executor.nil?
250
+ end
251
+
252
+ # --- server access ---
253
+
254
+ # @return [Hash] base transfer spec, built from CLI options on first use
255
+ def server_transfer_spec
256
+ build_server_access if @server_transfer_spec.nil?
257
+ @server_transfer_spec
234
258
  end
235
259
 
236
- # --- setup ---
260
+ # @return [LocalExecutor, Ssh, nil] ascmd executor, nil with WSS (ascmd not available)
261
+ def ascmd_executor
262
+ build_server_access if @server_transfer_spec.nil?
263
+ @ascmd_executor
264
+ end
237
265
 
238
266
  # Build the transfer spec and ascmd executor from CLI options.
239
- # Stores results in instance variables; returns empty context hash.
240
- def setup_server(**)
267
+ def build_server_access
241
268
  @server_transfer_spec = options_to_base_transfer_spec
242
269
  @ascmd_executor = case @connection_type
243
270
  when :local then LocalExecutor.new
@@ -245,14 +272,13 @@ module Aspera
245
272
  when :ssh then Ssh.new(@server_transfer_spec['remote_host'], @server_transfer_spec['remote_user'], @ssh_opts)
246
273
  else Aspera.error_unexpected_value(@connection_type) { 'connection type' }
247
274
  end
248
- {}
249
275
  end
250
276
 
251
277
  # --- health ---
252
278
 
253
- def action_health_transfer
279
+ def action_health_transfer(**)
254
280
  nagios = Nagios.new
255
- probe_ts = @server_transfer_spec.merge({
281
+ probe_ts = server_transfer_spec.merge({
256
282
  'direction' => 'send',
257
283
  'cookie' => 'aspera.sync', # hide in console
258
284
  'resume_policy' => 'none',
@@ -269,24 +295,23 @@ module Aspera
269
295
 
270
296
  # --- ascmd handlers ---
271
297
 
272
- def action_ls(command_arguments:, **)
273
- execute_ascmd(:ls, command_arguments) do |result|
274
- Result::ObjectList.new(result.map(&:stringify_keys), fields: %w[zmode zuid zgid size mtime name])
275
- end
276
- end
298
+ private
277
299
 
278
- def action_df(command_arguments:, **)
279
- execute_ascmd(:df, command_arguments) do |result|
280
- Result::ObjectList.new(result.map(&:stringify_keys))
281
- end
300
+ # AsCmd `cp` and `mv` take source and destination pairs.
301
+ # With option `to_folder`, paths are only sources, copied or moved into that folder.
302
+ # @param paths [Array<String>] command line arguments
303
+ # @return [Array<String>] flat list of source and destination pairs
304
+ def source_destination_pairs(paths)
305
+ to_folder = options.get_option(:to_folder)
306
+ return paths.flat_map { |source| [source, "#{to_folder.chomp('/')}/#{File.basename(source)}"] } unless to_folder.nil?
307
+ Aspera.assert(paths.length.even?, type: Cli::BadArgument) { 'expecting pairs: <source> <destination> ..., or sources with option to_folder' }
308
+ paths
282
309
  end
283
310
 
284
- private
285
-
286
- def execute_ascmd(op, command_arguments)
287
- ascmd = AsCmd.new(@ascmd_executor)
311
+ def execute_ascmd(op, arguments)
312
+ ascmd = AsCmd.new(ascmd_executor)
288
313
  begin
289
- result = ascmd.execute_single(op, command_arguments)
314
+ result = ascmd.execute_single(op, arguments)
290
315
  yield(result)
291
316
  rescue AsCmd::Error => e
292
317
  raise Cli::BadArgument, e.extended_message