aspera-cli 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -176,10 +176,34 @@ module Aspera
176
176
  return [:ssh_bypass_key_dsa,:ssh_bypass_key_rsa].map{|i|Installation.instance.path(i)}
177
177
  end
178
178
 
179
- def install_sdk
179
+ # download aspera SDK or use local file
180
+ # extract only ascp binary for current system architecture
181
+ def install_sdk(sdk_url)
180
182
  require 'zip'
181
183
  sdk_zip_path=File.join(Dir.tmpdir,'sdk.zip')
182
- Aspera::Rest.new(base_url: SDK_URL).call(operation: 'GET',save_to_file: sdk_zip_path)
184
+ if sdk_url.start_with?('file:')
185
+ # require specific file scheme: the path part is "relative", or absolute if there are 4 slash
186
+ raise 'use format: file:///<path>' unless sdk_url.start_with?('file:///')
187
+ sdk_zip_path=sdk_url.gsub(%r{^file:///},'')
188
+ else
189
+ redirect_remain=2
190
+ begin
191
+ Aspera::Rest.new(base_url: sdk_url).call(operation: 'GET',save_to_file: sdk_zip_path)
192
+ rescue Aspera::RestCallError => e
193
+ if e.response.is_a?(Net::HTTPRedirection)
194
+ if redirect_remain > 0
195
+ redirect_remain-=1
196
+ sdk_url=e.response['location']
197
+ retry
198
+ else
199
+ raise "too meny redirect"
200
+ end
201
+ else
202
+ raise e
203
+ end
204
+ end
205
+ end
206
+ # SDK is organized by architecture
183
207
  filter="/#{Environment.architecture}/"
184
208
  ascp_path=nil
185
209
  # first ensure license file is here so that ascp invokation for version works
@@ -187,6 +211,7 @@ module Aspera
187
211
  self.path(:aspera_conf)
188
212
  Zip::File.open(sdk_zip_path) do |zip_file|
189
213
  zip_file.each do |entry|
214
+ # get only specified arch, but not folder, only files
190
215
  if entry.name.include?(filter) and !entry.name.end_with?('/')
191
216
  archive_file=File.join(folder_path,File.basename(entry.name))
192
217
  File.open(archive_file, 'wb') do |output_stream|
@@ -220,9 +245,8 @@ module Aspera
220
245
  PRODUCT_INFO='product-info.mf'
221
246
  # policy for product selection
222
247
  FIRST_FOUND='FIRST'
223
- SDK_URL='https://eudemo.asperademo.com/aspera/faspex/sdk.zip'
224
248
 
225
- private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO,:SDK_URL
249
+ private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO
226
250
 
227
251
  # get some specific folder from specific applications: Connect or CLI
228
252
  def get_product_folders(name)
@@ -232,23 +232,26 @@ module Aspera
232
232
  end # case
233
233
  end # loop (process mgt port lines)
234
234
  # check that last status was received before process exit
235
- raise "INTERNAL: nil last status" if last_status_event.nil?
236
- case last_status_event['Type']
237
- when 'DONE'
238
- # return method (or just don't do anything)
239
- return
240
- when 'ERROR'
241
- Log.log.error("code: #{last_status_event['Code']}")
242
- if last_status_event['Description'] =~ /bearer token/i
243
- Log.log.error("need to regenerate token".red)
244
- if session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
245
- # regenerate token here, expired, or error on it
246
- env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
235
+ if last_status_event.nil?
236
+ Log.log.warn("no status read from ascp mgt port")
237
+ else
238
+ case last_status_event['Type']
239
+ when 'DONE'
240
+ # return method (or just don't do anything)
241
+ return
242
+ when 'ERROR'
243
+ Log.log.error("code: #{last_status_event['Code']}")
244
+ if last_status_event['Description'] =~ /bearer token/i
245
+ Log.log.error("need to regenerate token".red)
246
+ if session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
247
+ # regenerate token here, expired, or error on it
248
+ env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
249
+ end
247
250
  end
251
+ raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
252
+ else
253
+ raise "unexpected last event type: #{last_status_event['Type']}"
248
254
  end
249
- raise Fasp::Error.new(last_status_event['Description'],last_status_event['Code'].to_i)
250
- else
251
- raise "unexpected last event type: #{last_status_event['Type']}"
252
255
  end
253
256
  rescue SystemCallError => e
254
257
  # Process.spawn
@@ -262,8 +265,12 @@ module Aspera
262
265
  unless ascp_pid.nil?
263
266
  # "wait" for process to avoid zombie
264
267
  Process.wait(ascp_pid)
268
+ status=$?
265
269
  ascp_pid=nil
266
270
  session.delete(:io)
271
+ if !status.success?
272
+ raise Fasp::Error.new("ascp failed with code #{status.exitstatus}")
273
+ end
267
274
  end
268
275
  end # begin-ensure
269
276
  end # start_transfer_with_args_env
@@ -301,8 +308,9 @@ module Aspera
301
308
  @jobs={}
302
309
  # mutex protects global data accessed by threads
303
310
  @mutex=Mutex.new
304
- @resume_policy=ResumePolicy.new(agent_options)
305
311
  @enable_wss = agent_options[:wss] || false
312
+ agent_options.delete(:wss)
313
+ @resume_policy=ResumePolicy.new(agent_options)
306
314
  end
307
315
 
308
316
  # transfer thread entry
@@ -45,6 +45,18 @@ module Aspera
45
45
  end
46
46
  end # notify_listeners
47
47
 
48
+ def notify_begin(id,size)
49
+ notify_listeners('emulated',{LISTENER_SESSION_ID_B=>id,'Type'=>'NOTIFICATION','PreTransferBytes'=>size})
50
+ end
51
+
52
+ def notify_progress(id,size)
53
+ notify_listeners('emulated',{LISTENER_SESSION_ID_B=>id,'Type'=>'STATS','Bytescont'=>size})
54
+ end
55
+
56
+ def notify_end(id)
57
+ notify_listeners('emulated',{LISTENER_SESSION_ID_B=>id,'Type'=>'DONE'})
58
+ end
59
+
48
60
  public
49
61
  LISTENER_SESSION_ID_B='ListenerSessionId'
50
62
  LISTENER_SESSION_ID_S='listener_session_id'
@@ -55,7 +55,7 @@ module Aspera
55
55
  trdata=node_api_.read("ops/transfers/#{@transfer_id}")[:data] || {"status"=>"unknown"} rescue {"status"=>"waiting(read error)"}
56
56
  case trdata['status']
57
57
  when 'completed'
58
- notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@transfer_id,'Type'=>'DONE'})
58
+ notify_end(@transfer_id)
59
59
  break
60
60
  when 'waiting','partially_completed','unknown','waiting(read error)'
61
61
  if spinner.nil?
@@ -69,10 +69,10 @@ module Aspera
69
69
  #puts "running: sessions:#{trdata["sessions"].length}, #{trdata["sessions"].map{|i| i['bytes_transferred']}.join(',')}"
70
70
  if !started and trdata['precalc'].is_a?(Hash) and
71
71
  trdata['precalc']['status'].eql?('ready')
72
- notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@transfer_id,'Type'=>'NOTIFICATION','PreTransferBytes'=>trdata['precalc']['bytes_expected']})
72
+ notify_begin(@transfer_id,trdata['precalc']['bytes_expected'])
73
73
  started=true
74
74
  else
75
- notify_listeners('emulated',{Manager::LISTENER_SESSION_ID_B=>@transfer_id,'Type'=>'STATS','Bytescont'=>trdata['bytes_transferred']})
75
+ notify_progress(@transfer_id,trdata['bytes_transferred'])
76
76
  end
77
77
  else
78
78
  Log.log.warn("trdata -> #{trdata}")
@@ -81,7 +81,7 @@ module Aspera
81
81
  sleep 1
82
82
  end
83
83
  #TODO get status of sessions
84
- return []
84
+ return []
85
85
  end
86
86
  end
87
87
  end
@@ -16,10 +16,6 @@ module Aspera
16
16
  # because of garbage collection takes any file there
17
17
  # this could be refined, as , for instance, on macos, temp folder is already user specific
18
18
  @@file_list_folder=TempFileManager.instance.new_file_path_global('asession_filelists')
19
- SEC_IN_DAY=86400
20
- # assume no transfer last longer than this
21
- # (garbage collect file list which were not deleted after transfer)
22
- FILE_LIST_AGE_MAX_SEC=5*SEC_IN_DAY
23
19
  PARAM_DEFINITION={
24
20
  # parameters with env vars
25
21
  'remote_password' => { :type => :envvar, :variable=>'ASPERA_SCP_PASS'},
@@ -100,7 +96,7 @@ module Aspera
100
96
  'wss_port' => { :type => :defer, :accepted_types=>Integer},
101
97
  }
102
98
 
103
- private_constant :SEC_IN_DAY,:FILE_LIST_AGE_MAX_SEC,:PARAM_DEFINITION
99
+ private_constant :PARAM_DEFINITION
104
100
 
105
101
  def initialize(job_spec,options)
106
102
  @job_spec=job_spec
@@ -207,18 +203,9 @@ module Aspera
207
203
  # temp file list files are created here
208
204
  def self.file_list_folder=(v)
209
205
  @@file_list_folder=v
210
- unless @@file_list_folder.nil?
206
+ if !@@file_list_folder.nil?
211
207
  FileUtils.mkdir_p(@@file_list_folder)
212
- # garbage collect undeleted files
213
- Dir.entries(@@file_list_folder).each do |name|
214
- file_path=File.join(@@file_list_folder,name)
215
- age_sec=(Time.now - File.stat(file_path).mtime).to_i
216
- # check age of file, delete too old
217
- if File.file?(file_path) and age_sec > FILE_LIST_AGE_MAX_SEC
218
- Log.log.debug("garbage collecting #{name}")
219
- File.delete(file_path)
220
- end
221
- end
208
+ TempFileManager.instance.cleanup_expired(@@file_list_folder)
222
209
  end
223
210
  end
224
211
 
data/lib/aspera/log.rb CHANGED
@@ -14,7 +14,7 @@ module Aspera
14
14
  attr_reader :logger
15
15
  attr_reader :logger_type
16
16
  # levels are :debug,:info,:warn,:error,fatal,:unknown
17
- def self.levels; Logger::Severity.constants.map{|c| c.downcase.to_sym};end
17
+ def self.levels; Logger::Severity.constants.sort{|a,b|Logger::Severity.const_get(a)<=>Logger::Severity.const_get(b)}.map{|c|c.downcase.to_sym};end
18
18
 
19
19
  # where logs are sent to
20
20
  def self.logtypes; [:stderr,:stdout,:syslog];end
data/lib/aspera/node.rb CHANGED
@@ -1,14 +1,61 @@
1
+ require 'aspera/rest'
2
+ require 'aspera/log'
1
3
  require 'zlib'
2
4
  require 'base64'
5
+
3
6
  module Aspera
4
7
  # Provides additional functions using node API.
5
8
  class Node < Rest
9
+ # permissions
10
+ ACCESS_LEVELS=['delete','list','mkdir','preview','read','rename','write']
11
+
12
+ # for information only
6
13
  def self.decode_bearer_token(token)
7
14
  return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)
8
15
  end
16
+
9
17
  def initialize(rest_params)
10
18
  super(rest_params)
11
- # specifics here
19
+ end
20
+
21
+ # recursively crawl in a folder.
22
+ # subfolders a processed if the processing method returns true
23
+ # @param processor must provide a method to process each entry
24
+ # @param opt options
25
+ # - top_file_id file id to start at (default = access key root file id)
26
+ # - top_file_path path of top folder (default = /)
27
+ # - method processing method (default= process_entry)
28
+ def crawl(processor,opt={})
29
+ Log.log.debug("crawl1 #{opt}")
30
+ # not possible with bearer token
31
+ opt[:top_file_id] ||= read('access_keys/self')[:data]['root_file_id']
32
+ opt[:top_file_path] ||= '/'
33
+ opt[:method] ||= :process_entry
34
+ raise "processor must have #{opt[:method]}" unless processor.respond_to?(opt[:method])
35
+ Log.log.debug("crawl #{opt}")
36
+ #top_info=read("files/#{opt[:top_file_id]}")[:data]
37
+ folders_to_explore=[{id: opt[:top_file_id], relpath: opt[:top_file_path]}]
38
+ Log.dump(:folders_to_explore,folders_to_explore)
39
+ while !folders_to_explore.empty? do
40
+ current_item = folders_to_explore.shift
41
+ Log.log.debug("searching #{current_item[:relpath]}".bg_green)
42
+ # get folder content
43
+ folder_contents = begin
44
+ read("files/#{current_item[:id]}/files")[:data]
45
+ rescue => e
46
+ Log.log.warn("#{current_item[:relpath]}: #{e.class} #{e.message}")
47
+ []
48
+ end
49
+ Log.dump(:folder_contents,folder_contents)
50
+ folder_contents.each do |entry|
51
+ relative_path=File.join(current_item[:relpath],entry['name'])
52
+ Log.log.debug("looking #{relative_path}".bg_green)
53
+ # entry type is file, folder or link
54
+ if processor.send(opt[:method],entry,relative_path) and entry['type'].eql?('folder')
55
+ folders_to_explore.push({:id=>entry['id'],:relpath=>relative_path})
56
+ end
57
+ end
58
+ end
12
59
  end
13
60
  end
14
61
  end
data/lib/aspera/oauth.rb CHANGED
@@ -16,7 +16,10 @@ module Aspera
16
16
  # one hour validity (TODO: configurable?)
17
17
  JWT_EXPIRY_OFFSET=3600
18
18
  PERSIST_CATEGORY_TOKEN='token'
19
- private_constant :JWT_NOTBEFORE_OFFSET,:JWT_EXPIRY_OFFSET,:PERSIST_CATEGORY_TOKEN
19
+ # tokens older than 30 minutes will be discarded
20
+ TOKEN_EXPIRY_SECONDS=1800
21
+ THANK_YOU_HTML = "<html><head><title>Ok</title></head><body><h1>Thank you !</h1><p>You can close this window.</p></body></html>"
22
+ private_constant :JWT_NOTBEFORE_OFFSET,:JWT_EXPIRY_OFFSET,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRY_SECONDS,:THANK_YOU_HTML
20
23
  class << self
21
24
  # OAuth methods supported
22
25
  def auth_types
@@ -28,12 +31,18 @@ module Aspera
28
31
  end
29
32
 
30
33
  def persist_mgr
31
- raise "set persistency manager first" if @persist.nil?
34
+ if @persist.nil?
35
+ Log.log.warn('Not using persistency (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)')
36
+ # create NULL persistency class
37
+ @persist=Class.new do
38
+ def get(x);nil;end;def delete(x);nil;end;def put(x,y);nil;end;def garbage_collect(x,y);nil;end
39
+ end.new
40
+ end
32
41
  return @persist
33
42
  end
34
43
 
35
44
  def flush_tokens
36
- persist_mgr.flush_by_prefix(PERSIST_CATEGORY_TOKEN)
45
+ persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
37
46
  end
38
47
  end
39
48
 
@@ -79,10 +88,10 @@ module Aspera
79
88
  raise "redirect_uri must have a port" if uri.port.nil?
80
89
  # we could check that host is localhost or local address
81
90
  end
91
+ # cleanup expired tokens
92
+ self.class.persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_EXPIRY_SECONDS)
82
93
  end
83
94
 
84
- THANK_YOU_HTML = "<html><head><title>Ok</title></head><body><h1>Thank you !</h1><p>You can close this window.</p></body></html>"
85
-
86
95
  # open the login page, wait for code and check_code, then return code
87
96
  def goto_page_and_get_code(login_page_url,check_code)
88
97
  request_params=self.class.goto_page_and_get_request(@params[:redirect_uri],login_page_url)
@@ -117,6 +126,9 @@ module Aspera
117
126
 
118
127
  public
119
128
 
129
+ # used to change parameter, such as scope
130
+ attr_reader :params
131
+
120
132
  # @param options : :scope and :refresh
121
133
  def get_authorization(options={})
122
134
  # api scope can be overriden to get auth for other scope
@@ -184,14 +196,13 @@ module Aspera
184
196
  when :web
185
197
  # AoC Web based Auth
186
198
  check_code=SecureRandom.uuid
187
- login_page_url=Rest.build_uri(
188
- "#{@params[:base_url]}/#{@params[:path_authorize]}",
189
- p_client_id_and_scope.merge({
199
+ auth_params=p_client_id_and_scope.merge({
190
200
  :response_type => 'code',
191
201
  :redirect_uri => @params[:redirect_uri],
192
- :client_secret => @params[:client_secret],
193
202
  :state => check_code
194
- }))
203
+ })
204
+ auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
205
+ login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
195
206
  # here, we need a human to authorize on a web page
196
207
  code=goto_page_and_get_code(login_page_url,check_code)
197
208
  # exchange code for token
@@ -233,8 +244,10 @@ module Aspera
233
244
  }))
234
245
  when :url_token
235
246
  # AoC Public Link
247
+ params={:url_token=>@params[:url_token]}
248
+ params[:password]=@params[:password] if @params.has_key?(:password)
236
249
  resp=create_token_advanced({
237
- :json_params => {:url_token=>@params[:url_token]},
250
+ :json_params => params,
238
251
  :url_params => p_scope.merge({
239
252
  :grant_type => 'url_token'
240
253
  })})
@@ -55,12 +55,17 @@ module Aspera
55
55
  @cache.delete(object_id)
56
56
  end
57
57
 
58
- def flush_by_prefix(persist_category)
59
- persist_files=Dir[File.join(@folder,persist_category+'*'+FILE_SUFFIX)]
60
- persist_files.each do |filepath|
58
+ def garbage_collect(persist_category,max_age_seconds=nil)
59
+ garbage_files=Dir[File.join(@folder,persist_category+'*'+FILE_SUFFIX)]
60
+ if !max_age_seconds.nil?
61
+ current_time = Time.now
62
+ garbage_files.select! { |filepath| (current_time - File.stat(filepath).mtime).to_i > max_age_seconds}
63
+ end
64
+ garbage_files.each do |filepath|
61
65
  File.delete(filepath)
66
+ Log.log.debug("Deleted expired: #{filepath}")
62
67
  end
63
- return persist_files
68
+ return garbage_files
64
69
  end
65
70
 
66
71
  private
@@ -1,9 +1,11 @@
1
- require 'mimemagic'
2
- require 'mimemagic/overlay'
1
+ require 'aspera/log'
2
+ require 'singleton'
3
3
 
4
4
  module Aspera
5
5
  module Preview
6
+ # function conversion_type returns one of the types: CONVERSION_TYPES
6
7
  class FileTypes
8
+ include Singleton
7
9
  # values for conversion_type : input format
8
10
  CONVERSION_TYPES=[
9
11
  :image,
@@ -148,6 +150,7 @@ module Aspera
148
150
  'dif' => :office,
149
151
  'divx' => :video,
150
152
  'dng' => :image,
153
+ 'docx' => :office,
151
154
  'dpx' => :image,
152
155
  'epdf' => :image,
153
156
  'epi' => :image,
@@ -204,6 +207,7 @@ module Aspera
204
207
  'pam' => :image,
205
208
  'pcd' => :image,
206
209
  'pcds' => :image,
210
+ 'pdf' => :pdf,
207
211
  'pef' => :image,
208
212
  'picon' => :image,
209
213
  'pict' => :image,
@@ -261,39 +265,67 @@ module Aspera
261
265
  'x3f' => :image,
262
266
  'xcf' => :image,
263
267
  'xlk' => :office,
268
+ 'xlsx' => :office,
269
+ 'xls' => :office,
264
270
  'ycbcr' => :image,
265
271
  'ycbcra' => :image,
266
272
  'yuv' => :image,
267
273
  'zabw' => :office}
268
274
 
269
- #private_constant :SUPPORTED_MIME_TYPES, :SUPPORTED_EXTENSIONS
275
+ private_constant :SUPPORTED_MIME_TYPES, :SUPPORTED_EXTENSIONS
270
276
 
271
- def self.mime_from_file(filepath)
277
+ # @attr use_mimemagic [bool] true to use mimemagic to determine real mime type based on file content
278
+ attr_accessor :use_mimemagic
279
+
280
+ def initialize
281
+ @use_mimemagic=false
282
+ end
283
+
284
+ # use mime magic to find mime type based on file content (magic numbers)
285
+ def mime_from_file(filepath)
286
+ # moved here, as mimemagic can cause installation issues
287
+ require 'mimemagic'
288
+ require 'mimemagic/version'
289
+ require 'mimemagic/overlay' if MimeMagic::VERSION.start_with?('0.3.')
290
+ # check magic number inside file (empty string if not found)
272
291
  detected_mime=MimeMagic.by_magic(File.open(filepath)).to_s
273
- detected_mime=MimeMagic.by_path(filepath).to_s if detected_mime.empty?
292
+ # check extension only
293
+ if !SUPPORTED_MIME_TYPES.has_key?(detected_mime)
294
+ Log.log.debug("no conversion for #{detected_mime}, trying extension")
295
+ detected_mime=MimeMagic.by_extension(File.extname(filepath)).to_s
296
+ end
274
297
  detected_mime=nil if detected_mime.empty?
298
+ Log.log.debug("mimemagic: #{detected_mime.class.name} [#{detected_mime}]")
275
299
  return detected_mime
276
300
  end
277
301
 
278
- def self.conversion_type(filepath,mimetype,try_local_mime)
279
- detected_mime=nil
280
- if try_local_mime
302
+ # return file type, one of enum CONVERSION_TYPES
303
+ # @param filepath [String] full path to file
304
+ # @param mimetype [String] provided by node api
305
+ def conversion_type(filepath,mimetype)
306
+ Log.log.debug("conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})")
307
+ # 1- get type from provided mime type, using local mapping
308
+ conv_type=SUPPORTED_MIME_TYPES[mimetype] if ! mimetype.nil?
309
+ # 2- else, from computed mime type (if available)
310
+ if conv_type.nil? and @use_mimemagic
281
311
  detected_mime=mime_from_file(filepath)
282
- if mimetype.eql?(detected_mime)
283
- Log.log.debug("matching mime type per magic number")
284
- else
285
- # note: detected can be nil
286
- Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")
312
+ if ! detected_mime.nil?
313
+ conv_type=SUPPORTED_MIME_TYPES[detected_mime]
314
+ if ! mimetype.nil?
315
+ if mimetype.eql?(detected_mime)
316
+ Log.log.debug("matching mime type per magic number")
317
+ else
318
+ # note: detected can be nil
319
+ Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")
320
+ end
321
+ end
287
322
  end
288
323
  end
289
- # 1- get type from provided mime type
290
- result=FileTypes::SUPPORTED_MIME_TYPES[mimetype] unless mimetype.nil?
291
- # 2- else, from computed mime type
292
- result||=FileTypes::SUPPORTED_MIME_TYPES[detected_mime] unless detected_mime.nil?
293
- extension = File.extname(filepath).downcase.gsub(/^\./,'')
294
- # 3- else, from local extensions
295
- result||=FileTypes::SUPPORTED_EXTENSIONS[extension]
296
- return result
324
+ # 3- else, from extensions, using local mapping
325
+ extension = File.extname(filepath.downcase)[1..-1]
326
+ conv_type=SUPPORTED_EXTENSIONS[extension] if conv_type.nil?
327
+ Log.log.debug("conversion_type(#{extension}): #{conv_type.class.name} [#{conv_type}]")
328
+ return conv_type
297
329
  end
298
330
  end
299
331
  end