Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MIME registry #5765

Merged
merged 2 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spec/std/data/mime.types
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo/bar foo
176 changes: 176 additions & 0 deletions spec/std/mime_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
require "./spec_helper"
require "mime"

module MIME
def self.initialized
@@initialized
end

def self.reset
@@initialized = false
@@types = {} of String => String
@@types_lower = {} of String => String
@@extensions = {} of String => Set(String)
end
end

describe MIME do
it ".from_extension" do
MIME.from_extension(".html").partition(';')[0].should eq "text/html"
MIME.from_extension(".HTML").partition(';')[0].should eq "text/html"

expect_raises KeyError do
MIME.from_extension(".fooobar")
end
MIME.from_extension(".fooobar", "default/fooobar").should eq "default/fooobar"
MIME.from_extension(".fooobar") { "default/fooobar" }.should eq "default/fooobar"
end

it ".from_extension?" do
MIME.from_extension?(".html").should eq MIME.from_extension(".html")
MIME.from_extension?(".HTML").should eq MIME.from_extension(".HTML")

MIME.from_extension?(".fooobar").should be_nil
end

it ".from_filename" do
MIME.from_filename("test.html").should eq MIME.from_extension(".html")
MIME.from_filename("foo/bar.not-exists", "foo/bar-exist").should eq "foo/bar-exist"
MIME.from_filename("foo/bar.not-exists") { "foo/bar-exist" }.should eq "foo/bar-exist"
end

it ".from_filename" do
MIME.from_filename?("test.html").should eq MIME.from_extension(".html")
end

describe ".register" do
it "registers new type" do
MIME.register(".Custom-Type", "text/custom-type")

MIME.from_extension(".Custom-Type").should eq "text/custom-type"
MIME.from_extension(".custom-type").should eq "text/custom-type"
MIME.extensions("text/custom-type").should eq Set{".Custom-Type"}

MIME.register(".custom-type2", "text/custom-type")
MIME.extensions("text/custom-type").should eq Set{".Custom-Type", ".custom-type2"}

MIME.register(".custom-type", "text/custom-type-lower")
MIME.from_extension(".custom-type").should eq "text/custom-type-lower"
MIME.from_extension(".Custom-Type").should eq "text/custom-type"
end

it "fails for invalid extension" do
expect_raises ArgumentError, "Extension does not start with a dot" do
MIME.register("foo", "text/foo")
end

expect_raises ArgumentError, "String contains null byte" do
MIME.register(".foo\0", "text/foo")
end
end
end

describe ".extensions" do
it "lists extensions" do
MIME.extensions("text/html").should contain ".htm"
MIME.extensions("text/html").should contain ".html"
end

it "returns empty set" do
MIME.extensions("foo/bar").should eq Set(String).new
end

it "recognizes overridden types" do
MIME.register(".custom-type-overridden", "text/custom-type-overridden")
MIME.register(".custom-type-overridden", "text/custom-type-override")

MIME.extensions("text/custom-type-overridden").should eq Set(String).new
end
end

it "parses media types" do
MIME.register(".parse-media-type1", "text/html; charset=utf-8")
MIME.extensions("text/html").should contain (".parse-media-type1")

MIME.register(".parse-media-type2", "text/html; foo = bar; bar= foo ;")
MIME.extensions("text/html").should contain (".parse-media-type2")

MIME.register(".parse-media-type3", "foo/bar")
MIME.extensions("foo/bar").should contain (".parse-media-type3")

MIME.register(".parse-media-type4", " form-data ; name=foo")
MIME.extensions("form-data").should contain (".parse-media-type4")

MIME.register(".parse-media-type41", %(FORM-DATA;name="foo"))
MIME.extensions("form-data").should contain (".parse-media-type41")

MIME.register(".parse-media-type5", %( FORM-DATA ; name="foo"))
MIME.extensions("form-data").should contain (".parse-media-type5")

expect_raises ArgumentError, "Invalid media type" do
MIME.register(".parse-media-type6", ": inline; attachment; filename=foo.html")
end

expect_raises ArgumentError, "Invalid media type" do
MIME.register(".parse-media-type7", "filename=foo.html, filename=bar.html")
end

expect_raises ArgumentError, "Invalid media type" do
MIME.register(".parse-media-type8", %("foo; filename=bar;baz"; filename=qux))
end

expect_raises ArgumentError, "Invalid media type" do
MIME.register(".parse-media-type9", "x=y; filename=foo.html")
end

expect_raises ArgumentError, "Invalid media type" do
MIME.register(".parse-media-type10", "filename=foo.html")
end
end

it ".load_mime_database" do
MIME.from_extension?(".bar").should be_nil
MIME.from_extension?(".fbaz").should be_nil

MIME.load_mime_database IO::Memory.new <<-EOF
foo/bar bar
foo/baz baz fbaz #foobaz
# foo/foo foo
EOF

MIME.from_extension?(".bar").should eq "foo/bar"
MIME.from_extension?(".fbaz").should eq "foo/baz"
MIME.from_extension?(".#foobaz").should be_nil
MIME.from_extension?(".foobaz").should be_nil
MIME.from_extension?(".foo").should be_nil
end

describe ".init" do
it "loads defaults" do
MIME.reset
MIME.init
MIME.initialized.should be_true
MIME.from_extension(".html").partition(';')[0].should eq "text/html"
ensure
MIME.reset
end

it "skips loading defaults" do
MIME.reset
MIME.init(load_defaults: false)
MIME.initialized.should be_true
MIME.from_extension?(".html").should be_nil
ensure
MIME.reset
end

it "loads file" do
MIME.reset
MIME.initialized.should be_false
MIME.init(datapath("mime.types"))
MIME.from_extension?(".foo").should eq "foo/bar"
ensure
MIME.reset
end
end
end
12 changes: 12 additions & 0 deletions src/crystal/system/mime.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Crystal::System::MIME
# Load MIME types from operating system source.
# def self.load
end

{% if flag?(:unix) %}
require "./unix/mime"
{% elsif flag?(:win32) %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
require "./win32/mime"
{% else %}
{% raise "No Crystal::System::Mime implementation available" %}
{% end %}
24 changes: 24 additions & 0 deletions src/crystal/system/unix/mime.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Crystal::System::MIME
MIME_SOURCES = {
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
"/etc/mime.types", # Linux
"/etc/httpd/mime.types", # Apache on Mac OS X
"/usr/local/etc/mime.types", # FreeBSD
"/usr/share/misc/mime.types", # OpenBSD
"/etc/httpd/conf/mime.types", # Apache
"/etc/apache/mime.types", # Apache 1
"/etc/apache2/mime.types", # Apache 2
"/usr/local/lib/netscape/mime.types", # Netscape
"/usr/local/etc/httpd/conf/mime.types", # Apache 1.2
}

# Load MIME types from operating system source.
def self.load
MIME_SOURCES.each do |path|
next unless ::File.exists?(path)
::File.open(path) do |file|
::MIME.load_mime_database file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work the other way around: Crystal::System::MIME should provide a method that returns (or yields) a mime.types file that MIME would try to parse.

That would avoid the circular dependency that MIME depends on Crystal::System::MIME which itself depends on MIME.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the circular dependency either. But the fact that Crystal::System::MIME.load utilizes MIME.load_mime_database is already a platform specific implementation.

It could certainly be reversed if load yields if it needs to load a file. On windows it just wouldn't yield, but load the database differently. But that's baking platform-specifics into the abstraction API.

That's how I chose to implement it and I think it should stay this way. But I won't hurt about changing if requested.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's similar with Crystal::System::Time.load_localtime btw., the Unix implementation calls the non-platform-specific implementation Time::Location.read_zoneinfo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what makes me wary is that something from Crystal::System is initializing an external namespace; and ::MIME should initialize itself using Crystal::System. I wish we could find a better way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could pull the platform-dependent initialization implementations out of Crystal::System into the main namespace. But that doesn't change anything, really.

end
rescue
end
end
end
8 changes: 8 additions & 0 deletions src/crystal/system/win32/mime.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Crystal::System::MIME
# Load MIME types from operating system source.
def self.load
# TODO: MIME types in Windows are provided by the registry. This needs to be
# implemented when registry access it is available.
# Until then, there will no system-provided MIME types in Windows.
end
end
15 changes: 2 additions & 13 deletions src/http/server/handlers/static_file_handler.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "ecr/macros"
require "html"
require "uri"
require "mime"

# A simple handler that lists directories and serves files under a given public directory.
class HTTP::StaticFileHandler
Expand Down Expand Up @@ -72,7 +73,7 @@ class HTTP::StaticFileHandler
return
end

context.response.content_type = mime_type(file_path)
context.response.content_type = MIME.from_filename(file_path, "application/octet-stream")
context.response.content_length = File.size(file_path)
File.open(file_path) do |file|
IO.copy(file, context.response)
Expand Down Expand Up @@ -126,18 +127,6 @@ class HTTP::StaticFileHandler
File.info(file_path).modification_time
end

private def mime_type(path)
case File.extname(path)
when ".txt" then "text/plain"
when ".htm", ".html" then "text/html"
when ".css" then "text/css"
when ".js" then "application/javascript"
when ".svg" then "image/svg+xml"
when ".wasm" then "application/wasm"
else "application/octet-stream"
end
end

record DirectoryListing, request_path : String, path : String do
@escaped_request_path : String?

Expand Down
Loading