aspera-cli 4.25.6 → 4.26.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +89 -48
  4. data/CONTRIBUTING.md +1 -1
  5. data/lib/aspera/api/aoc.rb +120 -79
  6. data/lib/aspera/api/node.rb +103 -51
  7. data/lib/aspera/ascp/installation.rb +99 -32
  8. data/lib/aspera/assert.rb +17 -13
  9. data/lib/aspera/cli/extended_value.rb +7 -2
  10. data/lib/aspera/cli/formatter.rb +107 -95
  11. data/lib/aspera/cli/main.rb +69 -10
  12. data/lib/aspera/cli/manager.rb +158 -78
  13. data/lib/aspera/cli/options.schema.yaml +82 -0
  14. data/lib/aspera/cli/plugins/aoc.rb +247 -144
  15. data/lib/aspera/cli/plugins/ats.rb +3 -3
  16. data/lib/aspera/cli/plugins/base.rb +60 -76
  17. data/lib/aspera/cli/plugins/config.rb +14 -12
  18. data/lib/aspera/cli/plugins/console.rb +3 -3
  19. data/lib/aspera/cli/plugins/faspex.rb +6 -6
  20. data/lib/aspera/cli/plugins/faspex5.rb +24 -23
  21. data/lib/aspera/cli/plugins/node.rb +67 -71
  22. data/lib/aspera/cli/plugins/oauth.rb +5 -12
  23. data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
  24. data/lib/aspera/cli/plugins/preview.rb +116 -80
  25. data/lib/aspera/cli/plugins/server.rb +2 -10
  26. data/lib/aspera/cli/plugins/shares.rb +7 -7
  27. data/lib/aspera/cli/sync_actions.rb +1 -1
  28. data/lib/aspera/cli/transfer_agent.rb +17 -15
  29. data/lib/aspera/cli/version.rb +1 -1
  30. data/lib/aspera/command_line_builder.rb +22 -18
  31. data/lib/aspera/dot_container.rb +7 -3
  32. data/lib/aspera/environment.rb +6 -5
  33. data/lib/aspera/formatter_interface.rb +14 -0
  34. data/lib/aspera/hash_ext.rb +6 -0
  35. data/lib/aspera/log.rb +5 -4
  36. data/lib/aspera/markdown.rb +4 -1
  37. data/lib/aspera/oauth/factory.rb +1 -1
  38. data/lib/aspera/preview/file_types.rb +1 -1
  39. data/lib/aspera/preview/generator.rb +146 -91
  40. data/lib/aspera/preview/options.rb +4 -1
  41. data/lib/aspera/preview/terminal.rb +50 -20
  42. data/lib/aspera/preview/utils.rb +76 -34
  43. data/lib/aspera/products/transferd.rb +1 -1
  44. data/lib/aspera/proxy_auto_config.rb +3 -0
  45. data/lib/aspera/rest.rb +2 -1
  46. data/lib/aspera/rest_list.rb +23 -16
  47. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
  48. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
  49. data/lib/aspera/schema/documentation.rb +107 -0
  50. data/lib/aspera/schema/reader.rb +75 -0
  51. data/lib/aspera/schema/registry.rb +63 -0
  52. data/lib/aspera/secret_hider.rb +3 -1
  53. data/lib/aspera/sync/conf.schema.yaml +0 -26
  54. data/lib/aspera/sync/operations.rb +9 -5
  55. data/lib/aspera/transfer/faux_file.rb +1 -1
  56. data/lib/aspera/transfer/resumer.rb +1 -1
  57. data/lib/aspera/transfer/spec.rb +3 -3
  58. data/lib/aspera/transfer/spec.schema.yaml +1 -1
  59. data/lib/aspera/uri_reader.rb +17 -2
  60. data/lib/aspera/yaml.rb +4 -2
  61. data.tar.gz.sig +0 -0
  62. metadata +13 -7
  63. metadata.gz.sig +0 -0
  64. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -10,6 +10,8 @@ module Aspera
10
10
  # types of generation for video files
11
11
  VIDEO_CONVERSION_METHODS = %i[reencode blend clips].freeze
12
12
  VIDEO_THUMBNAIL_METHODS = %i[fixed animated].freeze
13
+ # methods for office document conversion
14
+ OFFICE_CONVERSION_METHODS = %i[soffice unoconv].freeze
13
15
  # options used in generator
14
16
  # for scaling see: https://trac.ffmpeg.org/wiki/Scaling
15
17
  # iw/ih : input width or height
@@ -20,11 +22,12 @@ module Aspera
20
22
  {name: :thumb_vid_fraction, default: 0.1, description: 'png: video: time percent position of snapshot'},
21
23
  {name: :thumb_img_size, default: 800, description: 'png: non-video: height (and width)'},
22
24
  {name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font for text rendering: `magick identify -list font`'},
25
+ {name: :office_conversion, default: :soffice, description: 'office: method for office document conversion', values: OFFICE_CONVERSION_METHODS},
23
26
  {name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS},
24
27
  {name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS},
25
28
  {name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg scale argument)'},
26
29
  {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'},
30
+ {name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg, keys: `in`, `out`'},
28
31
  {name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames'},
29
32
  {name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames'},
30
33
  {name: :blend_transframes, default: 5, description: 'mp4: blend: # transition blend frames'},
@@ -11,15 +11,22 @@ require 'aspera/environment'
11
11
  module Aspera
12
12
  module Preview
13
13
  module Backend
14
- # provides image pixels scaled to terminal
14
+ # Base decoder that rescales image data to the current terminal geometry.
15
15
  class Base
16
+ # @param reserve [Integer] number of terminal rows reserved for non-image output
17
+ # @param double [Boolean] when `true`, render two image rows in one terminal row
18
+ # @param font_ratio [Float] terminal font aspect ratio: height divided by width
16
19
  def initialize(reserve:, double:, font_ratio:)
17
20
  @reserve = reserve
18
21
  @height_ratio = double ? 2.0 : 1.0
19
22
  @font_ratio = font_ratio
20
23
  end
21
24
  Aspera.require_method!(:terminal_pixels)
22
- # compute scaling to fit terminal
25
+ # Compute output dimensions that fit inside the terminal while preserving aspect ratio.
26
+ #
27
+ # @param rows [Integer] source image height in pixels
28
+ # @param columns [Integer] source image width in pixels
29
+ # @return [Array<Integer>] scaled width and height for terminal rendering
23
30
  def terminal_scaling(rows, columns)
24
31
  (term_rows, term_columns) = IO.console.winsize || [24, 80]
25
32
  term_rows = [term_rows - @reserve, 2].max
@@ -29,22 +36,30 @@ module Aspera
29
36
  end
30
37
 
31
38
  class RMagick < Base
39
+ # Initialize the RMagick-backed decoder for a binary image payload.
40
+ #
41
+ # @param blob [String] encoded image binary content
42
+ # @param kwargs [Hash] forwarding options accepted by [`initialize`](lib/aspera/preview/terminal.rb:16)
32
43
  def initialize(blob, **kwargs)
33
44
  super(**kwargs)
34
- # do not require statically, as the package is optional
45
+ # Load lazily because this dependency is optional.
35
46
  require 'rmagick' # https://rmagick.github.io/index.html
36
47
  @image = Magick::ImageList.new.from_blob(blob)
37
48
  end
38
49
 
50
+ # Decode the image and return RGB pixels scaled for terminal rendering.
51
+ #
52
+ # @return [Array<Array<Array<Integer>>>] rows of `[red, green, blue]` pixel triplets
39
53
  def terminal_pixels
40
- # quantum depth is 8 or 16, see: `magick xc: -format "%q" info:`
54
+ # ImageMagick channel depth is typically 8 or 16 bits.
55
+ # See: `magick xc: -format "%q" info:`
41
56
  shift_for_8_bit = Magick::MAGICKCORE_QUANTUM_DEPTH - 8
42
- # get all pixel colors, adjusted for Rainbow
57
+ # Extract RGB values and normalize them to 8-bit channels for Rainbow.
43
58
  pixel_colors = []
44
59
  @image.scale(*terminal_scaling(@image.rows, @image.columns)).each_pixel do |pixel, col, row|
45
60
  pixel_rgb = [pixel.red, pixel.green, pixel.blue]
46
61
  pixel_rgb = pixel_rgb.map{ |color| color >> shift_for_8_bit} unless shift_for_8_bit.eql?(0)
47
- # init 2-dim array
62
+ # Initialize the destination 2D pixel matrix row by row.
48
63
  pixel_colors[row] ||= []
49
64
  pixel_colors[row][col] = pixel_rgb
50
65
  end
@@ -53,12 +68,19 @@ module Aspera
53
68
  end
54
69
 
55
70
  class ChunkyPNG < Base
71
+ # Initialize the ChunkyPNG-backed decoder for a PNG payload.
72
+ #
73
+ # @param blob [String] PNG binary content
74
+ # @param kwargs [Hash] forwarding options accepted by [`initialize`](lib/aspera/preview/terminal.rb:16)
56
75
  def initialize(blob, **kwargs)
57
76
  super(**kwargs)
58
77
  require 'chunky_png'
59
78
  @png = ::ChunkyPNG::Image.from_blob(blob)
60
79
  end
61
80
 
81
+ # Resize the PNG using nearest-neighbor sampling and return RGB pixel rows.
82
+ #
83
+ # @return [Array<Array<Array<Integer>>>] rows of `[red, green, blue]` pixel triplets
62
84
  def terminal_pixels
63
85
  src_w = @png.width
64
86
  src_h = @png.height
@@ -75,7 +97,7 @@ module Aspera
75
97
  sx = (dx * x_ratio).floor
76
98
  sx = src_w - 1 if sx >= src_w
77
99
  rgba = @png.get_pixel(sx, sy)
78
- # ChunkyPNG stores as 0xRRGGBBAA; extract 8-bit channels
100
+ # ChunkyPNG stores pixels as 0xRRGGBBAA; extract 8-bit RGB channels.
79
101
  pixel_colors[dy][dx] = %i[r g b].map{ |i| ::ChunkyPNG::Color.send(i, rgba)}
80
102
  end
81
103
  end
@@ -84,19 +106,21 @@ module Aspera
84
106
  end
85
107
  end
86
108
 
87
- # Display a picture in the terminal.
88
- # Either use coloured characters or iTerm2 protocol.
109
+ # Render an image for terminal output.
110
+ # Uses either colored text blocks or the iTerm2 inline-image protocol when available.
89
111
  class Terminal
90
- # Rainbow only supports 8-bit colors
91
- # env vars to detect terminal type
112
+ # Rainbow only supports 8-bit color values.
113
+ # Environment variables inspected to detect compatible terminal implementations.
92
114
  TERM_ENV_VARS = %w[TERM_PROGRAM LC_TERMINAL].freeze
93
- # terminal names that support iTerm2 image display
115
+ # Terminal identifiers known to support the iTerm2 inline-image protocol.
94
116
  ITERM_NAMES = %w[iTerm WezTerm mintty].freeze
95
- # TODO: retrieve terminal font ratio using some termcap ?
96
- # ratio = font height / font width
117
+ # Fallback font aspect ratio used to estimate how many image pixels fit in a character cell.
118
+ # Ratio = font height / font width.
97
119
  DEFAULT_FONT_RATIO = 32.0 / 14.0
98
120
  private_constant :TERM_ENV_VARS, :ITERM_NAMES, :DEFAULT_FONT_RATIO
99
121
  class << self
122
+ # Render an image blob for display in the current terminal.
123
+ #
100
124
  # @param blob [String] The image as a binary string
101
125
  # @param text [Boolean] `true` to display the image as text, `false` to use iTerm2 if supported
102
126
  # @param reserve [Integer] Number of lines to reserve for other text than the image
@@ -124,7 +148,7 @@ module Aspera
124
148
  return iterm_display_image(blob) if iterm_supported?
125
149
  raise 'Cannot decode picture.'
126
150
  end
127
- # now generate text
151
+ # Convert decoded pixels into terminal glyphs.
128
152
  text_pixels = []
129
153
  pixel_colors.each_with_index do |row_data, row|
130
154
  next if double && (row.odd? || row.eql?(pixel_colors.length - 1))
@@ -140,11 +164,14 @@ module Aspera
140
164
  return text_pixels.join
141
165
  end
142
166
 
143
- # display image in iTerm2
167
+ # Build the iTerm2 inline-image escape sequence.
144
168
  # https://iterm2.com/documentation-images.html
169
+ #
170
+ # @param blob [String] image binary content
171
+ # @return [String] escape sequence that displays the image inline
145
172
  def iterm_display_image(blob)
146
173
  # image = Magick::ImageList.new.from_blob(blob)
147
- # parameters for iTerm2 image display
174
+ # Parameters accepted by the iTerm2 inline-image protocol.
148
175
  arguments = {
149
176
  inline: 1,
150
177
  preserveAspectRatio: 1,
@@ -152,12 +179,15 @@ module Aspera
152
179
  # width: image.columns,
153
180
  # height: image.rows
154
181
  }.map{ |k, v| "#{k}=#{v}"}.join(';')
155
- # \a is BEL, \e is ESC : https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
156
- # escape sequence for iTerm2 image display
182
+ # `\a` is BEL and `\e` is ESC.
183
+ # See: https://github.com/ruby/ruby/blob/master/doc/syntax/literals.rdoc#label-Strings
184
+ # Return the full escape sequence expected by iTerm2-compatible terminals.
157
185
  return "\e]1337;File=#{arguments}:#{Base64.strict_encode64(blob)}\a"
158
186
  end
159
187
 
160
- # @return [Boolean] true if the terminal supports iTerm2 image display
188
+ # Detect whether the current terminal supports iTerm2 inline images.
189
+ #
190
+ # @return [Boolean] `true` when the current terminal advertises iTerm2 image support
161
191
  def iterm_supported?
162
192
  TERM_ENV_VARS.each do |env_var|
163
193
  return true if ITERM_NAMES.any?{ |term| ENV[env_var]&.include?(term)}
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # cspell:ignore ffprobe optipng unoconv
3
+ # cspell:ignore ffprobe optipng unoconv soffice
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'English'
@@ -11,32 +11,30 @@ require 'open3'
11
11
  module Aspera
12
12
  module Preview
13
13
  class Utils
14
- # from bash manual: meta-character need to be escaped
15
- BASH_SPECIAL_CHARACTERS = "|&;()<> \t#\n"
16
- # external binaries used
17
- EXTERNAL_TOOLS = %i[ffmpeg ffprobe magick optipng unoconv].freeze
14
+ # External binaries used
15
+ EXTERNAL_TOOLS = %i[ffmpeg ffprobe magick optipng unoconv soffice].freeze
16
+ # File name format for temporary files, used by both ffmpeg and ruby(Kernel.format)
18
17
  TEMP_FORMAT = 'img%04d.jpg'
18
+ # default parameters for ffmpeg
19
19
  FFMPEG_DEFAULT_PARAMS = [
20
20
  '-y', # overwrite output without asking
21
21
  '-loglevel', 'error' # show only errors and up
22
22
  ].freeze
23
- private_constant :BASH_SPECIAL_CHARACTERS, :EXTERNAL_TOOLS, :TEMP_FORMAT
23
+ private_constant :EXTERNAL_TOOLS, :TEMP_FORMAT, :FFMPEG_DEFAULT_PARAMS
24
24
 
25
25
  class << self
26
- # returns string with single quotes suitable for bash if there is any bash meta-character
27
- def shell_quote(argument)
28
- return argument unless argument.chars.any?{ |c| BASH_SPECIAL_CHARACTERS.include?(c)}
29
- # surround with single quotes, and escape single quotes
30
- return %Q{'#{argument.gsub("'"){ |_s| %q{'"'"'}}}'}
31
- end
32
-
33
- # check that external tools can be executed
26
+ # Check that external tools can be executed
27
+ # @param skip_types [Array<Symbol>] list of tools to skip
28
+ # @return [nil]
34
29
  def check_tools(skip_types = [])
35
30
  tools_to_check = EXTERNAL_TOOLS.dup
36
- tools_to_check.delete(:unoconv) if skip_types.include?(:office)
31
+ if skip_types.include?(:office)
32
+ tools_to_check.delete(:unoconv)
33
+ tools_to_check.delete(:soffice)
34
+ end
37
35
  # Check for binaries
38
36
  tools_to_check.each do |command_sym|
39
- external_command(command_sym, ['-h'], out: File::NULL)
37
+ silent_execute(command_sym, '-h')
40
38
  rescue Errno::ENOENT => e
41
39
  raise "missing #{command_sym} binary: #{e}"
42
40
  rescue
@@ -44,42 +42,47 @@ module Aspera
44
42
  end
45
43
  end
46
44
 
47
- # Execute external command
48
- # @return [nil]
49
- def external_command(command_sym, command_args)
50
- Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
51
- Environment.secure_execute(command_sym.to_s, *command_args.map(&:to_s), out: File::NULL, err: File::NULL)
45
+ # Execute external command, verify it is in the supported list
46
+ def execute(*args, **kwargs)
47
+ Aspera.assert_values(args.first, EXTERNAL_TOOLS){'command'}
48
+ Environment.secure_execute(*args, **kwargs)
52
49
  end
53
50
 
54
- # Execute external command and get stdout
55
- # @return [String]
56
- def external_capture(command_sym, command_args)
57
- Aspera.assert_values(command_sym, EXTERNAL_TOOLS){'command'}
58
- Environment.secure_execute(command_sym.to_s, *command_args.map(&:to_s), mode: :capture).first
51
+ # Execute external command silently
52
+ # @return [nil]
53
+ def silent_execute(*args)
54
+ execute(*args, out: File::NULL, err: File::NULL)
55
+ nil
59
56
  end
60
57
 
58
+ # Execute `ffmpeg`
59
+ # @return [nil]
61
60
  def ffmpeg(gl_p: FFMPEG_DEFAULT_PARAMS, in_p: [], in_f:, out_p: [], out_f:)
62
61
  Aspera.assert_type(gl_p, Array)
63
62
  Aspera.assert_type(in_p, Array)
64
63
  Aspera.assert_type(out_p, Array)
65
- external_command(:ffmpeg, gl_p + in_p + ['-i', in_f] + out_p + [out_f])
64
+ silent_execute(:ffmpeg, *gl_p, *in_p, '-i', in_f, *out_p, out_f)
66
65
  end
67
66
 
68
67
  # @return Float in seconds
69
68
  def video_get_duration(input_file)
70
- return external_capture(:ffprobe, [
69
+ return execute(
70
+ :ffprobe,
71
71
  '-loglevel', 'error',
72
72
  '-show_entries', 'format=duration',
73
73
  '-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
74
- input_file
75
- ]).to_f
74
+ input_file,
75
+ mode: :capture
76
+ ).first.to_f
76
77
  end
77
78
 
79
+ # File output format, including temp folder
78
80
  def ffmpeg_fmt(temp_folder)
79
81
  return File.join(temp_folder, TEMP_FORMAT)
80
82
  end
81
83
 
82
84
  def get_tmp_num_filepath(temp_folder, file_number)
85
+ # Format using {Kernel.format}
83
86
  return File.join(temp_folder, format(TEMP_FORMAT, file_number))
84
87
  end
85
88
 
@@ -97,19 +100,58 @@ module Aspera
97
100
  1.upto(count) do |i|
98
101
  percent = i * 100 / (count + 1)
99
102
  filename = get_tmp_num_filepath(temp_folder, index_begin + i)
100
- external_command(:magick, ['composite', '-blend', percent, img2, img1, filename])
103
+ silent_execute(:magick, 'composite', '-blend', percent, img2, img1, filename)
101
104
  end
102
105
  end
103
106
 
104
- def video_dump_frame(input_file, offset_seconds, scale, output_file, index = nil)
105
- output_file = get_tmp_num_filepath(output_file, index) unless index.nil?
107
+ # Dump a frame from a video file
108
+ # @param input_file [String] the input file path
109
+ # @param offset_seconds [Integer] the offset in seconds
110
+ # @param scale [String] the scale of the output frame
111
+ # @param output_file [String] the output file path
112
+ # @return [nil]
113
+ def video_dump_frame(input_file, offset_seconds, scale, output_file)
106
114
  ffmpeg(
107
115
  in_f: input_file,
108
116
  in_p: ['-ss', offset_seconds],
109
117
  out_f: output_file,
110
118
  out_p: ['-frames:v', 1, '-filter:v', "scale=#{scale}"]
111
119
  )
112
- return output_file
120
+ end
121
+
122
+ # Parse the output of `magick identify -list font` command
123
+ # @param output [String] the output from `magick -list font`
124
+ # @return [Hash] with keys :path and :fonts
125
+ # :path [String] the path to the type.xml file
126
+ # :fonts [Array<Hash>] array of font hashes with keys:
127
+ # :name, :family, :style, :stretch, :weight, :metrics, :glyphs, :index
128
+ def parse_magick_fonts(output)
129
+ result = {path: nil, fonts: []}
130
+ current_font = nil
131
+ output.each_line do |line|
132
+ line = line.strip
133
+ # Parse the Path line
134
+ if line.start_with?('Path:')
135
+ result[:path] = line.sub(/^Path:\s*/, '')
136
+ # Parse Font name
137
+ elsif line.start_with?('Font:')
138
+ # Save previous font if exists
139
+ result[:fonts] << current_font if current_font
140
+ # Start new font
141
+ current_font = {name: line.sub(/^Font:\s*/, '')}
142
+ # Parse font properties
143
+ elsif current_font && line.include?(':')
144
+ key, value = line.split(':', 2)
145
+ key = key.strip.gsub(/\s+/, '_').to_sym
146
+ value = value.strip
147
+ # Convert numeric values
148
+ value = value.to_i if key == :weight || key == :index
149
+ current_font[key] = value
150
+ end
151
+ end
152
+ # Don't forget the last font
153
+ result[:fonts] << current_font if current_font
154
+ result
113
155
  end
114
156
  end
115
157
  end
@@ -27,7 +27,7 @@ module Aspera
27
27
  sdk_directory
28
28
  end
29
29
 
30
- # @return the path to folder where SDK is installed
30
+ # @return the path to folder where SDK is or should be installed
31
31
  def sdk_directory
32
32
  Aspera.assert(!@sdk_dir.nil?){'SDK path was not initialized'}
33
33
  @sdk_dir
@@ -10,6 +10,9 @@ module URI
10
10
  # save original method that finds proxy in URI::Generic, it uses env var http_proxy
11
11
  alias_method :find_proxy_orig, :find_proxy
12
12
  class << self
13
+ # Register a custom proxy finder block
14
+ # @yieldparam url [String] The URL to find proxy for
15
+ # @yieldreturn [String, nil] Proxy URL or nil to fallback to original method
13
16
  def register_proxy_finder
14
17
  Aspera.assert(block_given?)
15
18
  # overload the method in URI : call user's provided block and fallback to original method
data/lib/aspera/rest.rb CHANGED
@@ -78,6 +78,7 @@ module Aspera
78
78
 
79
79
  # Indicate that the given Hash query uses php style for array parameters
80
80
  # @param query [Hash] A key can have Array value and result will use PHP format: a[]=1&a[]=2
81
+ # @return [Hash] The query parameters.
81
82
  def php_style(query)
82
83
  Aspera.assert_type(query, Hash){'query'}
83
84
  query[:x_array_php_style] = true
@@ -165,7 +166,7 @@ module Aspera
165
166
  next unless url_match
166
167
  url = url_match[1]
167
168
  # Extract parameters after the URL
168
- params_str = link_part[url_match.end(0)..-1]
169
+ params_str = link_part[url_match.end(0)..]
169
170
  # Check if this link has the specified rel (with or without quotes, case insensitive)
170
171
  next unless /;\s*rel\s*=\s*"?#{Regexp.escape(rel)}"?/i.match?(params_str)
171
172
  return url
@@ -11,33 +11,39 @@ module Aspera
11
11
 
12
12
  # Query entity by general search (read with parameter `q`)
13
13
  #
14
- # @param subpath [String] Path of entity in API
15
- # @param search_name [String] Name of searched entity
16
- # @param query [Hash] Optional additional search query parameters
14
+ # @param entity [String] Path of entity in API
15
+ # @param value [String] Value of field of searched entity
16
+ # @param field [String] Field of searched entity
17
+ # @param query [Hash] Optional additional search query parameters
17
18
  # @returns [Hash] A single entity matching the search, or an exception if not found or multiple found
18
- def lookup_by_name(subpath, search_name, query: nil)
19
- query = {} if query.nil?
19
+ def lookup_with_q(entity, value:, field: 'name', query: {})
20
+ Aspera.assert_type(query, Hash){'query'}
21
+ Aspera.assert_type(field, String){'field'}
20
22
  # returns entities matching the query (it matches against several fields in case insensitive way)
21
- matching_items = read(subpath, query.merge({'q' => search_name}))
22
- # API style: {totalcount:, ...} cspell: disable-line
23
- matching_items = matching_items[subpath] if matching_items.is_a?(Hash)
23
+ # We don't do paging, as anyway, we look for only one match
24
+ matching_items = read(entity, query.merge({'q' => value}))
25
+ # API style: {totalcount:, ...} cspell: disable-line: TODO: is that total_count ?
26
+ # @type [Array<Hash{String => String}>]
27
+ matching_items = matching_items[entity] if matching_items.is_a?(Hash)
24
28
  Aspera.assert_type(matching_items, Array)
25
29
  case matching_items.length
26
30
  when 1 then return matching_items.first
27
- when 0 then raise EntityNotFound, %Q{No such #{subpath}: "#{search_name}"}
31
+ when 0 then raise EntityNotFound, %Q{No such #{entity}: "#{value}"}
28
32
  else
29
33
  # multiple case insensitive partial matches, try case insensitive full match
30
- # (anyway AoC does not allow creation of 2 entities with same case insensitive name)
31
- name_matches = matching_items.select{ |i| i['name'].casecmp?(search_name)}
32
- case name_matches.length
33
- when 1 then return name_matches.first
34
- 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.)
35
- else raise "Two entities cannot have the same case insensitive name: #{name_matches.map{ |i| i['name']}}"
34
+ # (anyway AoC does not allow creation of 2 entities with same case insensitive field value)
35
+ value_matches = matching_items.select{ |i| i[field].casecmp?(value)}
36
+ case value_matches.length
37
+ when 1 then return value_matches.first
38
+ when 0 then raise %Q(#{entity}: Multiple case insensitive partial match for: "#{value}" in #{matching_items.map{ |i| i[field]}.join(', ')} but no case insensitive full match. Please be more specific or give exact #{field}.)
39
+ else raise "Two entities cannot have the same case insensitive #{field}: #{value_matches.map{ |i| i[field]}}"
36
40
  end
37
41
  end
38
42
  end
39
43
 
40
- # Get a (full or partial) list of all entities of a given type with query: offset/limit
44
+ # Get a (full or partial) list of all entities of a given type.
45
+ # Using query: `offset` + `limit`
46
+ # And response `total_count`
41
47
  # @param entity [String,Symbol] API endpoint of entity to list
42
48
  # @param items_key [String] Key in the result to get the list of items (Default: same as `entity`)
43
49
  # @param query [Hash,nil] Additional query parameters
@@ -114,6 +120,7 @@ module Aspera
114
120
  return found.first if found.length.eql?(1)
115
121
  raise Cli::BadIdentifier.new(entity, value, field: field, count: found.length)
116
122
  end
123
+
117
124
  PER_PAGE_DEFAULT = 1000
118
125
  private_constant :PER_PAGE_DEFAULT
119
126
  module_function :lookup_entity_generic