aspera-cli 4.12.0 → 4.13.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 (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +17 -0
  4. data/CONTRIBUTING.md +97 -22
  5. data/README.md +548 -394
  6. data/bin/ascli +3 -3
  7. data/docs/test_env.conf +12 -5
  8. data/lib/aspera/aoc.rb +42 -42
  9. data/lib/aspera/ascmd.rb +4 -3
  10. data/lib/aspera/cli/extended_value.rb +24 -37
  11. data/lib/aspera/cli/formatter.rb +6 -0
  12. data/lib/aspera/cli/info.rb +2 -4
  13. data/lib/aspera/cli/main.rb +6 -0
  14. data/lib/aspera/cli/manager.rb +15 -6
  15. data/lib/aspera/cli/plugin.rb +1 -5
  16. data/lib/aspera/cli/plugins/aoc.rb +23 -6
  17. data/lib/aspera/cli/plugins/config.rb +13 -6
  18. data/lib/aspera/cli/plugins/faspex.rb +4 -3
  19. data/lib/aspera/cli/plugins/faspex5.rb +175 -42
  20. data/lib/aspera/cli/plugins/node.rb +107 -50
  21. data/lib/aspera/cli/plugins/preview.rb +3 -3
  22. data/lib/aspera/cli/plugins/server.rb +11 -1
  23. data/lib/aspera/cli/plugins/sync.rb +3 -3
  24. data/lib/aspera/cli/transfer_agent.rb +24 -10
  25. data/lib/aspera/cli/version.rb +2 -1
  26. data/lib/aspera/command_line_builder.rb +2 -1
  27. data/lib/aspera/cos_node.rb +1 -1
  28. data/lib/aspera/fasp/agent_connect.rb +1 -1
  29. data/lib/aspera/fasp/agent_direct.rb +12 -12
  30. data/lib/aspera/fasp/agent_node.rb +14 -4
  31. data/lib/aspera/fasp/installation.rb +1 -0
  32. data/lib/aspera/fasp/parameters.rb +11 -3
  33. data/lib/aspera/fasp/parameters.yaml +3 -1
  34. data/lib/aspera/fasp/transfer_spec.rb +3 -1
  35. data/lib/aspera/faspex_gw.rb +1 -0
  36. data/lib/aspera/faspex_postproc.rb +2 -2
  37. data/lib/aspera/node.rb +11 -4
  38. data/lib/aspera/oauth.rb +3 -2
  39. data/lib/aspera/preview/file_types.rb +8 -6
  40. data/lib/aspera/preview/generator.rb +23 -11
  41. data/lib/aspera/preview/options.rb +3 -2
  42. data/lib/aspera/preview/terminal.rb +34 -0
  43. data/lib/aspera/preview/utils.rb +8 -8
  44. data/lib/aspera/rest.rb +5 -4
  45. data/lib/aspera/rest_call_error.rb +3 -1
  46. data/lib/aspera/secret_hider.rb +4 -4
  47. data/lib/aspera/sync.rb +39 -33
  48. data/lib/aspera/web_server_simple.rb +22 -18
  49. data.tar.gz.sig +0 -0
  50. metadata +39 -46
  51. metadata.gz.sig +0 -0
  52. data/examples/aoc.rb +0 -30
  53. data/examples/faspex4.rb +0 -94
  54. data/examples/node.rb +0 -96
  55. data/examples/server.rb +0 -93
@@ -38,8 +38,9 @@ module Aspera
38
38
  return @param_description_cache
39
39
  end
40
40
 
41
+ # @param to_text [bool] replace HTML entities with text equivalent
41
42
  # @return a table suitable to display in manual
42
- def man_table(to_text: true)
43
+ def man_table
43
44
  result = []
44
45
  description.each do |name, options|
45
46
  param = {name: name, type: [options[:accepted_types]].flatten.join(','), description: options[:desc]}
@@ -70,10 +71,17 @@ module Aspera
70
71
  if options.key?(:enum)
71
72
  param[:description] += "\nAllowed values: #{options[:enum].join(', ')}"
72
73
  end
73
- param[:description] = param[:description].gsub('/', '/') if to_text
74
+ # replace "solidus" HTML entity with its text value
75
+ param[:description] = param[:description].gsub('/', '\\')
74
76
  result.push(param)
75
77
  end
76
- return result
78
+ return result.sort do |a, b|
79
+ if a[:name].start_with?('EX_').eql?(b[:name].start_with?('EX_'))
80
+ a[:name] <=> b[:name]
81
+ else
82
+ b[:name] <=> a[:name]
83
+ end
84
+ end
77
85
  end
78
86
 
79
87
  # special encoding methods used in YAML (key: :convert)
@@ -559,7 +559,8 @@ remove_empty_source_directory:
559
559
  :cli:
560
560
  :type: :opt_without_arg
561
561
  EX_at_rest_password:
562
- :desc: "DEPRECATED: Prefer to use standard parameter: content_protection_password"
562
+ :desc: Content protection password
563
+ :deprecation: "Use standard spec parameter: content_protection_password"
563
564
  :agents:
564
565
  - :direct
565
566
  :cli:
@@ -703,6 +704,7 @@ EX_file_pair_list:
703
704
  :switch: "--file-pair-list"
704
705
  EX_ascp_args:
705
706
  :desc: Add native command line arguments to ascp
707
+ :deprecation: Use parameter ascp_args in option transfer_info
706
708
  :accepted_types: :array
707
709
  :agents:
708
710
  - :direct
@@ -15,7 +15,9 @@ module Aspera
15
15
  'ssh_port' => SSH_PORT,
16
16
  'fasp_port' => UDP_PORT
17
17
  }.freeze
18
- # define constants for enums of parameters: <paramater>_<enum>, e.g. CIPHER_AES_128
18
+ # reserved tag for Aspera
19
+ TAG_RESERVED = 'aspera'
20
+ # define constants for enums of parameters: <parameter>_<enum>, e.g. CIPHER_AES_128
19
21
  Aspera::Fasp::Parameters.description.each do |k, v|
20
22
  next unless v[:enum].is_a?(Array)
21
23
  v[:enum].each do |enum|
@@ -84,6 +84,7 @@ module Aspera
84
84
  response['Content-Type'] = 'application/json'
85
85
  response.body = {error: e.message}.to_json
86
86
  Log.log.error(e.message)
87
+ Log.log.debug{e.backtrace.join("\n")}
87
88
  end
88
89
  else
89
90
  response.status = 400
@@ -19,7 +19,7 @@ module Aspera
19
19
  @parameters[:fail_on_error] ||= false
20
20
  @parameters[:timeout_seconds] ||= 60
21
21
  super(server)
22
- Log.log.debug{"Faspex4PostProcServlet initialized"}
22
+ Log.log.debug{'Faspex4PostProcServlet initialized'}
23
23
  end
24
24
 
25
25
  def do_POST(request, response)
@@ -39,7 +39,7 @@ module Aspera
39
39
  return
40
40
  end
41
41
  # build script path by removing domain, and adding script folder
42
- script_file = request.path[@parameters[:root].size .. ]
42
+ script_file = request.path[@parameters[:root].size..]
43
43
  Log.log.debug{"script file=#{script_file}"}
44
44
  script_path = File.join(@parameters[:script_folder], script_file)
45
45
  Log.log.debug{"script=#{script_path}"}
data/lib/aspera/node.rb CHANGED
@@ -40,7 +40,9 @@ module Aspera
40
40
  end
41
41
  end
42
42
 
43
- REQUIRED_APP_INFO_FIELDS = %i[node_info app api workspace_info].freeze
43
+ # fields in @app_info
44
+ REQUIRED_APP_INFO_FIELDS = %i[api app node_info workspace_id workspace_name].freeze
45
+ # methods of @app_info[:api]
44
46
  REQUIRED_APP_API_METHODS = %i[node_api_from add_ts_tags].freeze
45
47
  private_constant :REQUIRED_APP_INFO_FIELDS, :REQUIRED_APP_API_METHODS
46
48
 
@@ -71,8 +73,13 @@ module Aspera
71
73
 
72
74
  # @returns [Aspera::Node] a Node or nil
73
75
  def node_id_to_node(node_id)
74
- return self if !@app_info.nil? && @app_info[:node_info]['id'].eql?(node_id)
75
- return @app_info[:api].node_api_from(node_id: node_id, workspace_info: @app_info[workspace_info]) unless @app_info.nil?
76
+ if !@app_info.nil?
77
+ return self if node_id.eql?(@app_info[:node_info]['id'])
78
+ return @app_info[:api].node_api_from(
79
+ node_id: node_id,
80
+ workspace_id: @app_info[:workspace_id],
81
+ workspace_name: @app_info[:workspace_name])
82
+ end
76
83
  Log.log.warn{"cannot resolve link with node id #{node_id}"}
77
84
  return nil
78
85
  end
@@ -216,7 +223,7 @@ module Aspera
216
223
  'direction' => direction,
217
224
  'token' => ak_token,
218
225
  'tags' => {
219
- 'aspera' => {
226
+ Fasp::TransferSpec::TAG_RESERVED => {
220
227
  'node' => {
221
228
  'access_key' => ak_name,
222
229
  'file_id' => file_id
data/lib/aspera/oauth.rb CHANGED
@@ -87,6 +87,7 @@ module Aspera
87
87
  # @param lambda_create called to create token
88
88
  # @param id_create called to generate unique id for token, for cache
89
89
  def register_token_creator(id, lambda_create, id_create)
90
+ Log.log.debug{"registering token creator #{id}"}
90
91
  raise 'ERROR: requites Symbol and 2 lambdas' unless id.is_a?(Symbol) && lambda_create.is_a?(Proc) && id_create.is_a?(Proc)
91
92
  @create_handlers[id] = lambda_create
92
93
  @id_handlers[id] = id_create
@@ -94,7 +95,7 @@ module Aspera
94
95
 
95
96
  # @return one of the registered creators for the given create type
96
97
  def token_creator(id)
97
- raise "token grant method unknown: #{id}/#{id.class}" unless @create_handlers.key?(id)
98
+ raise "token grant method unknown: '#{id}' (#{id.class})" unless @create_handlers.key?(id)
98
99
  @create_handlers[id]
99
100
  end
100
101
 
@@ -272,7 +273,7 @@ module Aspera
272
273
 
273
274
  # an API was already called, but failed, we need to regenerate or refresh
274
275
  if use_refresh_token
275
- if token_data.is_a?(Hash) && token_data.key?('refresh_token')
276
+ if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
276
277
  # save possible refresh token, before deleting the cache
277
278
  refresh_token = token_data['refresh_token']
278
279
  end
@@ -12,6 +12,7 @@ module Aspera
12
12
  CONVERSION_TYPES = %i[image office pdf plaintext video].freeze
13
13
 
14
14
  # define how files are processed based on mime type
15
+ # spellchecker:disable
15
16
  SUPPORTED_MIME_TYPES = {
16
17
  'application/json' => :plaintext,
17
18
  'application/mac-binhex40' => :office,
@@ -267,6 +268,7 @@ module Aspera
267
268
  'ycbcra' => :image,
268
269
  'yuv' => :image,
269
270
  'zabw' => :office}.freeze
271
+ # spellchecker:enable
270
272
 
271
273
  private_constant :SUPPORTED_MIME_TYPES, :SUPPORTED_EXTENSIONS
272
274
 
@@ -301,12 +303,12 @@ module Aspera
301
303
  def conversion_type(filepath, mimetype)
302
304
  Log.log.debug{"conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})"}
303
305
  # 1- get type from provided mime type, using local mapping
304
- conv_type = SUPPORTED_MIME_TYPES[mimetype] if !mimetype.nil?
306
+ conversion_type = SUPPORTED_MIME_TYPES[mimetype] if !mimetype.nil?
305
307
  # 2- else, from computed mime type (if available)
306
- if conv_type.nil? && @use_mimemagic
308
+ if conversion_type.nil? && @use_mimemagic
307
309
  detected_mime = mime_from_file(filepath)
308
310
  if !detected_mime.nil?
309
- conv_type = SUPPORTED_MIME_TYPES[detected_mime]
311
+ conversion_type = SUPPORTED_MIME_TYPES[detected_mime]
310
312
  if !mimetype.nil?
311
313
  if mimetype.eql?(detected_mime)
312
314
  Log.log.debug('matching mime type per magic number')
@@ -319,9 +321,9 @@ module Aspera
319
321
  end
320
322
  # 3- else, from extensions, using local mapping
321
323
  extension = File.extname(filepath.downcase)[1..-1]
322
- conv_type = SUPPORTED_EXTENSIONS[extension] if conv_type.nil?
323
- Log.log.debug{"conversion_type(#{extension}): #{conv_type.class.name} [#{conv_type}]"}
324
- return conv_type
324
+ conversion_type = SUPPORTED_EXTENSIONS[extension] if conversion_type.nil?
325
+ Log.log.debug{"conversion_type(#{extension}): #{conversion_type.class.name} [#{conversion_type}]"}
326
+ return conversion_type
325
327
  end
326
328
  end
327
329
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # ffmpeg options:
4
+ # spellchecker:ignore pauseframes libx264 trunc bufsize muxer apng libmp3lame maxrate posterize movflags faststart
5
+ # spellchecker:ignore palettegen paletteuse pointsize bordercolor repage lanczos
6
+
3
7
  require 'open3'
4
8
  require 'aspera/preview/options'
5
9
  require 'aspera/preview/utils'
@@ -12,6 +16,8 @@ module Aspera
12
16
  # values for preview_format : output format
13
17
  PREVIEW_FORMATS = %i[png mp4].freeze
14
18
 
19
+ FFMPEG_OPTIONS_LIST = %w[in out].freeze
20
+
15
21
  # CLI needs to know conversion type to know if need skip it
16
22
  attr_reader :conversion_type
17
23
 
@@ -22,7 +28,6 @@ module Aspera
22
28
  # supported preview type is one of Preview::PREVIEW_FORMATS
23
29
  # the resulting preview file type is taken from destination file extension.
24
30
  # conversion methods are provided by private methods: convert_<conversion_type>_to_<preview_format>
25
- # (combi = combination of source file type and destination format)
26
31
  # -> conversion_type is one of FileTypes::CONVERSION_TYPES
27
32
  # -> preview_format is one of Generator::PREVIEW_FORMATS
28
33
  # the conversion video->mp4 is implemented in methods: convert_video_to_mp4_using_<video_conversion>
@@ -70,10 +75,10 @@ module Aspera
70
75
  # check that generated size does not exceed maximum
71
76
  result_size = File.size(@destination_file_path)
72
77
  if result_size > @options.max_size
73
- Log.log.warn{"preview size exceeds maximum #{result_size} > #{@options.max_size}"}
78
+ Log.log.warn{"preview size exceeds maximum allowed #{result_size} > #{@options.max_size}"}
74
79
  end
75
80
  rescue StandardError => e
76
- Log.log.error{"Ignoging: #{e.message}"}
81
+ Log.log.error{"Ignoring: #{e.message}"}
77
82
  Log.log.debug(e.backtrace.join("\n").red)
78
83
  FileUtils.cp(File.expand_path(@preview_format_symb.eql?(:mp4) ? 'video_error.png' : 'image_error.png', File.dirname(__FILE__)), @destination_file_path)
79
84
  ensure
@@ -102,11 +107,11 @@ module Aspera
102
107
  def convert_video_to_mp4_using_blend
103
108
  p_duration = Utils.video_get_duration(@source_file_path)
104
109
  p_start_offset = @options.video_start_sec.to_i
105
- p_keyframecount = @options.blend_keyframes.to_i
110
+ p_key_frame_count = @options.blend_keyframes.to_i
106
111
  last_keyframe = nil
107
112
  current_index = 1
108
- 1.upto(p_keyframecount) do |i|
109
- offset_seconds = get_offset(p_duration, p_start_offset, p_keyframecount, i)
113
+ 1.upto(p_key_frame_count) do |i|
114
+ offset_seconds = get_offset(p_duration, p_start_offset, p_key_frame_count, i)
110
115
  Utils.video_dump_frame(@source_file_path, offset_seconds, @options.video_scale, this_tmpdir, current_index)
111
116
  Utils.video_dupe_frame(this_tmpdir, current_index, @options.blend_pauseframes)
112
117
  Utils.video_blend_frames(this_tmpdir, last_keyframe, current_index) unless last_keyframe.nil?
@@ -133,17 +138,17 @@ module Aspera
133
138
  File.open(filelist, 'w+') do |f|
134
139
  1.upto(@options.clips_count.to_i) do |i|
135
140
  offset_seconds = get_offset(p_duration, @options.video_start_sec.to_i, @options.clips_count.to_i, i)
136
- tmpfilename = format('clip%04d.mp4', i)
141
+ tmp_file_name = format('clip%04d.mp4', i)
137
142
  Utils.ffmpeg(
138
143
  in_f: @source_file_path,
139
144
  in_p: ['-ss', offset_seconds * 0.9],
140
- out_f: File.join(this_tmpdir, tmpfilename),
145
+ out_f: File.join(this_tmpdir, tmp_file_name),
141
146
  out_p: [
142
147
  '-ss', offset_seconds * 0.1,
143
148
  '-t', @options.clips_length,
144
149
  '-filter:v', "scale=#{@options.video_scale}",
145
150
  '-codec:a', 'libmp3lame'])
146
- f.puts("file '#{tmpfilename}'")
151
+ f.puts("file '#{tmp_file_name}'")
147
152
  end
148
153
  end
149
154
  # concat clips
@@ -154,12 +159,19 @@ module Aspera
154
159
  out_p: ['-codec', 'copy'])
155
160
  end
156
161
 
157
- # do a simple reencoding
162
+ # do a simple re-encoding
158
163
  def convert_video_to_mp4_using_reencode
164
+ options = @options.reencode_ffmpeg
165
+ raise 'reencode_ffmpeg must be a Hash' unless options.is_a?(Hash)
166
+ options.each do |k, v|
167
+ raise "Key not supported: #{k}. Use keys: #{FFMPEG_OPTIONS_LIST.join(',')}" unless FFMPEG_OPTIONS_LIST.include?(k)
168
+ raise "Value for #{k} must be Array" unless v.is_a?(Array)
169
+ end
159
170
  Utils.ffmpeg(
160
171
  in_f: @source_file_path,
172
+ in_p: options['in'] || ['-ss', @options.video_start_sec.to_i * 0.9],
161
173
  out_f: @destination_file_path,
162
- out_p: [
174
+ out_p: options['out'] || [
163
175
  '-t', '60',
164
176
  '-codec:v', 'libx264',
165
177
  '-profile:v', 'high',
@@ -20,8 +20,9 @@ module Aspera
20
20
  { name: :thumb_text_font, default: 'Courier', description: 'png: plaintext: font to render text with imagemagick convert (identify -list font)'},
21
21
  { name: :video_conversion, default: :reencode, description: 'mp4: method for preview generation', values: VIDEO_CONVERSION_METHODS },
22
22
  { name: :video_png_conv, default: :fixed, description: 'mp4: method for thumbnail generation', values: VIDEO_THUMBNAIL_METHODS },
23
- { name: :video_start_sec, default: 10, description: 'mp4: start offset (seconds) of video preview' },
24
- { name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: video scale (ffmpeg)' },
23
+ { name: :video_scale, default: "'min(iw,360)':-2", description: 'mp4: all: video scale (ffmpeg)' },
24
+ { name: :video_start_sec, default: 10, description: 'mp4: all: start offset (seconds) of video preview' },
25
+ { name: :reencode_ffmpeg, default: {}, description: 'mp4: reencode: options to ffmpeg' },
25
26
  { name: :blend_keyframes, default: 30, description: 'mp4: blend: # key frames' },
26
27
  { name: :blend_pauseframes, default: 3, description: 'mp4: blend: # pause frames' },
27
28
  { name: :blend_transframes, default: 5, description: 'mp4: blend: # transition blend frames' },
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rmagick' # https://rmagick.github.io/index.html
4
+ require 'rainbow'
5
+ require 'io/console'
6
+ module Aspera
7
+ module Preview
8
+ # function conversion_type returns one of the types: CONVERSION_TYPES
9
+ class Terminal
10
+ class << self
11
+ def build(blob, reserved_lines: 0)
12
+ # TODO: retrieve terminal ratio using
13
+ font_ratio = 1.7
14
+ (term_rows, term_columns) = IO.console.winsize
15
+ term_rows -= reserved_lines
16
+ image = Magick::ImageList.new.from_blob(blob)
17
+ chosen_factor = [term_rows / image.rows.to_f, term_columns / image.columns.to_f].min
18
+ image = image.scale((image.columns * chosen_factor * font_ratio).to_i, (image.rows * chosen_factor).to_i)
19
+ text_pixels = []
20
+ image.each_pixel do |pixel, col, row|
21
+ text_pixels.push("\n") if col.eql?(0) && !row.eql?(0)
22
+ pixel_rgb = [pixel.red, pixel.green, pixel.blue].map do |color|
23
+ # quantum depth is 8 or 16: convert xc: -format "%q" info:
24
+ # Rainbow only supports 8-bit colors
25
+ color >> (Magick::MAGICKCORE_QUANTUM_DEPTH - 8)
26
+ end
27
+ text_pixels.push(Rainbow(' ').background(pixel_rgb))
28
+ end
29
+ return text_pixels.join
30
+ end
31
+ end # class << self
32
+ end # class Terminal
33
+ end # module Preview
34
+ end # module Aspera
@@ -14,9 +14,9 @@ module Aspera
14
14
  # shell exit code when command is not found
15
15
  BASH_EXIT_NOT_FOUND = 127
16
16
  # external binaries used
17
- EXPERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
18
- TMPFMT = 'img%04d.jpg'
19
- private_constant :BASH_SPECIAL_CHARACTERS, :BASH_EXIT_NOT_FOUND, :EXPERNAL_TOOLS, :TMPFMT
17
+ EXTERNAL_TOOLS = %i[ffmpeg ffprobe convert composite optipng unoconv].freeze
18
+ TEMP_FORMAT = 'img%04d.jpg'
19
+ private_constant :BASH_SPECIAL_CHARACTERS, :BASH_EXIT_NOT_FOUND, :EXTERNAL_TOOLS, :TEMP_FORMAT
20
20
 
21
21
  class << self
22
22
  # returns string with single quotes suitable for bash if there is any bash metacharacter
@@ -27,7 +27,7 @@ module Aspera
27
27
 
28
28
  # check that external tools can be executed
29
29
  def check_tools(skip_types=[])
30
- tools_to_check = EXPERNAL_TOOLS.dup
30
+ tools_to_check = EXTERNAL_TOOLS.dup
31
31
  tools_to_check.delete(:unoconv) if skip_types.include?(:office)
32
32
  # Check for binaries
33
33
  tools_to_check.each do |command_symb|
@@ -39,7 +39,7 @@ module Aspera
39
39
  # one could use "system", but we would need to redirect stdout/err
40
40
  # @return true if su
41
41
  def external_command(command_symb, command_args)
42
- raise "unexpected command #{command_symb}" unless EXPERNAL_TOOLS.include?(command_symb)
42
+ raise "unexpected command #{command_symb}" unless EXTERNAL_TOOLS.include?(command_symb)
43
43
  # build command line, and quote special characters
44
44
  command = command_args.clone.unshift(command_symb).map{|i| shell_quote(i.to_s)}.join(' ')
45
45
  Log.log.debug{"cmd=#{command}".blue}
@@ -69,7 +69,7 @@ module Aspera
69
69
  # input_file,input_args,output_file,output_args
70
70
  a[:gl_p] ||= [
71
71
  '-y', # overwrite output without asking
72
- '-loglevel', 'error' # show only errors and up]
72
+ '-loglevel', 'error' # show only errors and up
73
73
  ]
74
74
  a[:in_p] ||= []
75
75
  a[:out_p] ||= []
@@ -88,11 +88,11 @@ module Aspera
88
88
  end
89
89
 
90
90
  def ffmpeg_fmt(temp_folder)
91
- return File.join(temp_folder, TMPFMT)
91
+ return File.join(temp_folder, TEMP_FORMAT)
92
92
  end
93
93
 
94
94
  def get_tmp_num_filepath(temp_folder, file_number)
95
- return File.join(temp_folder, format(TMPFMT, file_number))
95
+ return File.join(temp_folder, format(TEMP_FORMAT, file_number))
96
96
  end
97
97
 
98
98
  def video_dupe_frame(temp_folder, index, count)
data/lib/aspera/rest.rb CHANGED
@@ -224,8 +224,9 @@ module Aspera
224
224
  begin
225
225
  # we try the call, and will retry only if oauth, as we can, first with refresh, and then re-auth if refresh is bad
226
226
  oauth_tries ||= 2
227
- tries_remain_redirect ||= call_data[:redirect_max].nil? ? 0 : call_data[:redirect_max].to_i
228
- Log.log.debug('send request')
227
+ # initialize with number of initial retries allowed, nil gives zero
228
+ tries_remain_redirect = call_data[:redirect_max].to_i if tries_remain_redirect.nil?
229
+ Log.log.debug("send request (retries=#{tries_remain_redirect})")
229
230
  # make http request (pipelined)
230
231
  http_session.request(req) do |response|
231
232
  result[:http] = response
@@ -286,8 +287,8 @@ module Aspera
286
287
  Log.log.debug{"using new token=#{call_data[:headers]['Authorization']}"}
287
288
  retry unless (oauth_tries -= 1).zero?
288
289
  end # if oauth
289
- # moved ?
290
- if e.response.is_a?(Net::HTTPRedirection) && tries_remain_redirect.positive?
290
+ # redirect ? (any code beginning with 3)
291
+ if tries_remain_redirect.positive? && e.response.is_a?(Net::HTTPRedirection)
291
292
  tries_remain_redirect -= 1
292
293
  current_uri = URI.parse(call_data[:base_url])
293
294
  new_url = e.response['location']
@@ -5,7 +5,9 @@ module Aspera
5
5
  class RestCallError < StandardError
6
6
  attr_accessor :request, :response
7
7
 
8
- # @param http response
8
+ # @param req HTTP Request object
9
+ # @param resp HTTP Response object
10
+ # @param msg Error message
9
11
  def initialize(req, resp, msg)
10
12
  @request = req
11
13
  @response = resp
@@ -35,13 +35,13 @@ module Aspera
35
35
  def log_formatter(original_formatter)
36
36
  original_formatter ||= Logger::Formatter.new
37
37
  # NOTE: that @log_secrets may be set AFTER this init is done, so it's done at runtime
38
- return lambda do |severity, datetime, progname, msg|
38
+ return lambda do |severity, date_time, program_name, msg|
39
39
  if msg.is_a?(String) && !@log_secrets
40
- REGEX_LOG_REPLACES.each do |regx|
41
- msg = msg.gsub(regx){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"}
40
+ REGEX_LOG_REPLACES.each do |reg_ex|
41
+ msg = msg.gsub(reg_ex){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"}
42
42
  end
43
43
  end
44
- original_formatter.call(severity, datetime, progname, msg)
44
+ original_formatter.call(severity, date_time, program_name, msg)
45
45
  end
46
46
  end
47
47
 
data/lib/aspera/sync.rb CHANGED
@@ -8,6 +8,13 @@ require 'base64'
8
8
  module Aspera
9
9
  # builds command line arg for async
10
10
  class Sync
11
+ # default is push
12
+ DIRECTIONS = %i[push pull bidi].freeze
13
+ DIRECTION_TO_REQUEST_TYPE = {
14
+ push: :sync_upload,
15
+ pull: :sync_download,
16
+ bidi: :sync
17
+ }.freeze
11
18
  PARAMS_VX_INSTANCE =
12
19
  {
13
20
  'alt_logdir' => { cli: { type: :opt_with_arg}, accepted_types: :string},
@@ -38,7 +45,7 @@ module Aspera
38
45
  'cooloff' => { cli: { type: :opt_with_arg}, accepted_types: :int},
39
46
  'pending_max' => { cli: { type: :opt_with_arg}, accepted_types: :int},
40
47
  'scan_intensity' => { cli: { type: :opt_with_arg}, accepted_types: :string},
41
- 'cipher' => { cli: { type: :opt_with_arg}, accepted_types: :string, ts: true},
48
+ 'cipher' => { cli: { type: :opt_with_arg, convert: 'Aspera::Fasp::Parameters.convert_remove_hyphen'}, accepted_types: :string, ts: true},
42
49
  'transfer_threads' => { cli: { type: :opt_with_arg}, accepted_types: :int},
43
50
  'preserve_time' => { cli: { type: :opt_without_arg}, ts: :preserve_times},
44
51
  'preserve_access_time' => { cli: { type: :opt_without_arg}, ts: nil},
@@ -60,7 +67,7 @@ module Aspera
60
67
  PARAMS_VX_KEYS = %w[instance sessions].freeze
61
68
 
62
69
  # new API
63
- TS_TO_PARAMS = {
70
+ TS_TO_PARAMS_V2 = {
64
71
  'remote_host' => 'remote.host',
65
72
  'remote_user' => 'remote.user',
66
73
  'remote_password' => 'remote.pass',
@@ -74,14 +81,27 @@ module Aspera
74
81
 
75
82
  ASYNC_EXECUTABLE = 'async'
76
83
 
77
- private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :ASYNC_EXECUTABLE
84
+ private_constant :PARAMS_VX_INSTANCE, :PARAMS_VX_SESSION, :PARAMS_VX_KEYS, :ASYNC_EXECUTABLE, :TS_TO_PARAMS_V2
85
+
86
+ attr_reader :env_args
78
87
 
79
- class << self
80
- def update_parameters_with_transfer_spec(sync_params, transfer_spec)
81
- if sync_params.key?('local')
88
+ # @param sync_params [Hash] sync parameters, old or new format
89
+ # @param node_sync [Object|nil]
90
+ def initialize(sync_params, node_sync)
91
+ raise StandardError, 'parameter must be Hash' unless sync_params.is_a?(Hash)
92
+ raise 'node_sync misses method transfer_spec' unless node_sync.nil? || node_sync.respond_to?(:transfer_spec)
93
+ @env_args = {
94
+ args: [],
95
+ env: {}
96
+ }
97
+ if sync_params.key?('local')
98
+ # async native JSON format (v2)
99
+ raise StandardError, 'remote must be Hash' unless sync_params['remote'].is_a?(Hash)
100
+ unless node_sync.nil?
101
+ transfer_spec = node_sync.transfer_spec(sync_params['direction'], sync_params['local']['path'], sync_params['remote']['path'])
82
102
  # async native JSON format
83
103
  raise StandardError, 'local must be Hash' unless sync_params['local'].is_a?(Hash)
84
- TS_TO_PARAMS.each do |ts_param, sy_path|
104
+ TS_TO_PARAMS_V2.each do |ts_param, sy_path|
85
105
  next unless transfer_spec.key?(ts_param)
86
106
  sy_dig = sy_path.split('.')
87
107
  param = sy_dig.pop
@@ -93,38 +113,23 @@ module Aspera
93
113
  sync_params['remote']['connect_mode'] ||= sync_params['remote'].key?('ws_port') ? 'ws' : 'ssh'
94
114
  sync_params['remote']['private_key_paths'] ||= Fasp::Installation.instance.bypass_keys if transfer_spec.key?('token')
95
115
  sync_params['remote']['path'] ||= '/' if transfer_spec.dig(*%w[tags aspera node file_id])
96
- elsif sync_params.key?('sessions')
116
+ end
117
+ @env_args[:args] = ["--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"]
118
+ elsif sync_params.key?('sessions')
119
+ # ascli JSON format (v1)
120
+ unless node_sync.nil?
97
121
  sync_params['sessions'].each do |session|
98
- PARAMS_VX_SESSION.each do |async_param, behaviour|
99
- if behaviour.key?(:ts)
100
- tspec_param = behaviour[:ts].is_a?(TrueClass) ? async_param : behaviour[:ts].to_s
122
+ transfer_spec = node_sync.transfer_spec(session['direction'], session['local_dir'], session['remote_dir'])
123
+ PARAMS_VX_SESSION.each do |async_param, behavior|
124
+ if behavior.key?(:ts)
125
+ tspec_param = behavior[:ts].is_a?(TrueClass) ? async_param : behavior[:ts].to_s
101
126
  session[async_param] ||= transfer_spec[tspec_param] if transfer_spec.key?(tspec_param)
102
127
  end
103
128
  end
104
129
  session['private_key_paths'] = Fasp::Installation.instance.bypass_keys if transfer_spec.key?('token')
105
130
  session['remote_dir'] = '/' if transfer_spec.dig(*%w[tags aspera node file_id])
106
131
  end
107
- else
108
- raise 'At least one of `local` or `sessions` must be present in async parameters'
109
132
  end
110
- Log.dump(:sync, sync_params)
111
- end
112
- end
113
-
114
- attr_reader :env_args
115
-
116
- def initialize(sync_params)
117
- raise StandardError, 'parameter must be Hash' unless sync_params.is_a?(Hash)
118
- @env_args = {
119
- args: [],
120
- env: {}
121
- }
122
- if sync_params.key?('local')
123
- # async native JSON format
124
- raise StandardError, 'remote must be Hash' unless sync_params['remote'].is_a?(Hash)
125
- @env_args[:args] = "--conf64=#{Base64.strict_encode64(JSON.generate(sync_params))}"
126
- elsif sync_params.key?('sessions')
127
- # ascli JSON format
128
133
  raise StandardError, "Only 'sessions', and optionally 'instance' keys are allowed" unless
129
134
  sync_params.keys.push('instance').uniq.sort.eql?(PARAMS_VX_KEYS)
130
135
  raise StandardError, 'sessions key must be Array' unless sync_params['sessions'].is_a?(Array)
@@ -139,7 +144,7 @@ module Aspera
139
144
 
140
145
  sync_params['sessions'].each do |session_params|
141
146
  raise StandardError, 'sessions must contain hashes' unless session_params.is_a?(Hash)
142
- raise StandardError, 'session must contain at leat name' unless session_params.key?('name')
147
+ raise StandardError, 'session must contain at least name' unless session_params.key?('name')
143
148
  session_builder = Aspera::CommandLineBuilder.new(session_params, PARAMS_VX_SESSION)
144
149
  session_builder.process_params
145
150
  session_builder.add_env_args(@env_args[:env], @env_args[:args])
@@ -147,6 +152,7 @@ module Aspera
147
152
  else
148
153
  raise 'At least one of `local` or `sessions` must be present in async parameters'
149
154
  end
155
+ Log.dump(:sync, sync_params)
150
156
  end
151
157
 
152
158
  def start
@@ -160,7 +166,7 @@ module Aspera
160
166
  else raise 'internal error: unspecified case'
161
167
  end
162
168
  end
163
- end
169
+ end # end Sync
164
170
 
165
171
  class SyncAdmin
166
172
  ASYNC_ADMIN_EXECUTABLE = 'asyncadmin'
@@ -8,24 +8,26 @@ require 'openssl'
8
8
  module Aspera
9
9
  class WebServerSimple < WEBrick::HTTPServer
10
10
  CERT_PARAMETERS = %i[key cert chain].freeze
11
- # generates and adds self signed cert to provided webrick options
12
- def self.fill_self_signed_cert(cert, key)
13
- cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/C=FR/O=Test/OU=Test/CN=Test')
14
- cert.not_before = Time.now
15
- cert.not_after = Time.now + 365 * 24 * 60 * 60
16
- cert.public_key = key.public_key
17
- cert.serial = 0x0
18
- cert.version = 2
19
- ef = OpenSSL::X509::ExtensionFactory.new
20
- ef.issuer_certificate = cert
21
- ef.subject_certificate = cert
22
- cert.extensions = [
23
- ef.create_extension('basicConstraints', 'CA:TRUE', true),
24
- ef.create_extension('subjectKeyIdentifier', 'hash')
25
- # ef.create_extension('keyUsage', 'cRLSign,keyCertSign', true),
26
- ]
27
- cert.add_extension(ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always'))
28
- cert.sign(key, OpenSSL::Digest.new('SHA256'))
11
+ class << self
12
+ # generates and adds self signed cert to provided webrick options
13
+ def fill_self_signed_cert(cert, key)
14
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/C=FR/O=Test/OU=Test/CN=Test')
15
+ cert.not_before = Time.now
16
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
17
+ cert.public_key = key.public_key
18
+ cert.serial = 0x0
19
+ cert.version = 2
20
+ ef = OpenSSL::X509::ExtensionFactory.new
21
+ ef.issuer_certificate = cert
22
+ ef.subject_certificate = cert
23
+ cert.extensions = [
24
+ ef.create_extension('basicConstraints', 'CA:TRUE', true),
25
+ ef.create_extension('subjectKeyIdentifier', 'hash')
26
+ # ef.create_extension('keyUsage', 'cRLSign,keyCertSign', true),
27
+ ]
28
+ cert.add_extension(ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always'))
29
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
30
+ end
29
31
  end
30
32
 
31
33
  # @param uri [URI]
@@ -66,6 +68,8 @@ module Aspera
66
68
  end
67
69
  # self signed certificate generates characters on STDERR, see create_self_signed_cert in webrick/ssl.rb
68
70
  Log.capture_stderr { super(webrick_options) }
71
+ # kill -USR1 for graceful shutdown
72
+ Kernel.trap('USR1') { shutdown }
69
73
  end
70
74
 
71
75
  # log web server access ( option AccessLog )
data.tar.gz.sig CHANGED
Binary file