Commit aa9c213e authored by Andrew Gerrand's avatar Andrew Gerrand

http: ServeFile to handle Range header for partial requests

and send Content-Length.

Also includes some testing of the server code.

R=rsc
CC=golang-dev
https://golang.org/cl/2831041
parent 8984fa8f
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"mime" "mime"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
"utf8" "utf8"
...@@ -130,6 +131,9 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) { ...@@ -130,6 +131,9 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) {
} }
// serve file // serve file
size := d.Size
code := StatusOK
// use extension to find content type. // use extension to find content type.
ext := path.Ext(name) ext := path.Ext(name)
if ctype := mime.TypeByExtension(ext); ctype != "" { if ctype := mime.TypeByExtension(ext); ctype != "" {
...@@ -137,16 +141,40 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) { ...@@ -137,16 +141,40 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) {
} else { } else {
// read first chunk to decide between utf-8 text and binary // read first chunk to decide between utf-8 text and binary
var buf [1024]byte var buf [1024]byte
n, _ := io.ReadFull(f, buf[0:]) n, _ := io.ReadFull(f, buf[:])
b := buf[0:n] b := buf[:n]
if isText(b) { if isText(b) {
w.SetHeader("Content-Type", "text-plain; charset=utf-8") w.SetHeader("Content-Type", "text-plain; charset=utf-8")
} else { } else {
w.SetHeader("Content-Type", "application/octet-stream") // generic binary w.SetHeader("Content-Type", "application/octet-stream") // generic binary
} }
w.Write(b) f.Seek(0, 0) // rewind to output whole file
}
// handle Content-Range header.
// TODO(adg): handle multiple ranges
ranges, err := parseRange(r.Header["Range"], size)
if err != nil || len(ranges) > 1 {
Error(w, err.String(), StatusRequestedRangeNotSatisfiable)
return
}
if len(ranges) == 1 {
ra := ranges[0]
if _, err := f.Seek(ra.start, 0); err != nil {
Error(w, err.String(), StatusRequestedRangeNotSatisfiable)
return
}
size = ra.length
code = StatusPartialContent
w.SetHeader("Content-Range", fmt.Sprintf("%d-%d/%d", ra.start, ra.start+ra.length, d.Size))
} }
io.Copy(w, f)
w.SetHeader("Accept-Ranges", "bytes")
w.SetHeader("Content-Length", strconv.Itoa64(size))
w.WriteHeader(code)
io.Copyn(w, f, size)
} }
// ServeFile replies to the request with the contents of the named file or directory. // ServeFile replies to the request with the contents of the named file or directory.
...@@ -174,3 +202,62 @@ func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { ...@@ -174,3 +202,62 @@ func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
path = path[len(f.prefix):] path = path[len(f.prefix):]
serveFile(w, r, f.root+"/"+path, true) serveFile(w, r, f.root+"/"+path, true)
} }
// httpRange specifies the byte range to be sent to the client.
type httpRange struct {
start, length int64
}
// parseRange parses a Range header string as per RFC 2616.
func parseRange(s string, size int64) ([]httpRange, os.Error) {
if s == "" {
return nil, nil // header not present
}
const b = "bytes="
if !strings.HasPrefix(s, b) {
return nil, os.NewError("invalid range")
}
var ranges []httpRange
for _, ra := range strings.Split(s[len(b):], ",", -1) {
i := strings.Index(ra, "-")
if i < 0 {
return nil, os.NewError("invalid range")
}
start, end := ra[:i], ra[i+1:]
var r httpRange
if start == "" {
// If no start is specified, end specifies the
// range start relative to the end of the file.
i, err := strconv.Atoi64(end)
if err != nil {
return nil, os.NewError("invalid range")
}
if i > size {
i = size
}
r.start = size - i
r.length = size - r.start
} else {
i, err := strconv.Atoi64(start)
if err != nil || i > size || i < 0 {
return nil, os.NewError("invalid range")
}
r.start = i
if end == "" {
// If no end is specified, range extends to end of the file.
r.length = size - r.start
} else {
i, err := strconv.Atoi64(end)
if err != nil || r.start > i {
return nil, os.NewError("invalid range")
}
if i >= size {
i = size - 1
}
r.length = i - r.start + 1
}
}
ranges = append(ranges, r)
}
return ranges, nil
}
// 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 http
import (
"fmt"
"io/ioutil"
"net"
"os"
"sync"
"testing"
)
var ParseRangeTests = []struct {
s string
length int64
r []httpRange
}{
{"", 0, nil},
{"foo", 0, nil},
{"bytes=", 0, nil},
{"bytes=5-4", 10, nil},
{"bytes=0-2,5-4", 10, nil},
{"bytes=0-9", 10, []httpRange{{0, 10}}},
{"bytes=0-", 10, []httpRange{{0, 10}}},
{"bytes=5-", 10, []httpRange{{5, 5}}},
{"bytes=0-20", 10, []httpRange{{0, 10}}},
{"bytes=15-,0-5", 10, nil},
{"bytes=-5", 10, []httpRange{{5, 5}}},
{"bytes=-15", 10, []httpRange{{0, 10}}},
{"bytes=0-499", 10000, []httpRange{{0, 500}}},
{"bytes=500-999", 10000, []httpRange{{500, 500}}},
{"bytes=-500", 10000, []httpRange{{9500, 500}}},
{"bytes=9500-", 10000, []httpRange{{9500, 500}}},
{"bytes=0-0,-1", 10000, []httpRange{{0, 1}, {9999, 1}}},
{"bytes=500-600,601-999", 10000, []httpRange{{500, 101}, {601, 399}}},
{"bytes=500-700,601-999", 10000, []httpRange{{500, 201}, {601, 399}}},
}
func TestParseRange(t *testing.T) {
for _, test := range ParseRangeTests {
r := test.r
ranges, err := parseRange(test.s, test.length)
if err != nil && r != nil {
t.Errorf("parseRange(%q) returned error %q", test.s, err)
}
if len(ranges) != len(r) {
t.Errorf("len(parseRange(%q)) = %d, want %d", test.s, len(ranges), len(r))
continue
}
for i := range r {
if ranges[i].start != r[i].start {
t.Errorf("parseRange(%q)[%d].start = %d, want %d", test.s, i, ranges[i].start, r[i].start)
}
if ranges[i].length != r[i].length {
t.Errorf("parseRange(%q)[%d].length = %d, want %d", test.s, i, ranges[i].length, r[i].length)
}
}
}
}
const (
testFile = "testdata/file"
testFileLength = 11
)
var (
serverOnce sync.Once
serverAddr string
)
func startServer(t *testing.T) {
serverOnce.Do(func() {
HandleFunc("/ServeFile", func(w ResponseWriter, r *Request) {
ServeFile(w, r, "testdata/file")
})
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("listen:", err)
}
serverAddr = l.Addr().String()
go Serve(l, nil)
})
}
var ServeFileRangeTests = []struct {
start, end int
r string
code int
}{
{0, testFileLength, "", StatusOK},
{0, 5, "0-4", StatusPartialContent},
{2, testFileLength, "2-", StatusPartialContent},
{testFileLength - 5, testFileLength, "-5", StatusPartialContent},
{3, 8, "3-7", StatusPartialContent},
{0, 0, "20-", StatusRequestedRangeNotSatisfiable},
}
func TestServeFile(t *testing.T) {
startServer(t)
var err os.Error
file, err := ioutil.ReadFile(testFile)
if err != nil {
t.Fatal("reading file:", err)
}
// set up the Request (re-used for all tests)
var req Request
req.Header = make(map[string]string)
if req.URL, err = ParseURL("http://" + serverAddr + "/ServeFile"); err != nil {
t.Fatal("ParseURL:", err)
}
req.Method = "GET"
// straight GET
_, body := getBody(t, req)
if !equal(body, file) {
t.Fatalf("body mismatch: got %q, want %q", body, file)
}
// Range tests
for _, rt := range ServeFileRangeTests {
req.Header["Range"] = "bytes=" + rt.r
if rt.r == "" {
req.Header["Range"] = ""
}
r, body := getBody(t, req)
if r.StatusCode != rt.code {
t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, r.StatusCode, rt.code)
}
if rt.code == StatusRequestedRangeNotSatisfiable {
continue
}
h := fmt.Sprintf("%d-%d/%d", rt.start, rt.end, testFileLength)
if rt.r == "" {
h = ""
}
if r.Header["Content-Range"] != h {
t.Errorf("header mismatch: range=%q: got %q, want %q", rt.r, r.Header["Content-Range"], h)
}
if !equal(body, file[rt.start:rt.end]) {
t.Errorf("body mismatch: range=%q: got %q, want %q", rt.r, body, file[rt.start:rt.end])
}
}
}
func getBody(t *testing.T, req Request) (*Response, []byte) {
r, err := send(&req)
if err != nil {
t.Fatal(req.URL.String(), "send:", err)
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatal("reading Body:", err)
}
return r, b
}
func equal(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment