1
0
mirror of https://github.com/golang/go synced 2024-10-02 18:18:33 -06:00

mime/multipart and HTTP multipart/form-data support

Somewhat of a work-in-progress (in that MIME is a large spec), but this is
functional and enough for discussion and/or code review.

In addition to the unit tests, I've tested with curl and Chrome with
a variety of test files, making sure the digests of files are unaltered
when read via a multipart Part.

R=rsc, adg, dsymonds1, agl1
CC=golang-dev
https://golang.org/cl/1681049
This commit is contained in:
Brad Fitzpatrick 2010-07-14 17:26:14 -07:00 committed by Russ Cox
parent e9bcbc5398
commit 9b64fef71a
11 changed files with 820 additions and 8 deletions

View File

@ -94,6 +94,7 @@ DIRS=\
log\
math\
mime\
mime/multipart\
net\
netchan\
nntp\

View File

@ -16,6 +16,8 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"os"
"strconv"
"strings"
@ -40,6 +42,8 @@ var (
ErrNotSupported = &ProtocolError{"feature not supported"}
ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"}
ErrMissingContentLength = &ProtocolError{"missing ContentLength in HEAD response"}
ErrNotMultipart = &ProtocolError{"request Content-Type isn't multipart/form-data"}
ErrMissingBoundary = &ProtocolError{"no multipart boundary param Content-Type"}
)
type badStringError struct {
@ -139,6 +143,24 @@ func (r *Request) ProtoAtLeast(major, minor int) bool {
r.ProtoMajor == major && r.ProtoMinor >= minor
}
// MultipartReader returns a MIME multipart reader if this is a
// multipart/form-data POST request, else returns nil and an error.
func (r *Request) MultipartReader() (multipart.Reader, os.Error) {
v, ok := r.Header["Content-Type"]
if !ok {
return nil, ErrNotMultipart
}
d, params := mime.ParseMediaType(v)
if d != "multipart/form-data" {
return nil, ErrNotMultipart
}
boundary, ok := params["boundary"]
if !ok {
return nil, ErrMissingBoundary
}
return multipart.NewReader(r.Body, boundary), nil
}
// Return value if nonempty, def otherwise.
func valueOrDefault(value, def string) string {
if value != "" {

View File

@ -101,6 +101,24 @@ func TestPostContentTypeParsing(t *testing.T) {
}
}
func TestMultipartReader(t *testing.T) {
req := &Request{
Method: "POST",
Header: stringMap{"Content-Type": `multipart/form-data; boundary="foo123"`},
Body: nopCloser{new(bytes.Buffer)},
}
multipart, err := req.MultipartReader()
if multipart == nil {
t.Errorf("expected multipart; error: %v", err)
}
req.Header = stringMap{"Content-Type": "text/plain"}
multipart, err = req.MultipartReader()
if multipart != nil {
t.Errorf("unexpected multipart for text/plain")
}
}
func TestRedirect(t *testing.T) {
const (
start = "http://codesearch.google.com/"

View File

@ -6,6 +6,8 @@ include ../../Make.$(GOARCH)
TARG=mime
GOFILES=\
grammar.go\
mediatype.go\
type.go\
include ../../Make.pkg

36
src/pkg/mime/grammar.go Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mime
import (
"strings"
)
// isTSpecial returns true if rune is in 'tspecials' as defined by RFC
// 1531 and RFC 2045.
func isTSpecial(rune int) bool {
return strings.IndexRune(`()<>@,;:\"/[]?=`, rune) != -1
}
// IsTokenChar returns true if rune is in 'token' as defined by RFC
// 1531 and RFC 2045.
func IsTokenChar(rune int) bool {
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
// or tspecials>
return rune > 0x20 && rune < 0x7f && !isTSpecial(rune)
}
// IsQText returns true if rune is in 'qtext' as defined by RFC 822.
func IsQText(rune int) bool {
// CHAR = <any ASCII character> ; ( 0-177, 0.-127.)
// qtext = <any CHAR excepting <">, ; => may be folded
// "\" & CR, and including
// linear-white-space>
switch rune {
case int('"'), int('\\'), int('\r'):
return false
}
return rune < 0x80
}

120
src/pkg/mime/mediatype.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mime
import (
"bytes"
"strings"
"unicode"
)
// ParseMediaType parses a media type value and any optional
// parameters, per RFC 1531. Media types are the values in
// Content-Type and Content-Disposition headers (RFC 2183). On
// success, ParseMediaType returns the media type converted to
// lowercase and trimmed of white space and a non-nil params. On
// error, it returns an empty string and a nil params.
func ParseMediaType(v string) (mediatype string, params map[string]string) {
i := strings.Index(v, ";")
if i == -1 {
i = len(v)
}
mediatype = strings.TrimSpace(strings.ToLower(v[0:i]))
params = make(map[string]string)
v = v[i:]
for len(v) > 0 {
v = strings.TrimLeftFunc(v, unicode.IsSpace)
if len(v) == 0 {
return
}
key, value, rest := consumeMediaParam(v)
if key == "" {
// Parse error.
return "", nil
}
params[key] = value
v = rest
}
return
}
func isNotTokenChar(rune int) bool {
return !IsTokenChar(rune)
}
// consumeToken consumes a token from the beginning of provided
// string, per RFC 2045 section 5.1 (referenced from 2183), and return
// the token consumed and the rest of the string. Returns ("", v) on
// failure to consume at least one character.
func consumeToken(v string) (token, rest string) {
notPos := strings.IndexFunc(v, isNotTokenChar)
if notPos == -1 {
return v, ""
}
if notPos == 0 {
return "", v
}
return v[0:notPos], v[notPos:]
}
// consumeValue consumes a "value" per RFC 2045, where a value is
// either a 'token' or a 'quoted-string'. On success, consumeValue
// returns the value consumed (and de-quoted/escaped, if a
// quoted-string) and the rest of the string. On failure, returns
// ("", v).
func consumeValue(v string) (value, rest string) {
if !strings.HasPrefix(v, `"`) {
return consumeToken(v)
}
// parse a quoted-string
rest = v[1:] // consume the leading quote
buffer := new(bytes.Buffer)
var idx, rune int
var nextIsLiteral bool
for idx, rune = range rest {
switch {
case nextIsLiteral:
if rune >= 0x80 {
return "", v
}
buffer.WriteRune(rune)
nextIsLiteral = false
case rune == '"':
return buffer.String(), rest[idx+1:]
case IsQText(rune):
buffer.WriteRune(rune)
case rune == '\\':
nextIsLiteral = true
default:
return "", v
}
}
return "", v
}
func consumeMediaParam(v string) (param, value, rest string) {
rest = strings.TrimLeftFunc(v, unicode.IsSpace)
if !strings.HasPrefix(rest, ";") {
return "", "", v
}
rest = rest[1:] // consume semicolon
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
param, rest = consumeToken(rest)
if param == "" {
return "", "", v
}
if !strings.HasPrefix(rest, "=") {
return "", "", v
}
rest = rest[1:] // consume equals sign
value, rest = consumeValue(rest)
if value == "" {
return "", "", v
}
return param, value, rest
}

View File

@ -0,0 +1,117 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mime
import (
"testing"
)
func TestConsumeToken(t *testing.T) {
tests := [...][3]string{
[3]string{"foo bar", "foo", " bar"},
[3]string{"bar", "bar", ""},
[3]string{"", "", ""},
[3]string{" foo", "", " foo"},
}
for _, test := range tests {
token, rest := consumeToken(test[0])
expectedToken := test[1]
expectedRest := test[2]
if token != expectedToken {
t.Errorf("expected to consume token '%s', not '%s' from '%s'",
expectedToken, token, test[0])
} else if rest != expectedRest {
t.Errorf("expected to have left '%s', not '%s' after reading token '%s' from '%s'",
expectedRest, rest, token, test[0])
}
}
}
func TestConsumeValue(t *testing.T) {
tests := [...][3]string{
[3]string{"foo bar", "foo", " bar"},
[3]string{"bar", "bar", ""},
[3]string{" bar ", "", " bar "},
[3]string{`"My value"end`, "My value", "end"},
[3]string{`"My value" end`, "My value", " end"},
[3]string{`"\\" rest`, "\\", " rest"},
[3]string{`"My \" value"end`, "My \" value", "end"},
[3]string{`"\" rest`, "", `"\" rest`},
}
for _, test := range tests {
value, rest := consumeValue(test[0])
expectedValue := test[1]
expectedRest := test[2]
if value != expectedValue {
t.Errorf("expected to consume value [%s], not [%s] from [%s]",
expectedValue, value, test[0])
} else if rest != expectedRest {
t.Errorf("expected to have left [%s], not [%s] after reading value [%s] from [%s]",
expectedRest, rest, value, test[0])
}
}
}
func TestConsumeMediaParam(t *testing.T) {
tests := [...][4]string{
[4]string{" ; foo=bar", "foo", "bar", ""},
[4]string{"; foo=bar", "foo", "bar", ""},
[4]string{";foo=bar", "foo", "bar", ""},
[4]string{`;foo="bar"`, "foo", "bar", ""},
[4]string{`;foo="bar"; `, "foo", "bar", "; "},
[4]string{`;foo="bar"; foo=baz`, "foo", "bar", "; foo=baz"},
[4]string{` ; boundary=----CUT;`, "boundary", "----CUT", ";"},
[4]string{` ; key=value; blah="value";name="foo" `, "key", "value", `; blah="value";name="foo" `},
[4]string{`; blah="value";name="foo" `, "blah", "value", `;name="foo" `},
[4]string{`;name="foo" `, "name", "foo", ` `},
}
for _, test := range tests {
param, value, rest := consumeMediaParam(test[0])
expectedParam := test[1]
expectedValue := test[2]
expectedRest := test[3]
if param != expectedParam {
t.Errorf("expected to consume param [%s], not [%s] from [%s]",
expectedParam, param, test[0])
} else if value != expectedValue {
t.Errorf("expected to consume value [%s], not [%s] from [%s]",
expectedValue, value, test[0])
} else if rest != expectedRest {
t.Errorf("expected to have left [%s], not [%s] after reading [%s/%s] from [%s]",
expectedRest, rest, param, value, test[0])
}
}
}
func TestParseMediaType(t *testing.T) {
tests := [...]string{
`form-data; name="foo"`,
` form-data ; name=foo`,
`FORM-DATA;name="foo"`,
` FORM-DATA ; name="foo"`,
` FORM-DATA ; name="foo"`,
`form-data; key=value; blah="value";name="foo" `,
}
for _, test := range tests {
mt, params := ParseMediaType(test)
if mt != "form-data" {
t.Errorf("expected type form-data for %s, got [%s]", test, mt)
continue
}
if params["name"] != "foo" {
t.Errorf("expected name=foo for %s", test)
}
}
}
func TestParseMediaTypeBogus(t *testing.T) {
mt, params := ParseMediaType("bogus ;=========")
if mt != "" {
t.Error("expected empty type")
}
if params != nil {
t.Error("expected nil params")
}
}

View File

@ -0,0 +1,11 @@
# Copyright 2010 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
include ../../../Make.$(GOARCH)
TARG=mime/multipart
GOFILES=\
multipart.go\
include ../../../Make.pkg

View File

@ -0,0 +1,280 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
/*
Package multipart implements MIME multipart parsing, as defined in RFC
2046.
The implementation is sufficient for HTTP (RFC 2388) and the multipart
bodies generated by popular browsers.
*/
package multipart
import (
"bufio"
"bytes"
"io"
"mime"
"os"
"regexp"
"strings"
)
var headerRegexp *regexp.Regexp = regexp.MustCompile("^([a-zA-Z0-9\\-]+): *([^\r\n]+)")
// Reader is an iterator over parts in a MIME multipart body.
// Reader's underlying parser consumes its input as needed. Seeking
// isn't supported.
type Reader interface {
// NextPart returns the next part in the multipart, or (nil,
// nil) on EOF. An error is returned if the underlying reader
// reports errors, or on truncated or otherwise malformed
// input.
NextPart() (*Part, os.Error)
}
// A Part represents a single part in a multipart body.
type Part struct {
// The headers of the body, if any, with the keys canonicalized
// in the same fashion that the Go http.Request headers are.
// i.e. "foo-bar" changes case to "Foo-Bar"
Header map[string]string
buffer *bytes.Buffer
mr *multiReader
}
// FormName returns the name parameter if p has a Content-Disposition
// of type "form-data". Otherwise it returns the empty string.
func (p *Part) FormName() string {
// See http://tools.ietf.org/html/rfc2183 section 2 for EBNF
// of Content-Disposition value format.
v, ok := p.Header["Content-Disposition"]
if !ok {
return ""
}
d, params := mime.ParseMediaType(v)
if d != "form-data" {
return ""
}
return params["name"]
}
// NewReader creates a new multipart Reader reading from r using the
// given MIME boundary.
func NewReader(reader io.Reader, boundary string) Reader {
return &multiReader{
boundary: boundary,
dashBoundary: "--" + boundary,
endLine: "--" + boundary + "--",
bufReader: bufio.NewReader(reader),
}
}
// Implementation ....
type devNullWriter bool
func (*devNullWriter) Write(p []byte) (n int, err os.Error) {
return len(p), nil
}
var devNull = devNullWriter(false)
func newPart(mr *multiReader) (bp *Part, err os.Error) {
bp = new(Part)
bp.Header = make(map[string]string)
bp.mr = mr
bp.buffer = new(bytes.Buffer)
if err = bp.populateHeaders(); err != nil {
bp = nil
}
return
}
func (bp *Part) populateHeaders() os.Error {
for {
line, err := bp.mr.bufReader.ReadString('\n')
if err != nil {
return err
}
if line == "\n" || line == "\r\n" {
return nil
}
if matches := headerRegexp.MatchStrings(line); len(matches) == 3 {
key := matches[1]
value := matches[2]
// TODO: canonicalize headers ala http.Request.Header?
bp.Header[key] = value
continue
}
return os.NewError("Unexpected header line found parsing multipart body")
}
panic("unreachable")
}
// Read reads the body of a part, after its headers and before the
// next part (if any) begins.
func (bp *Part) Read(p []byte) (n int, err os.Error) {
for {
if bp.buffer.Len() >= len(p) {
// Internal buffer of unconsumed data is large enough for
// the read request. No need to parse more at the moment.
break
}
if !bp.mr.ensureBufferedLine() {
return 0, io.ErrUnexpectedEOF
}
if bp.mr.bufferedLineIsBoundary() {
// Don't consume this line
break
}
// Write all of this line, except the final CRLF
s := *bp.mr.bufferedLine
if strings.HasSuffix(s, "\r\n") {
bp.mr.consumeLine()
if !bp.mr.ensureBufferedLine() {
return 0, io.ErrUnexpectedEOF
}
if bp.mr.bufferedLineIsBoundary() {
// The final \r\n isn't ours. It logically belongs
// to the boundary line which follows.
bp.buffer.WriteString(s[0 : len(s)-2])
} else {
bp.buffer.WriteString(s)
}
break
}
if strings.HasSuffix(s, "\n") {
bp.buffer.WriteString(s)
bp.mr.consumeLine()
continue
}
return 0, os.NewError("multipart parse error during Read; unexpected line: " + s)
}
return bp.buffer.Read(p)
}
func (bp *Part) Close() os.Error {
io.Copy(&devNull, bp)
return nil
}
type multiReader struct {
boundary string
dashBoundary string // --boundary
endLine string // --boundary--
bufferedLine *string
bufReader *bufio.Reader
currentPart *Part
partsRead int
}
func (mr *multiReader) eof() bool {
return mr.bufferedLine == nil &&
!mr.readLine()
}
func (mr *multiReader) readLine() bool {
line, err := mr.bufReader.ReadString('\n')
if err != nil {
// TODO: care about err being EOF or not?
return false
}
mr.bufferedLine = &line
return true
}
func (mr *multiReader) bufferedLineIsBoundary() bool {
return strings.HasPrefix(*mr.bufferedLine, mr.dashBoundary)
}
func (mr *multiReader) ensureBufferedLine() bool {
if mr.bufferedLine == nil {
return mr.readLine()
}
return true
}
func (mr *multiReader) consumeLine() {
mr.bufferedLine = nil
}
func (mr *multiReader) NextPart() (*Part, os.Error) {
if mr.currentPart != nil {
mr.currentPart.Close()
}
for {
if mr.eof() {
return nil, io.ErrUnexpectedEOF
}
if isBoundaryDelimiterLine(*mr.bufferedLine, mr.dashBoundary) {
mr.consumeLine()
mr.partsRead++
bp, err := newPart(mr)
if err != nil {
return nil, err
}
mr.currentPart = bp
return bp, nil
}
if hasPrefixThenNewline(*mr.bufferedLine, mr.endLine) {
mr.consumeLine()
// Expected EOF (no error)
return nil, nil
}
if mr.partsRead == 0 {
// skip line
mr.consumeLine()
continue
}
return nil, os.NewError("Unexpected line in Next().")
}
panic("unreachable")
}
func isBoundaryDelimiterLine(line, dashPrefix string) bool {
// http://tools.ietf.org/html/rfc2046#section-5.1
// The boundary delimiter line is then defined as a line
// consisting entirely of two hyphen characters ("-",
// decimal value 45) followed by the boundary parameter
// value from the Content-Type header field, optional linear
// whitespace, and a terminating CRLF.
if !strings.HasPrefix(line, dashPrefix) {
return false
}
if strings.HasSuffix(line, "\r\n") {
return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-2])
}
// Violate the spec and also support newlines without the
// carriage return...
if strings.HasSuffix(line, "\n") {
return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-1])
}
return false
}
func onlyHorizontalWhitespace(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] != ' ' && s[i] != '\t' {
return false
}
}
return true
}
func hasPrefixThenNewline(s, prefix string) bool {
return strings.HasPrefix(s, prefix) &&
(len(s) == len(prefix)+1 && strings.HasSuffix(s, "\n") ||
len(s) == len(prefix)+2 && strings.HasSuffix(s, "\r\n"))
}

View File

@ -0,0 +1,204 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package multipart
import (
"bytes"
"fmt"
"io"
"json"
"regexp"
"strings"
"testing"
)
func TestHorizontalWhitespace(t *testing.T) {
if !onlyHorizontalWhitespace(" \t") {
t.Error("expected pass")
}
if onlyHorizontalWhitespace("foo bar") {
t.Error("expected failure")
}
}
func TestBoundaryLine(t *testing.T) {
boundary := "myBoundary"
prefix := "--" + boundary
if !isBoundaryDelimiterLine("--myBoundary\r\n", prefix) {
t.Error("expected")
}
if !isBoundaryDelimiterLine("--myBoundary \r\n", prefix) {
t.Error("expected")
}
if !isBoundaryDelimiterLine("--myBoundary \n", prefix) {
t.Error("expected")
}
if isBoundaryDelimiterLine("--myBoundary bogus \n", prefix) {
t.Error("expected fail")
}
if isBoundaryDelimiterLine("--myBoundary bogus--", prefix) {
t.Error("expected fail")
}
}
func escapeString(v string) string {
bytes, _ := json.Marshal(v)
return string(bytes)
}
func expectEq(t *testing.T, expected, actual, what string) {
if expected == actual {
return
}
t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)",
what, escapeString(actual), len(actual), escapeString(expected), len(expected))
}
func TestFormName(t *testing.T) {
p := new(Part)
p.Header = make(map[string]string)
tests := [...][2]string{
[2]string{`form-data; name="foo"`, "foo"},
[2]string{` form-data ; name=foo`, "foo"},
[2]string{`FORM-DATA;name="foo"`, "foo"},
[2]string{` FORM-DATA ; name="foo"`, "foo"},
[2]string{` FORM-DATA ; name="foo"`, "foo"},
[2]string{` FORM-DATA ; name=foo`, "foo"},
[2]string{` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo"},
}
for _, test := range tests {
p.Header["Content-Disposition"] = test[0]
expected := test[1]
actual := p.FormName()
if actual != expected {
t.Errorf("expected \"%s\"; got: \"%s\"", expected, actual)
}
}
}
func TestMultipart(t *testing.T) {
testBody := `
This is a multi-part message. This line is ignored.
--MyBoundary
Header1: value1
HEADER2: value2
foo-bar: baz
My value
The end.
--MyBoundary
Header1: value1b
HEADER2: value2b
foo-bar: bazb
Line 1
Line 2
Line 3 ends in a newline, but just one.
--MyBoundary
never read data
--MyBoundary--
`
testBody = regexp.MustCompile("\n").ReplaceAllString(testBody, "\r\n")
bodyReader := strings.NewReader(testBody)
reader := NewReader(bodyReader, "MyBoundary")
buf := new(bytes.Buffer)
// Part1
part, err := reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part1")
return
}
if part.Header["Header1"] != "value1" {
t.Error("Expected Header1: value")
}
if part.Header["foo-bar"] != "baz" {
t.Error("Expected foo-bar: baz")
}
buf.Reset()
io.Copy(buf, part)
expectEq(t, "My value\r\nThe end.",
buf.String(), "Value of first part")
// Part2
part, err = reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part2")
return
}
if part.Header["foo-bar"] != "bazb" {
t.Error("Expected foo-bar: bazb")
}
buf.Reset()
io.Copy(buf, part)
expectEq(t, "Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n",
buf.String(), "Value of second part")
// Part3
part, err = reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part3 without errors")
return
}
// Non-existent part4
part, err = reader.NextPart()
if part != nil {
t.Error("Didn't expect a third part.")
}
if err != nil {
t.Errorf("Unexpected error getting third part: %v", err)
}
}
func TestVariousTextLineEndings(t *testing.T) {
tests := [...]string{
"Foo\nBar",
"Foo\nBar\n",
"Foo\r\nBar",
"Foo\r\nBar\r\n",
"Foo\rBar",
"Foo\rBar\r",
"\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10",
}
for testNum, expectedBody := range tests {
body := "--BOUNDARY\r\n" +
"Content-Disposition: form-data; name=\"value\"\r\n" +
"\r\n" +
expectedBody +
"\r\n--BOUNDARY--\r\n"
bodyReader := strings.NewReader(body)
reader := NewReader(bodyReader, "BOUNDARY")
buf := new(bytes.Buffer)
part, err := reader.NextPart()
if part == nil {
t.Errorf("Expected a body part on text %d", testNum)
continue
}
if err != nil {
t.Errorf("Unexpected error on text %d: %v", testNum, err)
continue
}
written, err := io.Copy(buf, part)
expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum))
if err != nil {
t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err)
}
part, err = reader.NextPart()
if part != nil {
t.Errorf("Unexpected part in test %d", testNum)
}
if err != nil {
t.Errorf("Unexpected error in test %d: %v", testNum, err)
}
}
}

View File

@ -2,14 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The mime package translates file name extensions to MIME types.
// It consults the local system's mime.types file, which must be installed
// under one of these names:
//
// /etc/mime.types
// /etc/apache2/mime.types
// /etc/apache/mime.types
//
// The mime package implements parts of the MIME spec.
package mime
import (
@ -76,6 +69,14 @@ func initMime() {
// TypeByExtension returns the MIME type associated with the file extension ext.
// The extension ext should begin with a leading dot, as in ".html".
// When ext has no associated type, TypeByExtension returns "".
//
// The built-in table is small but is is augmented by the local
// system's mime.types file(s) if available under one or more of these
// names:
//
// /etc/mime.types
// /etc/apache2/mime.types
// /etc/apache/mime.types
func TypeByExtension(ext string) string {
once.Do(initMime)
return mimeTypes[ext]