aspera-cli 4.21.1 → 4.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +1 -1
  4. data/CHANGELOG.md +52 -22
  5. data/CONTRIBUTING.md +69 -148
  6. data/README.md +929 -668
  7. data/bin/ascli +5 -14
  8. data/bin/asession +1 -3
  9. data/examples/get_proto_file.rb +4 -3
  10. data/examples/proxy.pac +20 -20
  11. data/lib/aspera/agent/base.rb +11 -5
  12. data/lib/aspera/agent/connect.rb +30 -28
  13. data/lib/aspera/agent/{alpha.rb → desktop.rb} +35 -31
  14. data/lib/aspera/agent/direct.rb +141 -121
  15. data/lib/aspera/agent/httpgw.rb +22 -26
  16. data/lib/aspera/agent/node.rb +14 -11
  17. data/lib/aspera/agent/transferd.rb +30 -19
  18. data/lib/aspera/api/alee.rb +1 -1
  19. data/lib/aspera/api/aoc.rb +6 -6
  20. data/lib/aspera/api/cos_node.rb +2 -2
  21. data/lib/aspera/api/httpgw.rb +7 -3
  22. data/lib/aspera/api/node.rb +10 -8
  23. data/lib/aspera/ascmd.rb +3 -3
  24. data/lib/aspera/ascp/installation.rb +53 -72
  25. data/lib/aspera/ascp/management.rb +1 -1
  26. data/lib/aspera/assert.rb +11 -2
  27. data/lib/aspera/cli/error.rb +2 -2
  28. data/lib/aspera/cli/extended_value.rb +46 -21
  29. data/lib/aspera/cli/formatter.rb +55 -48
  30. data/lib/aspera/cli/hints.rb +1 -1
  31. data/lib/aspera/cli/info.rb +1 -0
  32. data/lib/aspera/cli/main.rb +192 -170
  33. data/lib/aspera/cli/manager.rb +18 -18
  34. data/lib/aspera/cli/plugin.rb +23 -20
  35. data/lib/aspera/cli/plugin_factory.rb +1 -1
  36. data/lib/aspera/cli/plugins/alee.rb +1 -1
  37. data/lib/aspera/cli/plugins/aoc.rb +247 -159
  38. data/lib/aspera/cli/plugins/ats.rb +19 -17
  39. data/lib/aspera/cli/plugins/config.rb +76 -113
  40. data/lib/aspera/cli/plugins/console.rb +5 -3
  41. data/lib/aspera/cli/plugins/faspex.rb +39 -35
  42. data/lib/aspera/cli/plugins/faspex5.rb +111 -84
  43. data/lib/aspera/cli/plugins/faspio.rb +13 -1
  44. data/lib/aspera/cli/plugins/httpgw.rb +13 -1
  45. data/lib/aspera/cli/plugins/node.rb +312 -182
  46. data/lib/aspera/cli/plugins/orchestrator.rb +34 -40
  47. data/lib/aspera/cli/plugins/preview.rb +3 -3
  48. data/lib/aspera/cli/plugins/server.rb +6 -6
  49. data/lib/aspera/cli/plugins/shares.rb +5 -5
  50. data/lib/aspera/cli/sync_actions.rb +19 -18
  51. data/lib/aspera/cli/transfer_agent.rb +5 -5
  52. data/lib/aspera/cli/transfer_progress.rb +2 -2
  53. data/lib/aspera/cli/version.rb +1 -1
  54. data/lib/aspera/command_line_builder.rb +116 -95
  55. data/lib/aspera/coverage.rb +8 -5
  56. data/lib/aspera/environment.rb +26 -17
  57. data/lib/aspera/faspex_gw.rb +14 -14
  58. data/lib/aspera/faspex_postproc.rb +10 -11
  59. data/lib/aspera/hash_ext.rb +4 -14
  60. data/lib/aspera/json_rpc.rb +1 -1
  61. data/lib/aspera/keychain/encrypted_hash.rb +47 -34
  62. data/lib/aspera/keychain/factory.rb +41 -0
  63. data/lib/aspera/keychain/hashicorp_vault.rb +71 -0
  64. data/lib/aspera/keychain/macos_security.rb +19 -11
  65. data/lib/aspera/log.rb +28 -34
  66. data/lib/aspera/nagios.rb +6 -6
  67. data/lib/aspera/node_simulator.rb +8 -8
  68. data/lib/aspera/oauth/base.rb +14 -7
  69. data/lib/aspera/oauth/factory.rb +5 -6
  70. data/lib/aspera/oauth/url_json.rb +6 -6
  71. data/lib/aspera/persistency_action_once.rb +6 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +13 -10
  74. data/lib/aspera/preview/options.rb +16 -16
  75. data/lib/aspera/preview/terminal.rb +4 -4
  76. data/lib/aspera/preview/utils.rb +15 -17
  77. data/lib/aspera/products/connect.rb +35 -1
  78. data/lib/aspera/products/{alpha.rb → desktop.rb} +3 -3
  79. data/lib/aspera/products/transferd.rb +9 -2
  80. data/lib/aspera/proxy_auto_config.rb +2 -2
  81. data/lib/aspera/rest.rb +56 -47
  82. data/lib/aspera/rest_errors_aspera.rb +1 -1
  83. data/lib/aspera/secret_hider.rb +12 -5
  84. data/lib/aspera/ssh.rb +4 -4
  85. data/lib/aspera/temp_file_manager.rb +5 -4
  86. data/lib/aspera/transfer/convert.rb +29 -0
  87. data/lib/aspera/transfer/error_info.rb +66 -66
  88. data/lib/aspera/transfer/parameters.rb +13 -68
  89. data/lib/aspera/transfer/spec.rb +5 -6
  90. data/lib/aspera/transfer/spec.schema.yaml +753 -0
  91. data/lib/aspera/transfer/spec_doc.rb +62 -0
  92. data/lib/aspera/transfer/sync.rb +23 -72
  93. data/lib/aspera/transfer/sync_instance.schema.yaml +13 -0
  94. data/lib/aspera/transfer/sync_session.schema.yaml +79 -0
  95. data/lib/aspera/transfer/uri.rb +6 -6
  96. data/lib/aspera/uri_reader.rb +18 -1
  97. data/lib/aspera/web_auth.rb +1 -1
  98. data/lib/aspera/web_server_simple.rb +53 -44
  99. data.tar.gz.sig +0 -0
  100. metadata +28 -165
  101. metadata.gz.sig +0 -0
  102. data/examples/build_exec +0 -74
  103. data/examples/build_exec_rubyc +0 -40
  104. data/examples/build_package.sh +0 -28
  105. data/lib/aspera/transfer/spec.yaml +0 -718
@@ -64,7 +64,7 @@ module Aspera
64
64
  garbage_files = current_files(persist_category)
65
65
  if !max_age_seconds.nil?
66
66
  current_time = Time.now
67
- garbage_files.select! { |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
67
+ garbage_files.select!{ |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
68
68
  end
69
69
  garbage_files.each do |filepath|
70
70
  File.delete(filepath)
@@ -79,7 +79,7 @@ module Aspera
79
79
  end
80
80
 
81
81
  def current_items(persist_category)
82
- current_files(persist_category).each_with_object({}) {|i, h| h[File.basename(i, FILE_SUFFIX)] = File.read(i)}
82
+ current_files(persist_category).each_with_object({}){ |i, h| h[File.basename(i, FILE_SUFFIX)] = File.read(i)}
83
83
  end
84
84
 
85
85
  private
@@ -4,7 +4,6 @@
4
4
  # spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
5
5
  # spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos unoconv optipng reencode conv transframes
6
6
 
7
- require 'open3'
8
7
  require 'aspera/preview/options'
9
8
  require 'aspera/preview/utils'
10
9
  require 'aspera/preview/file_types'
@@ -24,17 +23,18 @@ module Aspera
24
23
  # one of CONVERSION_TYPES
25
24
  attr_reader :conversion_type
26
25
 
27
- # @param src source file path
28
- # @param dst destination file path
29
- # @param api_mime_type optional mime type as provided by node api (or nil)
30
26
  # node API mime types are from: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
31
- # supported preview type is one of Preview::PREVIEW_FORMATS
32
27
  # the resulting preview file type is taken from destination file extension.
33
28
  # conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>
34
29
  # -> conversion_type is one of FileTypes::CONVERSION_TYPES
35
30
  # -> preview_format is one of Generator::PREVIEW_FORMATS
36
31
  # the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
37
32
  # -> conversion method is one of Generator::VIDEO_CONVERSION_METHODS
33
+ # @param src [String] source file path
34
+ # @param dst [String] destination file path
35
+ # @param options [Options] All conversion options
36
+ # @param main_temp_dir [String] Main temp folder, sub folder will be created for generation
37
+ # @param api_mime_type [String,nil] Optional mime type as provided by node api (or nil)
38
38
  def initialize(src, dst, options, main_temp_dir, api_mime_type)
39
39
  @source_file_path = src
40
40
  @destination_file_path = dst
@@ -54,7 +54,7 @@ module Aspera
54
54
  end
55
55
  @processing_method = @processing_method.to_sym
56
56
  Log.log.debug{"method: #{@processing_method}"}
57
- Aspera.assert(respond_to?(@processing_method, true)){"no processing know for #{conversion_type} -> #{@preview_format_sym}"}
57
+ Aspera.assert(respond_to?(@processing_method, true)){"no processing known for #{conversion_type} -> #{@preview_format_sym}"}
58
58
  end
59
59
 
60
60
  # create preview as specified in constructor
@@ -215,7 +215,8 @@ module Aspera
215
215
 
216
216
  def convert_pdf_to_png(source_file_path=nil)
217
217
  source_file_path ||= @source_file_path
218
- Utils.external_command(:convert, [
218
+ Utils.external_command(:magick, [
219
+ 'convert',
219
220
  '-size', "x#{@options.thumb_img_size}",
220
221
  '-background', 'white',
221
222
  '-flatten',
@@ -224,7 +225,8 @@ module Aspera
224
225
  end
225
226
 
226
227
  def convert_image_to_png
227
- Utils.external_command(:convert, [
228
+ Utils.external_command(:magick, [
229
+ 'convert',
228
230
  '-auto-orient',
229
231
  '-thumbnail', "#{@options.thumb_img_size}x#{@options.thumb_img_size}>",
230
232
  '-quality', 95,
@@ -238,8 +240,9 @@ module Aspera
238
240
  # text to png
239
241
  def convert_plaintext_to_png
240
242
  # get 100 first lines of text file
241
- first_lines = File.open(@source_file_path){|f|Array.new(100){f.readline rescue ''}.join}
242
- Utils.external_command(:convert, [
243
+ first_lines = File.open(@source_file_path){ |f| Array.new(100){f.readline rescue ''}.join}
244
+ Utils.external_command(:magick, [
245
+ 'convert',
243
246
  '-size', "#{@options.thumb_img_size}x#{@options.thumb_img_size}",
244
247
  'xc:white', # define canvas with background color (xc, or canvas) of preceding size
245
248
  '-font', @options.thumb_text_font,
@@ -15,22 +15,22 @@ module Aspera
15
15
  # iw/ih : input width or height
16
16
  # -x : keep aspect ratio, having value a multiple of x
17
17
  DESCRIPTIONS = [
18
- { name: :max_size, default: 1 << 24, description: 'maximum size (in bytes) of preview file' },
19
- { name: :thumb_vid_scale, default: "-1:'min(ih,100)'", description: 'png: video: size (ffmpeg scale argument)' },
20
- { name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot' },
21
- { name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)' },
22
- { name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font to render text with imagemagick convert (identify -list font)'},
23
- { name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS },
24
- { name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS },
25
- { name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg)' },
26
- { name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview' },
27
- { name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg' },
28
- { name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames' },
29
- { name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames' },
30
- { name: :blend_transframes, default: 5, description: 'mp4: blend: # transition blend frames' },
31
- { name: :blend_fps, default: 15, description: 'mp4: blend: frame per second' },
32
- { name: :clips_count, default: 5, description: 'mp4: clips: number of clips' },
33
- { name: :clips_length, default: 5, description: 'mp4: clips: length in seconds of each clips' }
18
+ {name: :max_size, default: 1 << 24, description: 'maximum size (in bytes) of preview file'},
19
+ {name: :thumb_vid_scale, default: "-1:'min(ih,100)'", description: 'png: video: size (ffmpeg scale argument)'},
20
+ {name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot'},
21
+ {name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)'},
22
+ {name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font for text rendering: `magick identify -list font`'},
23
+ {name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS},
24
+ {name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS},
25
+ {name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg scale argument)'},
26
+ {name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview'},
27
+ {name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg'},
28
+ {name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames'},
29
+ {name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames'},
30
+ {name: :blend_transframes, default: 5, description: 'mp4: blend: # transition blend frames'},
31
+ {name: :blend_fps, default: 15, description: 'mp4: blend: frame per second'},
32
+ {name: :clips_count, default: 5, description: 'mp4: clips: number of clips'},
33
+ {name: :clips_length, default: 5, description: 'mp4: clips: length in seconds of each clips'}
34
34
  ].freeze
35
35
  # add accessors
36
36
  DESCRIPTIONS.each do |opt|
@@ -44,13 +44,13 @@ module Aspera
44
44
  fit_term_ratio = [term_rows.to_f * font_ratio / image.rows.to_f, term_columns.to_f / image.columns.to_f].min
45
45
  height_ratio = double ? 2.0 : 1.0
46
46
  image = image.scale((image.columns * fit_term_ratio).to_i, (image.rows * fit_term_ratio * height_ratio / font_ratio).to_i)
47
- # quantum depth is 8 or 16, see: `convert xc: -format "%q" info:`
47
+ # quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
48
48
  shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
49
49
  # get all pixel colors, adjusted for Rainbow
50
50
  pixel_colors = []
51
51
  image.each_pixel do |pixel, col, row|
52
52
  pixel_rgb = [pixel.red, pixel.green, pixel.blue]
53
- pixel_rgb = pixel_rgb.map { |color| color >> shift_for_8_bit } unless shift_for_8_bit.eql?(0)
53
+ pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
54
54
  # init 2-dim array
55
55
  pixel_colors[row] ||= []
56
56
  pixel_colors[row][col] = pixel_rgb
@@ -82,7 +82,7 @@ module Aspera
82
82
  size: blob.length
83
83
  # width: image.columns,
84
84
  # height: image.rows
85
- }.map { |k, v| "#{k}=#{v}" }.join(';')
85
+ }.map{ |k, v| "#{k}=#{v}"}.join(';')
86
86
  # \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
87
87
  # escape sequence for iTerm2 image display
88
88
  return "\e]1337;File=#{arguments}:#{Base64.encode64(blob)}\a"
@@ -91,7 +91,7 @@ module Aspera
91
91
  # @return [Boolean] true if the terminal supports iTerm2 image display
92
92
  def iterm_supported?
93
93
  TERM_ENV_VARS.each do |env_var|
94
- return true if ITERM_NAMES.any? { |term| ENV[env_var]&.include?(term) }
94
+ return true if ITERM_NAMES.any?{ |term| ENV[env_var]&.include?(term)}
95
95
  end
96
96
  false
97
97
  end
@@ -14,16 +14,20 @@ module Aspera
14
14
  # from bash manual: meta-character need to be escaped
15
15
  BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
16
16
  # external binaries used
17
- EXTERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
17
+ EXTERNAL_TOOLS = %i[ffmpeg ffprobe magick optipng unoconv].freeze
18
18
  TEMP_FORMAT = 'img%04d.jpg'
19
+ FFMPEG_DEFAULT_PARAMS = [
20
+ '-y', # overwrite output without asking
21
+ '-loglevel', 'error' # show only errors and up
22
+ ].freeze
19
23
  private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
20
24
 
21
25
  class << self
22
26
  # returns string with single quotes suitable for bash if there is any bash meta-character
23
27
  def shell_quote(argument)
24
- return argument unless argument.chars.any?{|c|BASH_SPECIAL_CHARACTERS.include?(c)}
28
+ return argument unless argument.chars.any?{ |c| BASH_SPECIAL_CHARACTERS.include?(c)}
25
29
  # surround with single quotes, and escape single quotes
26
- return %Q{'#{argument.gsub("'"){|_s| %q{'"'"'}}}'}
30
+ return %Q{'#{argument.gsub("'"){ |_s| %q{'"'"'}}}'}
27
31
  end
28
32
 
29
33
  # check that external tools can be executed
@@ -32,7 +36,7 @@ module Aspera
32
36
  tools_to_check.delete(:unoconv) if skip_types.include?(:office)
33
37
  # Check for binaries
34
38
  tools_to_check.each do |command_sym|
35
- external_command(command_sym, ['-h'])
39
+ external_command(command_sym, ['-h'], out: File::NULL)
36
40
  rescue Errno::ENOENT => e
37
41
  raise "missing #{command_sym} binary: #{e}"
38
42
  rescue
@@ -45,7 +49,7 @@ module Aspera
45
49
  # @return nil
46
50
  def external_command(command_sym, command_args)
47
51
  Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
48
- Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s))
52
+ Environment.secure_execute(exec: command_sym.to_s, args: command_args.map(&:to_s), out: File::NULL, err: File::NULL)
49
53
  end
50
54
 
51
55
  def external_capture(command_sym, command_args)
@@ -53,17 +57,11 @@ module Aspera
53
57
  return Environment.secure_capture(exec: command_sym.to_s, args: command_args.map(&:to_s))
54
58
  end
55
59
 
56
- def ffmpeg(a)
57
- Aspera.assert_type(a, Hash)
58
- # input_file,input_args,output_file,output_args
59
- a[:gl_p] ||= [
60
- '-y', # overwrite output without asking
61
- '-loglevel', 'error' # show only errors and up
62
- ]
63
- a[:in_p] ||= []
64
- a[:out_p] ||= []
65
- Aspera.assert(%i[gl_p in_f in_p out_f out_p].eql?(a.keys.sort)){"wrong params (#{a.keys.sort})"}
66
- external_command(:ffmpeg, [a[:gl_p], a[:in_p], '-i', a[:in_f], a[:out_p], a[:out_f]].flatten)
60
+ def ffmpeg(gl_p: FFMPEG_DEFAULT_PARAMS, in_p: [], in_f:, out_p: [], out_f:)
61
+ Aspera.assert_type(gl_p, Array)
62
+ Aspera.assert_type(in_p, Array)
63
+ Aspera.assert_type(out_p, Array)
64
+ external_command(:ffmpeg, gl_p + in_p + ['-i', in_f] + out_p + [out_f])
67
65
  end
68
66
 
69
67
  # @return Float in seconds
@@ -97,7 +95,7 @@ module Aspera
97
95
  1.upto(count) do |i|
98
96
  percent = i * 100 / (count + 1)
99
97
  filename = get_tmp_num_filepath(temp_folder, index1 + i)
100
- external_command(:composite, ['-blend', percent, img2, img1, filename])
98
+ external_command(:magick, ['composite', '-blend', percent, img2, img1, filename])
101
99
  end
102
100
  end
103
101
 
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/environment'
4
+ require 'singleton'
4
5
 
5
6
  module Aspera
6
7
  module Products
7
8
  class Connect
9
+ include Singleton
8
10
  APP_NAME = 'IBM Aspera Connect'
11
+
9
12
  class << self
10
13
  # standard folder locations
11
14
  def locations
@@ -40,9 +43,40 @@ module Aspera
40
43
  app_root: File.join(Dir.home, '.aspera', 'connect'),
41
44
  run_root: File.join(Dir.home, '.aspera', 'connect')
42
45
  }]
43
- end.map { |i| i.merge({ expected: APP_NAME }) }
46
+ end.map{ |i| i.merge({expected: APP_NAME})}
47
+ end
48
+ end
49
+
50
+ def cdn_api
51
+ Rest.new(base_url: CDN_BASE_URL)
52
+ end
53
+
54
+ # retrieve structure from cloud (CDN) with all versions available
55
+ def versions
56
+ if @connect_versions.nil?
57
+ javascript = cdn_api.call(operation: 'GET', subpath: VERSION_INFO_FILE)
58
+ # get result on one line
59
+ connect_versions_javascript = javascript[:http].body.gsub(/\r?\n\s*/, '')
60
+ Log.log.debug{"javascript=[\n#{connect_versions_javascript}\n]"}
61
+ # get javascript object only
62
+ found = connect_versions_javascript.match(/^.*? = (.*);/)
63
+ raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
64
+ all_data = JSON.parse(found[1])
65
+ @connect_versions = all_data['entries']
44
66
  end
67
+ return @connect_versions
45
68
  end
69
+
70
+ private
71
+
72
+ def initialize
73
+ @connect_versions = nil
74
+ end
75
+
76
+ VERSION_INFO_FILE = 'connectversions.js' # cspell: disable-line
77
+ CDN_BASE_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
78
+
79
+ private_constant :VERSION_INFO_FILE, :CDN_BASE_URL
46
80
  end
47
81
  end
48
82
  end
@@ -4,8 +4,8 @@ require 'aspera/environment'
4
4
 
5
5
  module Aspera
6
6
  module Products
7
- # Aspera Desktop Alpha Client
8
- class Alpha
7
+ # Client Aspera for Desktop
8
+ class Desktop
9
9
  APP_NAME = 'IBM Aspera for Desktop'
10
10
  APP_IDENTIFIER = 'com.ibm.software.aspera.desktop'
11
11
  class << self
@@ -18,7 +18,7 @@ module Aspera
18
18
  sub_bin: File.join('Contents', 'Resources', 'transferd', 'bin')
19
19
  }]
20
20
  else []
21
- end.map { |i| i.merge({ expected: APP_NAME }) }
21
+ end.map{ |i| i.merge({expected: APP_NAME})}
22
22
  end
23
23
 
24
24
  def log_file
@@ -4,13 +4,18 @@ module Aspera
4
4
  module Products
5
5
  class Transferd
6
6
  APP_NAME = 'IBM Aspera Transfer Daemon'
7
+ V1_DAEMON_NAME = 'asperatransferd'
8
+ # from 1.1.5
9
+ V2_DAEMON_NAME = 'transferd'
10
+ # folders to extract from SDK archive
11
+ RUNTIME_FOLDERS = %w[bin lib sbin aspera].freeze
7
12
  class << self
8
13
  # standard folder locations
9
14
  def locations
10
15
  [{
11
16
  app_root: sdk_directory,
12
17
  sub_bin: ''
13
- }].map { |i| i.merge({ expected: APP_NAME }) }
18
+ }].map{ |i| i.merge({expected: APP_NAME})}
14
19
  end
15
20
 
16
21
  # location of SDK files
@@ -28,7 +33,9 @@ module Aspera
28
33
  end
29
34
 
30
35
  def transferd_path
31
- return File.join(sdk_directory, Environment.exe_file('asperatransferd')) # cspell:disable-line
36
+ v1_path = File.join(sdk_directory, Environment.exe_file(V1_DAEMON_NAME))
37
+ return v1_path if File.exist?(v1_path)
38
+ return File.join(sdk_directory, Environment.exe_file(V2_DAEMON_NAME))
32
39
  end
33
40
 
34
41
  # Well, the port number is only in log file
@@ -13,7 +13,7 @@ module URI
13
13
  def register_proxy_finder
14
14
  Aspera.assert(block_given?)
15
15
  # overload the method in URI : call user's provided block and fallback to original method
16
- define_method(:find_proxy) {|env_vars=ENV| yield(to_s) || find_proxy_orig(env_vars)}
16
+ define_method(:find_proxy){ |env_vars=ENV| yield(to_s) || find_proxy_orig(env_vars)}
17
17
  end
18
18
  end
19
19
  end
@@ -70,7 +70,7 @@ END_OF_JAVASCRIPT
70
70
  end
71
71
 
72
72
  def register_uri_generic
73
- URI::Generic.register_proxy_finder{|url_str|get_proxies(url_str).first}
73
+ URI::Generic.register_proxy_finder{ |url_str| get_proxies(url_str).first}
74
74
  # allow chaining
75
75
  return self
76
76
  end
data/lib/aspera/rest.rb CHANGED
@@ -31,15 +31,18 @@ module Aspera
31
31
  class RestParameters
32
32
  include Singleton
33
33
 
34
- attr_accessor :user_agent, :download_partial_suffix, :retry_on_error, :retry_sleep, :session_cb, :progress_bar
34
+ attr_accessor :user_agent, :download_partial_suffix, :retry_on_error, :retry_on_timeout, :retry_on_unavailable, :retry_max, :retry_sleep, :session_cb, :progress_bar
35
35
 
36
36
  private
37
37
 
38
38
  def initialize
39
39
  @user_agent = 'RubyAsperaRest'
40
40
  @download_partial_suffix = '.http_partial'
41
- @retry_on_error = 0
42
- @retry_sleep = nil
41
+ @retry_on_error = false
42
+ @retry_on_timeout = true
43
+ @retry_on_unavailable = true
44
+ @retry_max = 1
45
+ @retry_sleep = 4
43
46
  @session_cb = nil
44
47
  @progress_bar = nil
45
48
  end
@@ -57,12 +60,18 @@ module Aspera
57
60
  # error message when entity not found (TODO: use specific exception)
58
61
  ENTITY_NOT_FOUND = 'No such'
59
62
 
63
+ MIME_JSON = 'application/json'
64
+ MIME_WWW = 'application/x-www-form-urlencoded'
65
+ MIME_TEXT = 'text/plain'
66
+
60
67
  # Content-Type that are JSON
61
- JSON_DECODE = ['application/json', 'application/vnd.api+json', 'application/x-javascript'].freeze
68
+ JSON_DECODE = [MIME_JSON, 'application/vnd.api+json', 'application/x-javascript'].freeze
69
+
70
+ UNAVAILABLE_CODES = ['503']
62
71
 
63
72
  class << self
64
73
  # @return [String] Basic auth token
65
- def basic_token(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
74
+ def basic_authorization(user, pass); return "Basic #{Base64.strict_encode64("#{user}:#{pass}")}"; end
66
75
 
67
76
  # Build a parameter list prefixed with "[]"
68
77
  # @param values [Array] list of values
@@ -98,7 +107,7 @@ module Aspera
98
107
  end
99
108
  end
100
109
  when Array
101
- Aspera.assert(query.all?{|i| i.is_a?(Array) && i.length.eql?(2)}) {'Query must be array of arrays or 2 elements'}
110
+ Aspera.assert(query.all?{ |i| i.is_a?(Array) && i.length.eql?(2)}){'Query must be array of arrays or 2 elements'}
102
111
  query_array = query
103
112
  else
104
113
  raise "Query must be Hash or Array, not #{query.class}"
@@ -171,7 +180,7 @@ module Aspera
171
180
  one[1] = one[1].gsub(/\A"|"\z/, '')
172
181
  one
173
182
  end.to_h
174
- { type: type.downcase, parameters: parameters }
183
+ {type: type.downcase, parameters: parameters}
175
184
  end
176
185
  end
177
186
 
@@ -189,6 +198,7 @@ module Aspera
189
198
 
190
199
  attr_reader :base_url
191
200
  attr_reader :auth_params
201
+ attr_reader :headers
192
202
 
193
203
  # @return creation parameters
194
204
  def params
@@ -238,7 +248,7 @@ module Aspera
238
248
  @http_session = nil
239
249
  @redirect_max = redirect_max
240
250
  Aspera.assert_type(@redirect_max, Integer)
241
- @headers = headers
251
+ @headers = headers.clone
242
252
  Aspera.assert_type(@headers, Hash)
243
253
  @headers['User-Agent'] ||= RestParameters.instance.user_agent
244
254
  # OAuth object (created on demand)
@@ -249,7 +259,7 @@ module Aspera
249
259
  def oauth
250
260
  if @oauth.nil?
251
261
  Aspera.assert(@auth_params[:type].eql?(:oauth2)){'no OAuth defined'}
252
- oauth_parameters = @auth_params.reject { |k, _v| k.eql?(:type) }
262
+ oauth_parameters = @auth_params.reject{ |k, _v| k.eql?(:type)}
253
263
  Log.log.debug{Log.dump('oauth parameters', oauth_parameters)}
254
264
  @oauth = OAuth::Factory.instance.create(**oauth_parameters)
255
265
  end
@@ -260,20 +270,20 @@ module Aspera
260
270
  # @param operation [String] HTTP operation (GET, POST, PUT, DELETE)
261
271
  # @param subpath [String] subpath of REST API
262
272
  # @param query [Hash] URL parameters
273
+ # @param content_type [String,nil] Type of body parameters (one of MIME_*) and serialization, else use headers
263
274
  # @param body [Hash, String] body parameters
264
- # @param body_type [Symbol] type of body parameters (:json, :www, :text, nil)
275
+ # @param headers [Hash] additional headers (override Content-Type)
265
276
  # @param save_to_file (filepath)
266
277
  # @param return_error (bool)
267
- # @param headers [Hash] additional headers
268
278
  def call(
269
279
  operation:,
270
280
  subpath: nil,
271
281
  query: nil,
282
+ content_type: nil,
272
283
  body: nil,
273
- body_type: nil,
284
+ headers: nil,
274
285
  save_to_file: nil,
275
- return_error: false,
276
- headers: nil
286
+ return_error: false
277
287
  )
278
288
  subpath = subpath.to_s if subpath.is_a?(Symbol)
279
289
  subpath = '' if subpath.nil?
@@ -295,7 +305,7 @@ module Aspera
295
305
  Log.log.debug('using Basic auth')
296
306
  # done in build_req
297
307
  when :oauth2
298
- headers['Authorization'] = oauth.token unless headers.key?('Authorization')
308
+ headers['Authorization'] = oauth.authorization unless headers.key?('Authorization')
299
309
  when :url
300
310
  query ||= {}
301
311
  @auth_params[:url_query].each do |key, value|
@@ -317,19 +327,18 @@ module Aspera
317
327
  rescue NameError
318
328
  raise "unsupported operation : #{operation}"
319
329
  end
320
- case body_type
321
- when :json
330
+ case content_type
331
+ when nil # ignore
332
+ when MIME_JSON
322
333
  req.body = JSON.generate(body) # , ascii_only: true
323
- req['Content-Type'] = 'application/json'
324
- when :www
334
+ req['Content-Type'] = MIME_JSON
335
+ when MIME_WWW
325
336
  req.body = URI.encode_www_form(body)
326
- req['Content-Type'] = 'application/x-www-form-urlencoded'
327
- when :text
337
+ req['Content-Type'] = MIME_WWW
338
+ when MIME_TEXT
328
339
  req.body = body
329
- req['Content-Type'] = 'text/plain'
330
- when nil
331
- else
332
- raise "unsupported body type : #{body_type}"
340
+ req['Content-Type'] = MIME_TEXT
341
+ else Aspera.error_unexpected_value(content_type){'body type'}
333
342
  end
334
343
  # set headers
335
344
  headers.each do |key, value|
@@ -338,10 +347,8 @@ module Aspera
338
347
  # :type = :basic
339
348
  req.basic_auth(@auth_params[:username], @auth_params[:password]) if @auth_params[:type].eql?(:basic)
340
349
  Log.log.trace1{Log.dump(:req_body, req.body)}
341
- # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
342
- oauth_tries ||= 2
343
- timeout_tries ||= 5
344
- general_tries ||= 1 + RestParameters.instance.retry_on_error
350
+ # we try the call, and will retry on some error types
351
+ error_tries ||= 1 + RestParameters.instance.retry_max
345
352
  # initialize with number of initial retries allowed, nil gives zero
346
353
  tries_remain_redirect = @redirect_max if tries_remain_redirect.nil?
347
354
  Log.log.debug("send request (retries=#{tries_remain_redirect})")
@@ -350,7 +357,7 @@ module Aspera
350
357
  # make http request (pipelined)
351
358
  http_session.request(req) do |response|
352
359
  result[:http] = response
353
- result_mime = self.class.parse_header(result[:http]['Content-Type'] || 'text/plain')[:type]
360
+ result_mime = self.class.parse_header(result[:http]['Content-Type'] || MIME_TEXT)[:type]
354
361
  # JSON data needs to be parsed, in case it contains an error code
355
362
  if !save_to_file.nil? &&
356
363
  result[:http].code.to_s.start_with?('2') &&
@@ -394,7 +401,7 @@ module Aspera
394
401
  when *JSON_DECODE
395
402
  result[:data] = JSON.parse(result[:http].body) rescue result[:http].body
396
403
  Log.log.debug{Log.dump('result_data', result[:data])}
397
- else # when 'text/plain'
404
+ else # when MIME_TEXT
398
405
  result[:data] = result[:http].body
399
406
  end
400
407
  RestErrorAnalyzer.instance.raise_on_error(req, result)
@@ -402,25 +409,27 @@ module Aspera
402
409
  rescue RestCallError => e
403
410
  do_retry = false
404
411
  # AoC have some timeout , like Connect to platform.bss.asperasoft.com:443 ...
405
- do_retry = true if e.response.body.include?('failed: connect timed out') && (timeout_tries -= 1).positive?
412
+ do_retry ||= true if e.response.body.include?('failed: connect timed out') && RestParameters.instance.retry_on_timeout
413
+ # AoC sometimes not available
414
+ do_retry ||= true if RestParameters.instance.retry_on_unavailable && UNAVAILABLE_CODES.include?(result[:http].code.to_s)
406
415
  # possibility to retry anything if it fails
407
- do_retry = true if (general_tries -= 1).positive?
416
+ do_retry ||= true if RestParameters.instance.retry_on_error
408
417
  # not authorized: oauth token expired
409
418
  if @not_auth_codes.include?(result[:http].code.to_s) && @auth_params[:type].eql?(:oauth2)
410
419
  begin
411
420
  # try to use refresh token
412
- req['Authorization'] = oauth.token(refresh: true)
421
+ req['Authorization'] = oauth.authorization(refresh: true)
413
422
  rescue RestCallError => e_tok
414
423
  e = e_tok
415
424
  Log.log.error('refresh failed'.bg_red)
416
425
  # regenerate a brand new token
417
- req['Authorization'] = oauth.token(refresh: true)
426
+ req['Authorization'] = oauth.authorization(cache: false)
418
427
  end
419
428
  Log.log.debug{"using new token=#{headers['Authorization']}"}
420
- do_retry = true if (oauth_tries -= 1).positive?
429
+ do_retry ||= true
421
430
  end
422
- if do_retry
423
- sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.nil?
431
+ if do_retry && (error_tries -= 1).positive?
432
+ sleep(RestParameters.instance.retry_sleep) unless RestParameters.instance.retry_sleep.eql?(0)
424
433
  retry
425
434
  end
426
435
  # redirect ? (any code beginning with 3)
@@ -436,7 +445,7 @@ module Aspera
436
445
  end
437
446
  # forwards the request to the new location
438
447
  return self.class.new(base_url: new_url, redirect_max: tries_remain_redirect).call(
439
- operation: operation, query: query, body: body, body_type: body_type,
448
+ operation: operation, query: query, body: body, content_type: content_type,
440
449
  save_to_file: save_to_file, return_error: return_error, headers: headers)
441
450
  end
442
451
  # raise exception if could not retry and not return error in result
@@ -452,23 +461,23 @@ module Aspera
452
461
  #
453
462
 
454
463
  def create(subpath, params)
455
- return call(operation: 'POST', subpath: subpath, headers: {'Accept' => 'application/json'}, body: params, body_type: :json)[:data]
464
+ return call(operation: 'POST', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
456
465
  end
457
466
 
458
467
  def read(subpath, query=nil)
459
- return call(operation: 'GET', subpath: subpath, headers: {'Accept' => 'application/json'}, query: query)[:data]
468
+ return call(operation: 'GET', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: query)[:data]
460
469
  end
461
470
 
462
471
  def update(subpath, params)
463
- return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => 'application/json'}, body: params, body_type: :json)[:data]
472
+ return call(operation: 'PUT', subpath: subpath, headers: {'Accept' => MIME_JSON}, body: params, content_type: MIME_JSON)[:data]
464
473
  end
465
474
 
466
475
  def delete(subpath, params=nil)
467
- return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => 'application/json'}, query: params)[:data]
476
+ return call(operation: 'DELETE', subpath: subpath, headers: {'Accept' => MIME_JSON}, query: params)[:data]
468
477
  end
469
478
 
470
479
  def cancel(subpath)
471
- return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => 'application/json'})[:data]
480
+ return call(operation: 'CANCEL', subpath: subpath, headers: {'Accept' => MIME_JSON})[:data]
472
481
  end
473
482
 
474
483
  # Query entity by general search (read with parameter `q`)
@@ -490,11 +499,11 @@ module Aspera
490
499
  else
491
500
  # multiple case insensitive partial matches, try case insensitive full match
492
501
  # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
493
- name_matches = matching_items.select{|i|i['name'].casecmp?(search_name)}
502
+ name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
494
503
  case name_matches.length
495
504
  when 1 then return name_matches.first
496
- 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
497
- else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{|i|i['name']}}"
505
+ 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
506
+ else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
498
507
  end
499
508
  end
500
509
  end
@@ -35,7 +35,7 @@ module Aspera
35
35
  d_t_s.each do |res|
36
36
  r_err = res.dig(*%w[transfer_spec error]) || res['error']
37
37
  next unless r_err.is_a?(Hash)
38
- RestErrorAnalyzer.add_error(call_context, type, "#{r_err['code']}: #{r_err['reason']}: #{r_err['user_message']}")
38
+ RestErrorAnalyzer.add_error(call_context, type, r_err.values.join(': '))
39
39
  end
40
40
  end
41
41
  RestErrorAnalyzer.instance.add_simple_handler(name: 'T9:IBM cloud IAM', path: ['errorMessage'])