51

Is there a bullet proof way to detect MIME type of uploaded file in Ruby or Ruby on Rails? I'm uploading JPEGs and PNGs using SWFupload and content_type is always "application/octet-stream"

Vincent
  • 16,086
  • 18
  • 67
  • 73

9 Answers9

49

The ruby-filemagic gem will do it:

require 'filemagic'

puts FileMagic.new(FileMagic::MAGIC_MIME).file(__FILE__)
# => text/x-ruby; charset=us-ascii

This gem does not look at the file extension at all. It reads a bit of the file contents and uses that to guess the file's type.

David Barlow
  • 4,914
  • 1
  • 28
  • 24
Wayne Conrad
  • 103,207
  • 26
  • 155
  • 191
  • 11
    On OS X I just needed to `brew install libmagic` before `gem install ruby-filemagic` would work. But the gem works like a charm for image/png, image/jpg, application/x-shockwave-flash, video/mp4, application/ogg, image/vnd.adobe.photoshop, application/pdf, video/x-ms-asf, etc. – Russell B Jun 21 '14 at 16:59
  • Just to clarify (and contrast with @NARKOZ's answer) this gem doesn't look at the extension to find the mime type, unlike the rails MIME::types option. – alexanderbird Jun 12 '15 at 22:42
  • I have case when filemagic cannot recognize simple image/jpeg: `FileMagic.new(FileMagic::MAGIC_MIME).file(URI.parse('https://d2qh54gyqi6t5f.cloudfront.net/boat_images/1/1813/1813941/2979796L.jpg').open.path) #=> "application/octet-stream; charset=binary"` but `IO.read(URI.parse('https://d2qh54gyqi6t5f.cloudfront.net/boat_images/1/1813/1813941/2979796L.jpg').open.path, 10) =~ /^#{Regexp.new("\xff\xd8\xff\xe0\x00\x10JFIF".force_encoding('binary'))}/ #=> 0` so I use it after @alain-beauvois header file checks – Lev Lukomsky Jul 21 '16 at 15:04
  • sadly, this gem doesn't seem to work on Rails 5 yet :( – priki Aug 15 '16 at 23:31
  • @DenisUyeda The FileMagic gem is not, to my knowledge dependent upon Rails in any way, so I'm surprised to learn that it doesn't work with Rails 5. – Wayne Conrad Aug 15 '16 at 23:50
  • 1
    Worked for me on Rails 5 but on Fedora/RHEL/CentOS, also needs `dnf install file-devel` for `gem "ruby-filemagic"` to compile – Kevin Feb 13 '18 at 23:12
  • Not exactly a lightweight solution. Filemagic installs a whole lot of dependencies...which hopefully won't collide with any of your other gems. Just sayin'. – Huliax Apr 09 '19 at 15:10
  • NOT bulletproof. Not working for me in a non-Rails codebase. `NameError (uninitialized constant Refile::Rails)`. – Huliax Apr 09 '19 at 17:43
  • @Huliax That doesn't look like an issue with filemagic at all. – Wayne Conrad Apr 09 '19 at 18:07
  • @WayneConrad Stack trace says otherwise. Code that depends on broken code is broken. – Huliax Apr 10 '19 at 19:27
  • @Huliax It's a mystery to me how the error you posted yesterday could be caused by filemagic, but you've seen some evidence I haven't. I'm sorry this solution didn't work for you. I don't know what you mean by "Code that depends on broken code is broken." – Wayne Conrad Apr 10 '19 at 19:36
  • Wow, that's a heavy dependency (outside of Rails). – Raphael May 13 '19 at 12:54
  • @Raphael I suppose it is (it binds to a C library, after all). But of all the heavy dependencies I've ever used in Ruby, this one has never given me any trouble at all, either in development or deployment. Unlike, say, rmagic, which can be pretty painful. – Wayne Conrad May 13 '19 at 13:43
  • @WayneConrad I don't know about pain using it; all I noted was an install time of 5min and 40 installed dependencies. o.o While certainly fair for a full Rails application, for quick scripts you'd want to use something else. (Well, I see that the question is tagged for rails, but this is one of the top search results when you Google for generic Ruby.) – Raphael May 13 '19 at 13:51
40

In Ruby on Rails you can do:

MIME::Types.type_for("filename.gif").first.content_type # => "image/gif"
NARKOZ
  • 27,203
  • 7
  • 68
  • 90
  • > Return the list of MIME::Types which belongs to the file based on its filename extension. If platform is true, then only file types that are specific to the current platform will be returned. – knoopx Sep 26 '11 at 11:48
  • 42
    Not a Valid answer, it just detect the file type based on its extention. If you name a PNG file with FLV extention it will detect it to be a => "video/x-flv" – Nadeem Yasin Oct 01 '12 at 13:06
  • 9
    Might be good enough if you enforce consistent file extension. – Olivier Amblet Feb 11 '13 at 12:29
  • 7
    Down-voted because the extension has nothing to do with the actual file type. There might not be an extension at all. – panzi Mar 12 '14 at 23:58
  • 2
    Rails 4.0.1: NameError: uninitialized constant MIME, what are your rails version? – Ivan Black Apr 26 '14 at 12:17
  • 6
    @IvanBlack this is from a Ruby gem, `mime-types` – mauro_oto Sep 22 '14 at 19:45
  • Note that you don't need Ruby on Rails. This is present in the [`mime-types`](https://github.com/mime-types/ruby-mime-types) gem which is used by RoR, but on itself is a lightweight gem. – 3limin4t0r Apr 28 '19 at 11:26
21

You can use this reliable method base on the magic header of the file :

def get_image_extension(local_file_path)
  png = Regexp.new("\x89PNG".force_encoding("binary"))
  jpg = Regexp.new("\xff\xd8\xff\xe0\x00\x10JFIF".force_encoding("binary"))
  jpg2 = Regexp.new("\xff\xd8\xff\xe1(.*){2}Exif".force_encoding("binary"))
  case IO.read(local_file_path, 10)
  when /^GIF8/
    'gif'
  when /^#{png}/
    'png'
  when /^#{jpg}/
    'jpg'
  when /^#{jpg2}/
    'jpg'
  else
    mime_type = `file #{local_file_path} --mime-type`.gsub("\n", '') # Works on linux and mac
    raise UnprocessableEntity, "unknown file type" if !mime_type
    mime_type.split(':')[1].split('/')[1].gsub('x-', '').gsub(/jpeg/, 'jpg').gsub(/text/, 'txt').gsub(/x-/, '')
  end  
end
Alain Beauvois
  • 5,896
  • 3
  • 44
  • 26
  • 9
    Using string interpolation when running an external command in backticks is generally not a good idea. `local_file_path` could be set to `;rm -rf .`. In this particular case the method would safely fail with `Errno::ENOENT` without wiping the current directory, but you better don't rely on that when the file name is provided by a user. –  Jun 07 '13 at 01:25
  • 1
    `file` is not guaranteed to be installed on every Linux distribution. For example it is not in docker://ubuntu:latest. Look before you leap. – RegularlyScheduledProgramming Oct 29 '19 at 20:34
18

The ruby-filemagic gem is good solution, but requires additional dependencies on libmagic (recently removed from CarrierWave as part of CarrierWave::MagicMimeTypes removal).

If you're interested in a pure ruby implementation, consider the MimeMagic gem! It works well for file types listed in the freedesktop.org mime database:

require 'mimemagic'

MimeMagic.by_magic(File.open('Table-Flip-Guy.jpg')).type # => "image/jpeg" 

For Microsoft Office 2007+ formats (xlsx, docx, and pptx), require the overlay (unless you're okay with the generic "application/zip" MIME type for these files)

require 'mimemagic'    
require 'mimemagic/overlay'

MimeMagic.by_magic(File.open('big_spreadsheet.xlsx')).type # => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 
Marc
  • 4,546
  • 2
  • 29
  • 45
10

filemagic gem is good solution but depends lots of unnecessary gems. (rails, aws-sdk-core, ...)

If your app is small and only runs in Linux or OSX, consider to use file program:

require 'shellwords'
mimetype = `file --brief --mime-type - < #{Shellwords.shellescape(__FILE__)}`.strip

Note: Replace __FILE__ with any expr contains the filepath.

kaorukobo
  • 2,223
  • 2
  • 22
  • 29
10

As of 2021, I would claim that the best tool to compute mime types based on all the available hints (magic number, file name when the magic number does not suffice, user hints) is Marcel.

To shamelessly quote the documentation itself:

Marcel::MimeType.for Pathname.new("example.gif")
#  => "image/gif"

File.open "example.gif" do |file|
  Marcel::MimeType.for file
end
#  => "image/gif"

Marcel::MimeType.for Pathname.new("unrecognisable-data"), name: "example.pdf"
#  => "application/pdf"

Marcel::MimeType.for extension: ".pdf"
#  => "application/pdf"

Marcel::MimeType.for Pathname.new("unrecognisable-data"), name: "example", declared_type: "image/png"
#  => "image/png"

Marcel::MimeType.for StringIO.new(File.read "unrecognisable-data")
#  => "application/octet-stream"
akim
  • 8,255
  • 3
  • 44
  • 60
  • Is there no built in way in Rails in 2022 to get the mime type of a file? – Corey Mar 24 '22 at 21:49
  • Checking my Gemfile.lock, ActiveStorage 7.0.4.3 is dependent on marcel and mini_mime, so this is available to you by virtue of using Rails – David Aldridge Apr 17 '23 at 14:17
5

mimemagic gem will also do it

https://github.com/minad/mimemagic

from the oficial documentation

MimeMagic is a library to detect the mime type of a file by extension or by content. It uses the mime database provided by freedesktop.org (see http://freedesktop.org/wiki/Software/shared-mime-info/).

require 'mimemagic'
MimeMagic.by_extension('html').text?
MimeMagic.by_extension('.html').child_of? 'text/plain'
MimeMagic.by_path('filename.txt')
MimeMagic.by_magic(File.open('test.html'))
# etc...
priki
  • 709
  • 1
  • 7
  • 13
1

in case you are doing this from scratch, install mimemagic gem

gem 'mimemagic'

open stream(bytes of target image)

url="https://i.ebayimg.com/images/g/rbIAAOSwojpgyQz1/s-l500.jpg"
result = URI.parse(url).open

then check data-stream's file type for example:

MimeMagic.by_magic(result).type == "image/jpeg"

even though as mentioned above

%w(JPEG GIF TIFF PNG).include?(MimeMagic.by_magic(result).type)

this might be more elegant

d1jhoni1b
  • 7,497
  • 1
  • 51
  • 37
-4

You can use

Mime::Type.lookup_by_extension(extention_name)

Thanks

Siva
  • 171
  • 1
  • 4
  • 7
    Downvoted because the extension has nothing to do with the actual file type. There might not be an extension. – panzi Mar 12 '14 at 23:57
  • Might be good enough if you enforce consistent file extension. – NARKOZ Mar 20 '14 at 12:20
  • 1
    -1 because this perpetuates the belief that file extensions are related to filetype (Windows is the only place where this is accurate). – Dan May 12 '14 at 16:50