aspera-cli 4.25.6 → 4.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +74 -47
  4. data/CONTRIBUTING.md +1 -1
  5. data/lib/aspera/api/aoc.rb +118 -78
  6. data/lib/aspera/api/node.rb +101 -49
  7. data/lib/aspera/ascp/installation.rb +94 -30
  8. data/lib/aspera/cli/extended_value.rb +1 -0
  9. data/lib/aspera/cli/formatter.rb +47 -40
  10. data/lib/aspera/cli/manager.rb +30 -4
  11. data/lib/aspera/cli/plugins/aoc.rb +214 -136
  12. data/lib/aspera/cli/plugins/ats.rb +3 -3
  13. data/lib/aspera/cli/plugins/base.rb +17 -42
  14. data/lib/aspera/cli/plugins/config.rb +5 -3
  15. data/lib/aspera/cli/plugins/console.rb +3 -3
  16. data/lib/aspera/cli/plugins/faspex.rb +5 -5
  17. data/lib/aspera/cli/plugins/faspex5.rb +20 -18
  18. data/lib/aspera/cli/plugins/node.rb +66 -70
  19. data/lib/aspera/cli/plugins/oauth.rb +5 -12
  20. data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
  21. data/lib/aspera/cli/plugins/preview.rb +116 -80
  22. data/lib/aspera/cli/plugins/server.rb +2 -10
  23. data/lib/aspera/cli/plugins/shares.rb +7 -7
  24. data/lib/aspera/cli/version.rb +1 -1
  25. data/lib/aspera/dot_container.rb +7 -3
  26. data/lib/aspera/environment.rb +3 -2
  27. data/lib/aspera/log.rb +1 -1
  28. data/lib/aspera/preview/file_types.rb +1 -1
  29. data/lib/aspera/preview/generator.rb +146 -91
  30. data/lib/aspera/preview/options.rb +4 -1
  31. data/lib/aspera/preview/terminal.rb +50 -20
  32. data/lib/aspera/preview/utils.rb +76 -34
  33. data/lib/aspera/products/transferd.rb +1 -1
  34. data/lib/aspera/rest.rb +1 -0
  35. data/lib/aspera/rest_list.rb +23 -16
  36. data/lib/aspera/secret_hider.rb +3 -1
  37. data/lib/aspera/uri_reader.rb +17 -2
  38. data.tar.gz.sig +0 -0
  39. metadata +5 -5
  40. metadata.gz.sig +0 -0
@@ -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
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
@@ -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
@@ -21,6 +21,8 @@ module Aspera
21
21
  ALL_SECRETS = (ASCP_ENV_SECRETS + KEY_SECRETS + HTTP_SECRETS).freeze
22
22
  ALL_SECRETS2 = (KEY_SECRETS + HTTP_SECRETS).freeze
23
23
  KEY_FALSE_POSITIVES = [/^access_key$/, /^fallback_private_key$/].freeze
24
+ # min length of hidden secrets, use `+` or `{n,}`
25
+ SECRET_LENGTH = '{5,}'
24
26
  # regex that define named captures :begin and :end
25
27
  REGEX_LOG_REPLACES = [
26
28
  # private key values (place first)
@@ -32,7 +34,7 @@ module Aspera
32
34
  # rendered JSON or Ruby
33
35
  /(?<begin>(?:(?<quote>["'])|:)[^"':=]*(?:#{ALL_SECRETS.join('|')})[^"':=]*\k<quote>?(?:=>|:) *")[^"]+(?<end>")/,
34
36
  # logged data
35
- /(?<begin>(?:#{ALL_SECRETS2.join('|')})[ =:]+).*(?<end>$)/,
37
+ /(?<begin>(?:#{ALL_SECRETS2.join('|')})[ =:]+)[^ "]#{SECRET_LENGTH}(?<end>$)/,
36
38
  # cred in http dump
37
39
  /(?<begin>(?:#{HTTP_SECRETS.join('|')}): )[^\\]+(?<end>\\)/i
38
40
  ].freeze
@@ -14,6 +14,22 @@ module Aspera
14
14
  SCHEME_FILE_PFX2 = "#{SCHEME_FILE_PFX1}///"
15
15
  private_constant :SCHEME_FILE, :SCHEME_FILE_PFX1, :SCHEME_FILE_PFX2
16
16
  class << self
17
+ # @return [Boolean] true if the URL is a file:// URL
18
+ def file?(url)
19
+ url.start_with?(SCHEME_FILE_PFX2)
20
+ end
21
+
22
+ # @return [String] a file:// URL for the given path
23
+ def file_url(path)
24
+ return "#{SCHEME_FILE_PFX2}#{path}"
25
+ end
26
+
27
+ # @return [String] the path of a file:// URL
28
+ def file_path(url)
29
+ Aspera.assert(file?(url)){"use format: #{file_url('<path>')}"}
30
+ File.expand_path(url[SCHEME_FILE_PFX2.length..-1])
31
+ end
32
+
17
33
  # Read some content from some URI, support file: , http: and https: schemes
18
34
  def read(uri_to_read)
19
35
  uri = URI.parse(uri_to_read)
@@ -41,8 +57,7 @@ module Aspera
41
57
  if url.start_with?(SCHEME_FILE_PFX1)
42
58
  # for file scheme, return directly the path
43
59
  # require specific file scheme: the path part is "relative", or absolute if there are 4 slash
44
- raise "use format: #{SCHEME_FILE_PFX2}<path>" unless url.start_with?(SCHEME_FILE_PFX2)
45
- return File.expand_path(url[SCHEME_FILE_PFX2.length..-1])
60
+ return file_path(url)
46
61
  elsif url.start_with?('data:')
47
62
  # download to temp file
48
63
  # auto-delete on exit
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aspera-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.25.6
4
+ version: 4.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Laurent Martin
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: jwt
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '3.0'
103
+ version: '2.0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '3.0'
110
+ version: '2.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: marcel
113
113
  requirement: !ruby/object:Gem::Requirement
metadata.gz.sig CHANGED
Binary file