aspera-cli 4.0.0.pre2 → 4.2.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +761 -210
  3. data/bin/ascli +2 -0
  4. data/bin/dascli +13 -0
  5. data/docs/Makefile +2 -1
  6. data/docs/README.erb.md +628 -160
  7. data/docs/test_env.conf +22 -10
  8. data/docs/transfer_spec.html +1 -1
  9. data/lib/aspera/aoc.rb +87 -108
  10. data/lib/aspera/cli/formater.rb +2 -0
  11. data/lib/aspera/cli/main.rb +48 -45
  12. data/lib/aspera/cli/manager.rb +19 -6
  13. data/lib/aspera/cli/plugin.rb +9 -4
  14. data/lib/aspera/cli/plugins/alee.rb +1 -1
  15. data/lib/aspera/cli/plugins/aoc.rb +208 -183
  16. data/lib/aspera/cli/plugins/ats.rb +2 -2
  17. data/lib/aspera/cli/plugins/config.rb +205 -125
  18. data/lib/aspera/cli/plugins/console.rb +2 -2
  19. data/lib/aspera/cli/plugins/faspex.rb +15 -8
  20. data/lib/aspera/cli/plugins/faspex5.rb +76 -37
  21. data/lib/aspera/cli/plugins/node.rb +3 -3
  22. data/lib/aspera/cli/plugins/preview.rb +35 -25
  23. data/lib/aspera/cli/plugins/server.rb +23 -8
  24. data/lib/aspera/cli/transfer_agent.rb +7 -6
  25. data/lib/aspera/cli/version.rb +1 -1
  26. data/lib/aspera/colors.rb +5 -1
  27. data/lib/aspera/cos_node.rb +33 -28
  28. data/lib/aspera/environment.rb +15 -4
  29. data/lib/aspera/fasp/connect.rb +28 -21
  30. data/lib/aspera/fasp/http_gw.rb +140 -28
  31. data/lib/aspera/fasp/installation.rb +119 -57
  32. data/lib/aspera/fasp/local.rb +174 -178
  33. data/lib/aspera/fasp/manager.rb +12 -0
  34. data/lib/aspera/fasp/node.rb +4 -4
  35. data/lib/aspera/fasp/parameters.rb +6 -18
  36. data/lib/aspera/fasp/resume_policy.rb +13 -12
  37. data/lib/aspera/log.rb +10 -2
  38. data/lib/aspera/node.rb +61 -1
  39. data/lib/aspera/oauth.rb +36 -13
  40. data/lib/aspera/persistency_folder.rb +9 -4
  41. data/lib/aspera/preview/file_types.rb +53 -21
  42. data/lib/aspera/preview/generator.rb +3 -3
  43. data/lib/aspera/rest.rb +29 -18
  44. data/lib/aspera/secrets.rb +20 -0
  45. data/lib/aspera/temp_file_manager.rb +19 -0
  46. metadata +40 -22
@@ -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'},
@@ -63,7 +59,7 @@ module Aspera
63
59
  'exclude_older_than' => { :type => :opt_with_arg, :accepted_types=>Integer},
64
60
  'preserve_acls' => { :type => :opt_with_arg, :accepted_types=>String},
65
61
  'move_after_transfer' => { :type => :opt_with_arg, :accepted_types=>String},
66
- 'multi_session_threshold' => { :type => :opt_with_arg, :accepted_types=>String},
62
+ 'multi_session_threshold' => { :type => :opt_with_arg, :accepted_types=>Integer},
67
63
  # non standard parameters
68
64
  'EX_fasp_proxy_url' => { :type => :opt_with_arg, :option_switch=>'--proxy',:accepted_types=>String},
69
65
  'EX_http_proxy_url' => { :type => :opt_with_arg, :option_switch=>'-x',:accepted_types=>String},
@@ -82,11 +78,12 @@ module Aspera
82
78
  'lock_rate_policy' => { :type => :ignore, :accepted_types=>Aspera::CommandLineBuilder::BOOLEAN_CLASSES},
83
79
  'lock_min_rate' => { :type => :ignore, :accepted_types=>Aspera::CommandLineBuilder::BOOLEAN_CLASSES},
84
80
  'lock_target_rate' => { :type => :ignore, :accepted_types=>Aspera::CommandLineBuilder::BOOLEAN_CLASSES},
85
- #'authentication' => { :type => :ignore, :accepted_types=>String}, # = token
81
+ 'authentication' => { :type => :ignore, :accepted_types=>String}, # value = token
86
82
  'https_fallback_port' => { :type => :ignore, :accepted_types=>Integer}, # same as http fallback, option -t ?
87
83
  'content_protection' => { :type => :ignore, :accepted_types=>String},
88
84
  'cipher_allowed' => { :type => :ignore, :accepted_types=>String},
89
85
  'multi_session' => { :type => :ignore, :accepted_types=>Integer}, # managed
86
+ 'obfuscate_file_names' => { :type => :ignore, :accepted_types=>Aspera::CommandLineBuilder::BOOLEAN_CLASSES},
90
87
  # optional tags ( additional option to generate: {:space=>' ',:object_nl=>' ',:space_before=>'+',:array_nl=>'1'} )
91
88
  'tags' => { :type => :opt_with_arg, :option_switch=>'--tags64',:accepted_types=>Hash,:encode=>lambda{|tags|Base64.strict_encode64(JSON.generate(tags))}},
92
89
  # special processing @builder.process_param( called individually
@@ -100,7 +97,7 @@ module Aspera
100
97
  'wss_port' => { :type => :defer, :accepted_types=>Integer},
101
98
  }
102
99
 
103
- private_constant :SEC_IN_DAY,:FILE_LIST_AGE_MAX_SEC,:PARAM_DEFINITION
100
+ private_constant :PARAM_DEFINITION
104
101
 
105
102
  def initialize(job_spec,options)
106
103
  @job_spec=job_spec
@@ -207,18 +204,9 @@ module Aspera
207
204
  # temp file list files are created here
208
205
  def self.file_list_folder=(v)
209
206
  @@file_list_folder=v
210
- unless @@file_list_folder.nil?
207
+ if !@@file_list_folder.nil?
211
208
  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
209
+ TempFileManager.instance.cleanup_expired(@@file_list_folder)
222
210
  end
223
211
  end
224
212
 
@@ -14,17 +14,21 @@ module Aspera
14
14
  :sleep_max => 60
15
15
  }
16
16
 
17
- def initialize(params={})
17
+ # @param params see DEFAULTS
18
+ def initialize(params=nil)
18
19
  @parameters=DEFAULTS.clone
19
- return if params.nil?
20
- raise "expecting Hash (or nil), but have #{params.class}" unless params.is_a?(Hash)
21
- params.each do |k,v|
22
- if DEFAULTS.has_key?(k)
23
- @parameters[k]=v
24
- else
25
- raise "unknown resume parameter: #{k}, expect one of #{DEFAULTS.keys.map{|i|i.to_s}.join(",")}"
20
+ if !params.nil?
21
+ raise "expecting Hash (or nil), but have #{params.class}" unless params.is_a?(Hash)
22
+ params.each do |k,v|
23
+ if DEFAULTS.has_key?(k)
24
+ raise "#{k} must be Integer" unless v.is_a?(Integer)
25
+ @parameters[k]=v
26
+ else
27
+ raise "unknown resume parameter: #{k}, expect one of #{DEFAULTS.keys.map{|i|i.to_s}.join(",")}"
28
+ end
26
29
  end
27
30
  end
31
+ Log.log.debug("resume params=#{@parameters}")
28
32
  end
29
33
 
30
34
  # calls block a number of times (resumes) until success or limit reached
@@ -45,10 +49,7 @@ module Aspera
45
49
  # failure in ascp
46
50
  if e.retryable? then
47
51
  # exit if we exceed the max number of retry
48
- unless remaining_resumes > 0
49
- Log.log.error "Maximum number of retry reached"
50
- raise Fasp::Error,"max retry after: [#{status[:message]}]"
51
- end
52
+ raise Fasp::Error,'Maximum number of retry reached' if remaining_resumes <= 0
52
53
  else
53
54
  # give one chance only to non retryable errors
54
55
  unless remaining_resumes.eql?(@parameters[:iter_max])
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
@@ -49,7 +49,15 @@ module Aspera
49
49
 
50
50
  # change underlying logger, but keep log level
51
51
  def logger_type=(new_logtype)
52
- current_severity_integer=@logger.nil? ? Logger::Severity::WARN : @logger.level
52
+ current_severity_integer=if @logger.nil?
53
+ if ENV.has_key?('AS_LOG_LEVEL')
54
+ ENV['AS_LOG_LEVEL']
55
+ else
56
+ Logger::Severity::WARN
57
+ end
58
+ else
59
+ @logger.level
60
+ end
53
61
  case new_logtype
54
62
  when :stderr
55
63
  @logger = Logger.new(STDERR)
data/lib/aspera/node.rb CHANGED
@@ -1,14 +1,74 @@
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
+ MATCH_EXEC_PREFIX='exec:'
12
+
13
+ # for information only
6
14
  def self.decode_bearer_token(token)
7
15
  return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)
8
16
  end
17
+
18
+ # for access keys: provide expression to match entry in folder
19
+ # if no prefix: regex
20
+ # if prefix: ruby code
21
+ # if filder is nil, then always match
22
+ def self.file_matcher(match_expression)
23
+ match_expression||="#{MATCH_EXEC_PREFIX}true"
24
+ if match_expression.start_with?(MATCH_EXEC_PREFIX)
25
+ return eval "lambda{|f|#{match_expression[MATCH_EXEC_PREFIX.length..-1]}}"
26
+ end
27
+ return lambda{|f|f['name'].match(/#{match_expression}/)}
28
+ end
29
+
9
30
  def initialize(rest_params)
10
31
  super(rest_params)
11
- # specifics here
32
+ end
33
+
34
+ # recursively crawl in a folder.
35
+ # subfolders a processed if the processing method returns true
36
+ # @param processor must provide a method to process each entry
37
+ # @param opt options
38
+ # - top_file_id file id to start at (default = access key root file id)
39
+ # - top_file_path path of top folder (default = /)
40
+ # - method processing method (default= process_entry)
41
+ def crawl(processor,opt={})
42
+ Log.log.debug("crawl1 #{opt}")
43
+ # not possible with bearer token
44
+ opt[:top_file_id] ||= read('access_keys/self')[:data]['root_file_id']
45
+ opt[:top_file_path] ||= '/'
46
+ opt[:method] ||= :process_entry
47
+ raise "processor must have #{opt[:method]}" unless processor.respond_to?(opt[:method])
48
+ Log.log.debug("crawl #{opt}")
49
+ #top_info=read("files/#{opt[:top_file_id]}")[:data]
50
+ folders_to_explore=[{id: opt[:top_file_id], relpath: opt[:top_file_path]}]
51
+ Log.dump(:folders_to_explore,folders_to_explore)
52
+ while !folders_to_explore.empty? do
53
+ current_item = folders_to_explore.shift
54
+ Log.log.debug("searching #{current_item[:relpath]}".bg_green)
55
+ # get folder content
56
+ folder_contents = begin
57
+ read("files/#{current_item[:id]}/files")[:data]
58
+ rescue => e
59
+ Log.log.warn("#{current_item[:relpath]}: #{e.class} #{e.message}")
60
+ []
61
+ end
62
+ Log.dump(:folder_contents,folder_contents)
63
+ folder_contents.each do |entry|
64
+ relative_path=File.join(current_item[:relpath],entry['name'])
65
+ Log.log.debug("looking #{relative_path}".bg_green)
66
+ # entry type is file, folder or link
67
+ if processor.send(opt[:method],entry,relative_path) and entry['type'].eql?('folder')
68
+ folders_to_explore.push({:id=>entry['id'],:relpath=>relative_path})
69
+ end
70
+ end
71
+ end
12
72
  end
13
73
  end
14
74
  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
@@ -214,6 +225,16 @@ module Aspera
214
225
  :nbf => seconds_since_epoch-JWT_NOTBEFORE_OFFSET, # not before
215
226
  :exp => seconds_since_epoch+JWT_EXPIRY_OFFSET # expiration
216
227
  }
228
+ # Hum.. compliant ? TODO: remove when Faspex5 API is clarified
229
+ if @params[:jwt_is_f5]
230
+ payload[:jti] = SecureRandom.uuid
231
+ payload[:iat] = seconds_since_epoch
232
+ payload.delete(:nbf)
233
+ p_scope[:redirect_uri]="https://127.0.0.1:5000/token"
234
+ p_scope[:state]=SecureRandom.uuid
235
+ p_scope[:client_id]=@params[:client_id]
236
+ @token_auth_api.params[:auth]={:type=>:none}
237
+ end
217
238
 
218
239
  # non standard, only for global ids
219
240
  payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
@@ -222,8 +243,8 @@ module Aspera
222
243
 
223
244
  Log.log.debug("private=[#{rsa_private}]")
224
245
 
225
- Log.log.debug("JWT assertion=[#{payload}]")
226
- assertion = JWT.encode(payload, rsa_private, 'RS256')
246
+ Log.log.debug("JWT payload=[#{payload}]")
247
+ assertion = JWT.encode(payload, rsa_private, 'RS256',@params[:jwt_headers]||{})
227
248
 
228
249
  Log.log.debug("assertion=[#{assertion}]")
229
250
 
@@ -233,8 +254,10 @@ module Aspera
233
254
  }))
234
255
  when :url_token
235
256
  # AoC Public Link
257
+ params={:url_token=>@params[:url_token]}
258
+ params[:password]=@params[:password] if @params.has_key?(:password)
236
259
  resp=create_token_advanced({
237
- :json_params => {:url_token=>@params[:url_token]},
260
+ :json_params => params,
238
261
  :url_params => p_scope.merge({
239
262
  :grant_type => 'url_token'
240
263
  })})
@@ -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